diff --git a/.gitignore b/.gitignore index 09a8ec79c5..67f1e82fb4 100644 --- a/.gitignore +++ b/.gitignore @@ -78,4 +78,6 @@ xcode-files .bsp/** .sourcekit-lsp/** /.claude/ +**/.claude/settings.local.json +**/.vscode/launch.json /buildbox/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 47725bea31..d30916ab33 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,12 +22,10 @@ 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 - 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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..8a6239dbda --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,149 @@ +# CLAUDE.md + +This file provides guidance to AI assistants when working with code in this repository. + +## Build + +The app is built using Bazel via the `Make.py` wrapper. There is no selective per-module build — the only supported invocation builds the full `Telegram/Telegram` target. + +**Command:** + +```sh +python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 +``` + +Add `--continueOnError` after `build` (forwards to bazel's `--keep_going`) when verifying changes that may surface errors in many files at once — it lets the full set of errors land in one pass instead of stopping at the first failing target. + +The build needs `TELEGRAM_CODESIGNING_GIT_PASSWORD` in the environment. It is set in `~/.zshrc` but Claude Code's bash tool does NOT source shell config by default. Prefix build commands with `source ~/.zshrc 2>/dev/null;` to pick it up. + +## Code Style Guidelines +- **Naming**: PascalCase for types, camelCase for variables/methods +- **Imports**: Group and sort imports at the top of files +- **Error Handling**: Properly handle errors with appropriate redaction of sensitive data +- **Formatting**: Use standard Swift/Objective-C formatting and spacing +- **Types**: Prefer strong typing and explicit type annotations where needed +- **Documentation**: Document public APIs with comments + +## Project Structure +- Core launch and application extensions code is in `Telegram/` directory +- Most code is organized into libraries in `submodules/` +- External code is located in `third-party/` +- No tests are used at the moment + +## Postbox → TelegramEngine refactor (in progress) + +A gradual migration is underway to eliminate direct `import Postbox` from consumer submodules in favor of `TelegramEngine`. + +**Historical record:** Wave-by-wave outcomes, the running tally of Postbox-free modules, and full verbose forms of the guidance subsections below live in [`docs/superpowers/postbox-refactor-log.md`](docs/superpowers/postbox-refactor-log.md). Read that file when you need wave-specific context, a full worked example of a pattern, or the history of a particular module's migration. + +Waves landed so far (as of 2026-04-24): 45 waves plus standalone cleanups. See the log file for per-wave detail; the list of still-open migration opportunities lives in the `project_postbox_refactor_next_wave.md` memory file. + +### Rules that apply to every wave + +1. `TelegramCore` does **not** `@_exported import Postbox`. Once a consumer drops `import Postbox`, every remaining Postbox-type reference must use an engine-typealiased equivalent. +2. **Never typealias `Postbox`, `Account`, or `MediaBox`.** These umbrella types rename without encapsulating. Narrow utility typealiases (`MemoryBuffer`, `PostboxDecoder`, `PostboxEncoder`, `AdaptedPostboxDecoder`, `MediaResource`, …) remain allowed and expected. +3. No new engine wrapper **structs** unless the wave's spec explicitly allows — only typealiases and thin forwarding methods. +4. **Discovery first:** before adding any new engine wrapper/typealias, grep `submodules/TelegramCore/Sources/TelegramEngine/` for existing equivalents. Record the search result in the commit message. +5. **Abandonment protocol:** if a module can only be refactored by violating rule 2 or by editing a module outside the current wave's list, mark the task Abandoned with a recorded reason. Do NOT substitute a new module mid-wave. +6. Full project build per module. No unit tests exist in this project. +7. **TelegramCore never imports UIKit/Display.** `TelegramCore` is shared with the Telegram-Mac codebase; its Bazel `deps` and source files must not reference UIKit, Display, or any Apple-UI framework. UIKit-needing helpers (image scaling, rendering, etc.) stay in consumer-side submodules. + +### Engine typealias cheat sheet (existing aliases) + +``` +PeerId → EnginePeer.Id +MessageId → EngineMessage.Id +MessageIndex → EngineMessage.Index +MessageTags → EngineMessage.Tags +MessageAttribute → EngineMessage.Attribute +MessageFlags → EngineMessage.Flags +MessageForwardInfo → EngineMessage.ForwardInfo +MediaId → EngineMedia.Id +PreferencesEntry → EnginePreferencesEntry +TempBox → EngineTempBox +PinnedItemId → EngineChatList.PinnedItem.Id +MemoryBuffer → EngineMemoryBuffer (added 2026-04) +PostboxDecoder → EnginePostboxDecoder (added 2026-04) +PostboxEncoder → EnginePostboxEncoder (added 2026-04) +AdaptedPostboxDecoder → EngineAdaptedPostboxDecoder (added 2026-04) +ItemCollectionId → EngineItemCollectionId (added 2026-04-20) +FetchResourceSourceType → EngineFetchResourceSourceType (added 2026-04-20) +FetchResourceError → EngineFetchResourceError (added 2026-04-20) +``` + +For the `MediaResource` Postbox protocol, prefer the TelegramCore subtype `TelegramMediaResource` when the consumer's usage allows (note: `EngineMediaResource` is a wrapper **class**, not a typealias, so it is not interchangeable with the protocol). + +### MediaResource → EngineMediaResource consumer migration + +`EngineMediaResource` is a `final class` in `TelegramCore` wrapping a `MediaResource` value. Unlike the typealiases above it is **not** interchangeable with the protocol, but it does provide wrap/unwrap helpers: + +- `EngineMediaResource(rawResource)` — wrap a raw `MediaResource`. +- `engineResource._asResource()` — unwrap to the raw `MediaResource`. +- `EngineMediaResource.ResourceData(rawResourceData)` — wrap `MediaResourceData`. +- `EngineMediaResource.Id(rawMediaResourceId)` — wrap `MediaResourceId`. + +**Pattern for facade functions:** when a `TelegramEngine.` method leaks raw `MediaResource` in its public signature, **change the facade signature in place** to `EngineMediaResource` (and change any closure parameter types the same way). Bridge inside the facade body by calling the existing `_internal_*` function with `engineResource._asResource()` / wrapping raw inputs from inner closures with `EngineMediaResource(rawResource)`. Update all call sites in the same commit. The `_internal_*` function stays on raw `MediaResource` — it is the Postbox-facing layer. + +Do **not** add opt-in `EngineMediaResource` overloads alongside raw-`MediaResource` overloads. Duplicate signatures fragment the public API and leave the leak in place forever. + +For consumer modules, prefer `EngineMediaResource` as the type in properties, locals, generic arguments and function parameters when the usage is a pure type reference. Do **not** try to use `EngineMediaResource` where a class must conform to `TelegramMediaResource` (Postbox protocol) or override `isEqual(to: MediaResource)` — those remain `import Postbox`. + +### Wave-selection guidance + +Distilled lessons from waves 1–26. Each bullet below has a full-form counterpart in `postbox-refactor-log.md` (same subsection heading) with backstory, example scripts, and per-wave numbers. + +**Shape selection.** The "leaf module, drop Postbox in isolation" approach (wave 1) only works when the candidate's public API doesn't leak Postbox domain types. Most candidates DO leak (`postbox: Postbox` / `account: Account` in public inits, `Media`/`Message` as public parameter types). Grep each candidate for `:\s*Postbox\b`, `:\s*Account\b`, `:\s*MediaBox\b`, and `Media`/`Message` as public parameter types before committing to a wave; abandon candidates whose public API leaks. + +**Inventory at execution time, not just planning time.** Planning-time grep often undercounts. Re-inventory at Task-1 time using the full token set `\b(postbox|mediaBox|transaction|PostboxView|combinedView|MediaResource|PostboxDecoder|PostboxEncoder|MemoryBuffer)\b|^import Postbox` over the module's sources. If the count exceeds the plan, abandon before editing code rather than substituting a different module. + +**Two feasible wave shapes.** Shape 1 = "per-module Postbox drop" (fragile; wave 1 lost 6 of 10 candidates). Shape 2 = "per-engine-facade-API migrate in place, update all call sites in one commit" (validated from wave 2 onward). Prefer shape 2 when the target is an API surface that multiple consumer modules depend on. + +**Enum-payload migrations need full case-site grep.** When changing the payload type of a public enum, grep `case \.` / `let \.` / `\.\(` across the enum's defining module — not just call sites of the facade that returns it. Wave 4 undercounted by 6 sites (shortcut constructions and destructures inside the same file as the facade) because the inventory only grepped facade callers. + +**Unused-import sweeps** (wave-shape applied in waves 6, 14). Speculatively drop `^import Postbox$` from every candidate file, build with `--continueOnError`, extract failing files and restore their imports, iterate. After a few iterations, do pattern-based preemptive restores for files naming Postbox-only symbols (`MediaBox`, `PostboxCoding`, `PostboxDecoder`, `PostboxEncoder`, `TempBoxFile`, `ValueBoxKey`, `Postbox\b`, `PeerId`, `MessageId`, `MediaId`, `MessageIndex`, `MessageAndThreadId`, `PeerNameIndex`). Scope never leaves the consumer-module candidate set — halt if errors surface in TelegramCore / Postbox / TelegramApi. Run a matching BUILD-dep sweep immediately after (near-zero execution risk). Full methodology, scripts, and iteration-count history in the log. + +**Public-Postbox-type inventory** (wave-11-pattern planning). Grep candidate modules against the full Postbox public-types allowlist, not just the pattern's target tokens. Waves before 16 missed types like `EngineMessageHistoryThread.Info` (Postbox-defined despite its "Engine" prefix) and `PeerStoryStats`. "Engine"-prefixed types can still be Postbox-defined — grep for the defining module, don't trust naming. Build allowlist with `grep -rhE "^public\s+(class|struct|enum|protocol|typealias)\s+\w+" submodules/Postbox/Sources/ | awk '{print $3}' | sed 's/[(:<].*//' | sort -u`, then grep candidates against it. Full script in the log. + +**Wave-shape G: facade addition + consumer sweep in one commit** (validated across waves 19–26). Recipe: +1. Target a `MediaBox` method whose Postbox signature uses clean leaf types (`MediaResourceId`, `Data`, `String`, `Bool`) and whose return type is either non-Postbox or has an existing `Engine*` wrapper. +2. Pre-flight inventory: classify each call site as Shape A (`context.account.postbox.mediaBox.X(...)`, migratable), Shape B (different overload via `AccountContext`, migratable), Shape C (raw `account: Account` local, skip — needs per-module rework), Shape D (`self.postbox` stored field, skip). Also check for `accountManager.mediaBox.X(...)` — a separate migration path. +3. Design facade with `EngineMediaResource.Id` or `EngineMediaResource` parameters and engine-or-clean return types; preserve default argument values. +4. WIP-interference check: `git status --short | grep -v "^??"` — if any Shape-A site is in a WIP file, either skip those sites or wait. +5. Name-collision check: if the facade signature names a Swift stdlib type with availability restrictions (`RangeSet`, iOS 18+), verify the third-party module import is present in `TelegramEngineResources.swift`. +6. Batch duplicate call expressions with `replace_all=true`. +7. Cheapness: 5–50 sites per wave, single atomic commit, expected first-pass-clean build. If post-migration grep for the migrated expression returns empty (excluding Shape C/D) and build is green, commit. + +Full per-shape recipe and wave-specific examples in the log. + +### TelegramEngine.Resources facade inventory (as of wave 32) + +All mediaBox methods with clean signatures (no Postbox-protocol leaks, no complex return-type migrations) have been migrated to `TelegramEngine.Resources`. Quick reference for consumers — all of these live in `submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift`: + +| Facade | Wave | Wraps | +|---|---|---| +| `fetch(reference:userLocation:userContentType:)` | 3 | `fetchedMediaResource` | +| `status(resource:)` | 3 | `MediaBox.resourceStatus` (resource-based) | +| `status(id:, resourceSize:)` | 32 | `MediaBox.resourceStatus(_ id:, resourceSize:)` | +| `data(resource:, pathExtension:, waitUntilFetchStatus:)` | 3 | `MediaBox.resourceData` (resource-based) | +| `data(id:, attemptSynchronously:)` | 3 | `MediaBox.resourceData` (id-based, defaults to `.complete(waitUntilFetchStatus: false)`) | +| `custom(id:, fetch:, cacheTimeout:, attemptSynchronously:)` | pre-wave-21 | `MediaBox.customResourceData` | +| `httpData(url:, preserveExactUrl:)` | pre-wave-21 | `fetchHttpResource` | +| `shortLivedResourceCachePathPrefix(id:)` | 19 | `MediaBox.shortLivedResourceCachePathPrefix` | +| `completedResourcePath(id:, pathExtension:)` | 21 | `MediaBox.completedResourcePath(id:, pathExtension:)` | +| `storeResourceData(id:, data:, synchronous:)` | 22 | `MediaBox.storeResourceData(_ id:, data:, synchronous:)` | +| `cancelInteractiveResourceFetch(id:)` | 23 | `MediaBox.cancelInteractiveResourceFetch(resourceId:)` | +| `moveResourceData(id:, toTempPath:)` | 24 | `MediaBox.moveResourceData(_ id:, toTempPath:)` | +| `moveResourceData(from:, to:, synchronous:)` | 24 | `MediaBox.moveResourceData(from:, to:, synchronous:)` | +| `copyResourceData(id:, fromTempPath:)` | 25 | `MediaBox.copyResourceData(_ id:, fromTempPath:)` | +| `copyResourceData(from:, to:, synchronous:)` | 25 | `MediaBox.copyResourceData(from:, to:, synchronous:)` | +| `resourceRangesStatus(resource:)` | 26 | `MediaBox.resourceRangesStatus(_ resource:)` | +| `removeCachedResources(ids:, force:, notify:)` | 26 | `MediaBox.removeCachedResources(_ ids:, force:, notify:)` | + +**Facade-shape convention:** all of these take `EngineMediaResource.Id` or `EngineMediaResource` (never raw `MediaResourceId`/`MediaResource`). Return types either don't leak Postbox (`Void`, `String`, `String?`, `Signal, NoError>`, `Signal`) or wrap via TelegramCore type (`Signal`). + +**Swift-stdlib-vs-third-party-module name collisions** (learned in wave 26): `RangeSet` collides with Swift stdlib's `RangeSet` (iOS 18+ only). Fix: `import RangeSet` at the file top of any TelegramCore file that names `RangeSet` in a signature. `TelegramCore/BUILD` already depends on `//submodules/Utils/RangeSet:RangeSet`. Future facade additions in TelegramEngineResources.swift should re-check this if new signature types are introduced. diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index f4d57a995e..7cb1fb2643 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -13,7 +13,6 @@ "https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1", "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215", "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/source.json": "d725d73707d01bb46ab3ca59ba408b8e9bd336642ca77a2269d4bfb8bbfd413d", - "https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b", "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", "https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d", @@ -23,12 +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.36.0/MODULE.bazel": "596cb62090b039caf1cad1d52a8bc35cf188ca9a4e279a828005e7ee49a1bec3", - "https://bcr.bazel.build/modules/bazel_features/1.36.0/source.json": "279625cafa5b63cc0a8ee8448d93bc5ac1431f6000c50414051173fd22a6df3c", "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", @@ -42,15 +41,11 @@ "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/gazelle/0.32.0/MODULE.bazel": "b499f58a5d0d3537f3cf5b76d8ada18242f64ec474d8391247438bf04f58c7b8", - "https://bcr.bazel.build/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350", - "https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a", - "https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0", - "https://bcr.bazel.build/modules/gazelle/0.43.0/MODULE.bazel": "846e1fe396eefc0f9ddad2b33e9bd364dd993fc2f42a88e31590fe0b0eefa3f0", - "https://bcr.bazel.build/modules/gazelle/0.43.0/source.json": "021a77f6625906d9d176e2fa351175e842622a5d45989312f2ad4924aab72df6", "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", @@ -80,12 +75,9 @@ "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/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573", - "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", - "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", @@ -107,19 +99,13 @@ "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_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8", - "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270", - "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd", - "https://bcr.bazel.build/modules/rules_go/0.50.1/MODULE.bazel": "b91a308dc5782bb0a8021ad4330c81fea5bda77f96b9e4c117b9b9c8f6665ee0", - "https://bcr.bazel.build/modules/rules_go/0.60.0/MODULE.bazel": "4a57ff2ffc2a3570e3c5646575c5a4b07287e91bcdac5d1f72383d51502b48cb", - "https://bcr.bazel.build/modules/rules_go/0.60.0/source.json": "1e21368c5e0c3013a110bd79a8fcff8ca46b5bcb2b561713a7273cbfcff7c464", "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", "https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39", @@ -156,26 +142,24 @@ "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", "https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483", - "https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f", "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", @@ -183,11 +167,10 @@ "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.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198" @@ -258,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": {}, diff --git a/Random.txt b/Random.txt index c407a4cb13..66824569ca 100644 --- a/Random.txt +++ b/Random.txt @@ -1 +1 @@ -06de25b179c80e59 +c27f02bf6e413fdc diff --git a/Telegram/BUILD b/Telegram/BUILD index a1cf7d9524..22655f3a9b 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -488,7 +488,7 @@ icloud_fragment = "" if not telegram_enable_icloud else """ iCloud.{telegram_bundle_id} com.apple.developer.ubiquity-kvstore-identifier -{telegram_team_id}.* +{telegram_team_id}.{telegram_bundle_id} com.apple.developer.icloud-container-environment {telegram_icloud_environment} """.format( @@ -1635,7 +1635,6 @@ plist_fragment( UIAppFonts - SFCompactRounded-Semibold.otf AremacFS-Regular.otf AremacFS-Semibold.otf diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index 72292568a5..d52e425248 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -2958,7 +2958,7 @@ extension Customoji { if let cg = (image as UIImage).cgImage { return cg } var rendered: CGImage? - let work = { rendered = renderCGImage(image as! UIImage) } + let work = { rendered = renderCGImage(image) } if Thread.isMainThread { work() } else { 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/Telegram/Telegram-iOS/Resources/IntroCall.tgs b/Telegram/Telegram-iOS/Resources/IntroCall.tgs deleted file mode 100644 index b819c8302f..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/IntroCall.tgs and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/intro/start_arrow@2x.png b/Telegram/Telegram-iOS/Resources/intro/start_arrow@2x.png deleted file mode 100644 index fe59b3e2b5..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/intro/start_arrow@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/intro/start_arrow_ipad.png b/Telegram/Telegram-iOS/Resources/intro/start_arrow_ipad.png deleted file mode 100644 index eaa2cd6dd1..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/intro/start_arrow_ipad.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/Resources/intro/start_arrow_ipad@2x.png b/Telegram/Telegram-iOS/Resources/intro/start_arrow_ipad@2x.png deleted file mode 100644 index 08a47e44ac..0000000000 Binary files a/Telegram/Telegram-iOS/Resources/intro/start_arrow_ipad@2x.png and /dev/null differ diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 73d3543c70..117c2807bb 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -2067,6 +2067,7 @@ "StickerPack.Share" = "Share"; "StickerPack.Send" = "Send Sticker"; "StickerPack.AddSticker" = "Add Sticker"; +"StickerPack.RemoveStickerSet" = "Remove Sticker Set"; "StickerPack.RemoveStickerCount_1" = "Remove 1 Sticker"; "StickerPack.RemoveStickerCount_2" = "Remove 2 Stickers"; @@ -2706,6 +2707,7 @@ Unused sets are archived when you add more."; "Channel.AdminLog.AddMembers" = "Add Members"; "Channel.AdminLog.SendPolls" = "Send Polls"; "Channel.AdminLog.ManageTopics" = "Manage Topics"; +"Channel.AdminLog.SendReactions" = "Send Reactions"; "Channel.AdminLog.CanChangeInfo" = "Change Info"; "Channel.AdminLog.CanSendMessages" = "Post Messages"; @@ -8771,6 +8773,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."; @@ -16153,6 +16156,41 @@ Error: %8$@"; "TextProcessing.ResultBadge" = "Result"; "TextProcessing.Translate.LanguageStyle" = "%1$@ (%2$@)"; "TextProcessing.StyleTooltip" = "Select Style"; +"TextProcessing.AlertCreatorDeleteStyle.Title" = "Delete Style"; +"TextProcessing.AlertCreatorDeleteStyle.Text" = "Are you sure you want to delete this style? It will be removed for everyone who installed it."; +"TextProcessing.AlertDeleteStyle.Title" = "Delete Style"; +"TextProcessing.AlertDeleteStyle.Text" = "Are you sure you want to delete this style?"; +"TextProcessing.StyleMenu.Edit" = "Edit Style"; +"TextProcessing.StyleMenu.Share" = "Share Style"; +"TextProcessing.StyleMenu.Delete" = "Delete Style"; +"TextProcessing.StyleMenu.ButtonClose" = "Close"; +"TextProcessing.StyleMenu.ButtonAdd" = "Add Style"; +"TextProcessing.StylePreview.ExampleHeader" = "EXAMPLE"; +"TextProcessing.StylePreview.ExampleHeaderRefresh" = "ANOTHER EXAMPLE"; +"TextProcessing.StylePreview.Subtitle" = "Add this style to instantly\nrewrite your messages."; +"TextProcessing.StylePreview.Before" = "Before"; +"TextProcessing.StylePreview.After" = "After"; +"TextProcessing.AlertTooManyStyles.Title" = "Too Many Styles"; +"TextProcessing.AlertTooManyStyles.Text" = "Please delete some of your saved styles to create a new one."; +"TextProcessing.ToastStyleCreated.Title" = "%@ style created!"; +"TextProcessing.ToastStyleCreated.Text" = "Press and hold a style to edit or share the link."; +"TextProcessing.StyleList.Add" = "Add Style"; +"TextProcessing.StyleFooterAuthor" = "Style by [%@]()"; +"TextProcessing.StyleFooterUserCount_1" = "Used by 1 person"; +"TextProcessing.StyleFooterUserCount_any" = "Used by %d people"; +"TextProcessing.StyleFooterCreatedByFormat" = "%1$@. %2$@"; +"TextProcessing.StyleFooterCreatedBy" = "Created by [%@]()"; +"TextProcessing.StyleFooterCreatedBySimpleFormat" = "%@."; +"TextProcessing.EditStyle.NamePlaceholder" = "Style Name (for example, \"Pirate\")"; +"TextProcessing.EditStyle.TextPlaceholder" = "Instructions (for example, \"Write like a swashbuckling pirate. Use arr, ye, matey, and talk about treasure, the sea, and rum\")"; +"TextProcessing.EditStyle.TitleCreate" = "New Style"; +"TextProcessing.EditStyle.TitleEdit" = "Edit Style"; +"TextProcessing.EditStyle.ActionCreate" = "Create"; +"TextProcessing.EditStyle.ActionEdit" = "Save"; +"TextProcessing.EditStyle.Delete" = "Delete Style"; +"TextProcessing.EditStyle.AddLink" = "Add a link to my account"; +"TextProcessing.ToastStyleAdded.Title" = "Style Added"; +"TextProcessing.ToastStyleAdded.Text" = "Tap 'AI' → '%@' when typing your next long message."; "Bot.AlertCanNotCreateBots" = "%@ can't manage other bots."; @@ -16168,3 +16206,103 @@ 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.OptionsNeededOne" = "Add at least one option"; +"CreatePoll.QuizCorrectOptionNeeded" = "Select a correct option"; +"CreatePoll.QuizCorrectOptionNeededMultiple" = "Select at least one correct option"; +"CreatePoll.QuizCountryNeeded" = "Select at least one country"; + +"Stars.Intro.Transaction.Commission.Title" = "%@ commission"; + +"Conversation.ViewPollStats" = "View Statistics"; +"PollStats.Title" = "Poll Stats"; +"PollStats.GraphHeader" = "VOTE TIMELINE"; + +"CreatePoll.RestrictToSubscribers" = "Restrict to Subscribers"; +"CreatePoll.RestrictToSubscribersInfo" = "Only subscribers who joined 24+ hours ago can vote"; + +"CreatePoll.LimitCountry" = "Limit by Country"; +"CreatePoll.LimitCountryInfo" = "Only users from selected countries can vote"; +"CreatePoll.AllowedCountries" = "Allowed Countries"; +"CreatePoll.AllowedCountries.Countries_1" = "%@ country"; +"CreatePoll.AllowedCountries.Countries_any" = "%@ countries"; + +"Chat.Poll.Restriction.Subscribers" = "Only subscribers of **%@** can vote."; +"Chat.Poll.Restriction.Subscribers.TimeLimit" = "Only subscribers who joined more than **24 hours** ago can vote."; +"Chat.Poll.Restriction.Country" = "Only users from %@ can vote."; +"Chat.Poll.Restriction.SubscribersCountry" = "Only subscribers of **%@** from %@ can vote."; +"Chat.Poll.Restriction.Country.CountriesDelimiter" = ", "; +"Chat.Poll.Restriction.Country.CountriesLastDelimiter" = " and "; + +"Conversation.MessageGuestChatForUser" = "for %@"; + +"Settings.About.PrivacyHelpEmpty" = "A few words about you."; +"Settings.About.PrivacyHelpEveryone" = "Everyone can see your bio. [Change >]()"; +"Settings.About.PrivacyHelpContacts" = "Only your contacts can see your bio. [Change >]()"; +"Settings.About.PrivacyHelpNobody" = "Nobody can see your bio. [Change >]()"; + +"Settings.Birthday.PrivacyHelpEveryone" = "Everyone can see your birthday. [Change >]()"; +"Settings.Birthday.PrivacyHelpContacts" = "Only your contacts can see your birthday. [Change >]()"; +"Settings.Birthday.PrivacyHelpNobody" = "Nobody can see your birthday. [Change >]()"; + +"GroupPermission.NoSendReactions" = "no reactions"; +"Channel.BanUser.PermissionSendReactions" = "Send Reactions"; + +"Chat.AdminAction.ToastReactionsDeletedTitleSingle" = "Reaction Deleted"; +"Chat.AdminAction.ToastReactionsDeletedTextSingle" = "Reaction Deleted."; +"Chat.AdminAction.ToastReactionsDeletedTextMultiple" = "Reactions Deleted."; +"Chat.AdminAction.ToastMessagesAndReactionsDeletedText" = "Messages and reactions deleted."; + +"Premium.SignUp.SignUpNewInfo" = "Get Telegram Premium for %@"; +"Premium.SignUp.SignUpNewInfo.Days_1" = "%@ day"; +"Premium.SignUp.SignUpNewInfo.Days_any" = "%@ days"; +"Premium.SignUp.SignUpNewInfoNone" = "Get Telegram Premium"; + +"Login.Fee.Support.NewText.Days_1" = "%@ day"; +"Login.Fee.Support.NewText.Days_any" = "%@ days"; +"Login.Fee.Support.NewText" = "Sign up for a %@ Telegram Premium subscription to help cover the SMS costs."; +"Login.Fee.Support.NewTextNone" = "Sign up for Telegram Premium subscription to help cover the SMS costs."; + +"Login.Fee.GetPremiumNone" = "Get Telegram Premium"; +"Login.Fee.GetPremiumForDays" = "Get Telegram Premium for %@"; +"Login.Fee.GetPremiumForDays.Days_1" = "%@ day"; +"Login.Fee.GetPremiumForDays.Days_any" = "%@ days"; + +"PeerInfo.DeleteReaction" = "Delete Reaction"; +"Chat.DeleteReactionInfo" = "Tap and hold to delete reaction."; + +"Chat.AdminActionSheet.DeleteReactionTitle" = "Delete 1 Reaction"; +"Chat.AdminActionSheet.DeleteAllMessages" = "Delete All Messages"; +"Chat.AdminActionSheet.DeleteAllReactions" = "Delete All Reactions"; + +"Conversation.CalendarSearch.Title" = "Search"; +"Conversation.CalendarSearch.Done" = "Done"; + +"ScheduleMessage.SilentPosting.YouEnabled" = "You will receive a silent notification"; +"ScheduleMessage.SilentPosting.YouDisabled" = "You will be notified"; +"ScheduleMessage.SilentPosting.UserEnabled" = "%@ will receive a silent notification"; +"ScheduleMessage.SilentPosting.UserDisabled" = "%@ will be notified"; +"ScheduleMessage.SilentPosting.GroupEnabled" = "Members will receive a silent notification"; +"ScheduleMessage.SilentPosting.GroupDisabled" = "Members will be notified"; +"ScheduleMessage.SilentPosting.ChannelEnabled" = "Subscribers will receive a silent notification"; +"ScheduleMessage.SilentPosting.ChannelDisabled" = "Subscribers will be notified"; + +"Settings.ChatAutomation" = "Chat Automation"; +"Settings.ChatAutomationInfo" = "Add a bot to reply to messages on your behalf."; +"Settings.ChatAutomationOff" = "Off"; + +"Chat.SendReactionRestricted" = "You cannot send reactions in this chat."; + +"ChatbotSetup.BotInstalled" = "%@ now manages your account."; + +"ChatbotSetup.SetupNotCompleted.Title" = "No Bot Added"; +"ChatbotSetup.SetupNotCompleted.Text" = "You haven’t added a bot to manage your account. Leave anyway?"; +"ChatbotSetup.SetupNotCompleted.Leave" = "Leave"; + +"Chat.SavedMessagesStatusViewAsChats" = "Tap to view as chats"; +"Chat.ToastVoiceMessageDeviceMuted" = "Device is muted."; + +"VideoChat.StatusPeerJoined" = "%@ joined"; +"VideoChat.StatusPeerLeft" = "%@ left"; 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/Tests/AnimationCacheTest/BUILD b/Tests/AnimationCacheTest/BUILD index b42fca4697..43d5b08ae6 100644 --- a/Tests/AnimationCacheTest/BUILD +++ b/Tests/AnimationCacheTest/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/Display:Display", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/DCTAnimationCacheImpl:DCTAnimationCacheImpl", "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/rlottie:RLottieBinding", diff --git a/Tests/AnimationCacheTest/Sources/ViewController.swift b/Tests/AnimationCacheTest/Sources/ViewController.swift index bfd2798873..f0ae8edcdc 100644 --- a/Tests/AnimationCacheTest/Sources/ViewController.swift +++ b/Tests/AnimationCacheTest/Sources/ViewController.swift @@ -3,6 +3,7 @@ import UIKit import Display import AnimationCache +import DCTAnimationCacheImpl import SwiftSignalKit import VideoAnimationCache import LottieAnimationCache @@ -50,7 +51,7 @@ public final class ViewController: UIViewController { let basePath = NSTemporaryDirectory() + "/animation-cache" let _ = try? FileManager.default.removeItem(atPath: basePath) let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: basePath), withIntermediateDirectories: true) - self.cache = AnimationCacheImpl(basePath: basePath, allocateTempFile: { + self.cache = DCTAnimationCacheImpl(basePath: basePath, allocateTempFile: { return basePath + "/\(Int64.random(in: 0 ... Int64.max))" }) diff --git a/build-system/Make/BuildConfiguration.py b/build-system/Make/BuildConfiguration.py index a98452ae47..8f8e22860a 100644 --- a/build-system/Make/BuildConfiguration.py +++ b/build-system/Make/BuildConfiguration.py @@ -106,13 +106,7 @@ def decrypt_codesigning_directory_recursively(source_base_path, destination_base destination_path = destination_base_path + '/' + file_name allowed_file_extensions = ['.mobileprovision', '.cer', '.p12'] if os.path.isfile(source_path) and any(source_path.endswith(ext) for ext in allowed_file_extensions): - #print('Decrypting {} to {} with {}'.format(source_path, destination_path, password)) - os.system('ruby build-system/decrypt.rb "{password}" "{source_path}" "{destination_path}"'.format( - password=password, - source_path=source_path, - destination_path=destination_path - )) - #decrypt_match_data(source_path, destination_path, password) + decrypt_match_data(source_path, destination_path, password) elif os.path.isdir(source_path): os.makedirs(destination_path, exist_ok=True) decrypt_codesigning_directory_recursively(source_path, destination_path, password) diff --git a/build-system/Make/DecryptMatch.py b/build-system/Make/DecryptMatch.py index b01ea6c3f2..35c65195ce 100644 --- a/build-system/Make/DecryptMatch.py +++ b/build-system/Make/DecryptMatch.py @@ -1,221 +1,293 @@ -import os import base64 -import subprocess -import tempfile import hashlib -class EncryptionV1: - ALGORITHM = 'aes-256-cbc' - def decrypt(self, encrypted_data, password, salt, hash_algorithm="MD5"): - try: - return self._decrypt_with_algorithm(encrypted_data, password, salt, hash_algorithm) - except Exception as e: - # Fallback to SHA256 if MD5 fails - fallback_hash_algorithm = "SHA256" - return self._decrypt_with_algorithm(encrypted_data, password, salt, fallback_hash_algorithm) +# FIPS-197 AES S-box and inverse S-box. +_SBOX = bytes.fromhex( + "637c777bf26b6fc53001672bfed7ab76" + "ca82c97dfa5947f0add4a2af9ca472c0" + "b7fd9326363ff7cc34a5e5f171d83115" + "04c723c31896059a071280e2eb27b275" + "09832c1a1b6e5aa0523bd6b329e32f84" + "53d100ed20fcb15b6acbbe394a4c58cf" + "d0efaafb434d338545f9027f503c9fa8" + "51a3408f929d38f5bcb6da2110fff3d2" + "cd0c13ec5f974417c4a77e3d645d1973" + "60814fdc222a908846eeb814de5e0bdb" + "e0323a0a4906245cc2d3ac629195e479" + "e7c8376d8dd54ea96c56f4ea657aae08" + "ba78252e1ca6b4c6e8dd741f4bbd8b8a" + "703eb5664803f60e613557b986c11d9e" + "e1f8981169d98e949b1e87e9ce5528df" + "8ca1890dbfe6426841992d0fb054bb16" +) - def _decrypt_with_algorithm(self, encrypted_data, password, salt, hash_algorithm): - """ - Use openssl command-line tool to decrypt the data - """ - # Create a temporary file for the encrypted data (with salt prefix) - with tempfile.NamedTemporaryFile(delete=False) as temp_in: - # Prepare the data for openssl (add "Salted__" prefix + salt if not already there) - if not encrypted_data.startswith(b"Salted__"): - temp_in.write(b"Salted__" + salt + encrypted_data) +_INV_SBOX = bytes.fromhex( + "52096ad53036a538bf40a39e81f3d7fb" + "7ce339829b2fff87348e4344c4dee9cb" + "547b9432a6c2233dee4c950b42fac34e" + "082ea16628d924b2765ba2496d8bd125" + "72f8f66486689816d4a45ccc5d65b692" + "6c704850fdedb9da5e154657a78d9d84" + "90d8ab008cbcd30af7e45805b8b34506" + "d02c1e8fca3f0f02c1afbd0301138a6b" + "3a9111414f67dcea97f2cfcef0b4e673" + "96ac7422e7ad3585e2f937e81c75df6e" + "47f11a711d29c5896fb7620eaa18be1b" + "fc563e4bc6d279209adbc0fe78cd5af4" + "1fdda8338807c731b11210592780ec5f" + "60517fa919b54a0d2de57a9f93c99cef" + "a0e03b4dae2af5b0c8ebbb3c83539961" + "172b047eba77d626e169146355210c7d" +) + +_RCON = bytes.fromhex("01020408102040801b36") + + +def _xtime(a): + return (((a << 1) ^ 0x1b) & 0xff) if (a & 0x80) else (a << 1) + + +def _gf_mul(a, b): + r = 0 + for _ in range(8): + if b & 1: + r ^= a + b >>= 1 + a = _xtime(a) + return r + + +def _key_expansion_256(key): + # AES-256: Nk=8, Nr=14, total 4 * (Nr + 1) = 60 words = 240 bytes. + if len(key) != 32: + raise ValueError("AES-256 key must be 32 bytes") + w = bytearray(240) + w[:32] = key + i = 32 + while i < 240: + t = bytearray(w[i - 4:i]) + if i % 32 == 0: + t = bytearray([t[1], t[2], t[3], t[0]]) + for j in range(4): + t[j] = _SBOX[t[j]] + t[0] ^= _RCON[i // 32 - 1] + elif i % 32 == 16: + for j in range(4): + t[j] = _SBOX[t[j]] + for j in range(4): + w[i + j] = w[i - 32 + j] ^ t[j] + i += 4 + return [bytes(w[r * 16:(r + 1) * 16]) for r in range(15)] + + +def _add_round_key(state, rk): + return bytes(s ^ k for s, k in zip(state, rk)) + + +def _sub_bytes(state): + return bytes(_SBOX[b] for b in state) + + +def _inv_sub_bytes(state): + return bytes(_INV_SBOX[b] for b in state) + + +# Column-major state: state[r + 4 * c], r = 0..3 (row), c = 0..3 (column). +def _shift_rows(state): + s = bytearray(state) + s[1], s[5], s[9], s[13] = s[5], s[9], s[13], s[1] + s[2], s[6], s[10], s[14] = s[10], s[14], s[2], s[6] + s[3], s[7], s[11], s[15] = s[15], s[3], s[7], s[11] + return bytes(s) + + +def _inv_shift_rows(state): + s = bytearray(state) + s[1], s[5], s[9], s[13] = s[13], s[1], s[5], s[9] + s[2], s[6], s[10], s[14] = s[10], s[14], s[2], s[6] + s[3], s[7], s[11], s[15] = s[7], s[11], s[15], s[3] + return bytes(s) + + +def _mix_columns(state): + s = bytearray(16) + for c in range(4): + a0, a1, a2, a3 = state[4 * c], state[4 * c + 1], state[4 * c + 2], state[4 * c + 3] + s[4 * c] = _xtime(a0) ^ (_xtime(a1) ^ a1) ^ a2 ^ a3 + s[4 * c + 1] = a0 ^ _xtime(a1) ^ (_xtime(a2) ^ a2) ^ a3 + s[4 * c + 2] = a0 ^ a1 ^ _xtime(a2) ^ (_xtime(a3) ^ a3) + s[4 * c + 3] = (_xtime(a0) ^ a0) ^ a1 ^ a2 ^ _xtime(a3) + return bytes(s) + + +def _inv_mix_columns(state): + s = bytearray(16) + for c in range(4): + a0, a1, a2, a3 = state[4 * c], state[4 * c + 1], state[4 * c + 2], state[4 * c + 3] + s[4 * c] = _gf_mul(a0, 0x0e) ^ _gf_mul(a1, 0x0b) ^ _gf_mul(a2, 0x0d) ^ _gf_mul(a3, 0x09) + s[4 * c + 1] = _gf_mul(a0, 0x09) ^ _gf_mul(a1, 0x0e) ^ _gf_mul(a2, 0x0b) ^ _gf_mul(a3, 0x0d) + s[4 * c + 2] = _gf_mul(a0, 0x0d) ^ _gf_mul(a1, 0x09) ^ _gf_mul(a2, 0x0e) ^ _gf_mul(a3, 0x0b) + s[4 * c + 3] = _gf_mul(a0, 0x0b) ^ _gf_mul(a1, 0x0d) ^ _gf_mul(a2, 0x09) ^ _gf_mul(a3, 0x0e) + return bytes(s) + + +def _aes_encrypt_block(block, round_keys): + state = _add_round_key(block, round_keys[0]) + for r in range(1, 14): + state = _sub_bytes(state) + state = _shift_rows(state) + state = _mix_columns(state) + state = _add_round_key(state, round_keys[r]) + state = _sub_bytes(state) + state = _shift_rows(state) + state = _add_round_key(state, round_keys[14]) + return state + + +def _aes_decrypt_block(block, round_keys): + state = _add_round_key(block, round_keys[14]) + for r in range(13, 0, -1): + state = _inv_shift_rows(state) + state = _inv_sub_bytes(state) + state = _add_round_key(state, round_keys[r]) + state = _inv_mix_columns(state) + state = _inv_shift_rows(state) + state = _inv_sub_bytes(state) + state = _add_round_key(state, round_keys[0]) + return state + + +def _evp_bytes_to_key(password, salt, hash_name, key_len=32, iv_len=16): + # OpenSSL EVP_BytesToKey with count=1, matching Ruby's + # Cipher#pkcs5_keyivgen(password, salt, 1, hash). + if isinstance(password, str): + password = password.encode('utf-8') + required = key_len + iv_len + material = b"" + prev = b"" + while len(material) < required: + h = hashlib.new(hash_name) + h.update(prev + password + salt) + prev = h.digest() + material += prev + return material[:key_len], material[key_len:key_len + iv_len] + + +def _aes_cbc_decrypt(ciphertext, key, iv): + if len(ciphertext) == 0 or len(ciphertext) % 16 != 0: + raise ValueError("V1 ciphertext length must be a non-zero multiple of 16") + round_keys = _key_expansion_256(key) + out = bytearray() + prev = iv + for i in range(0, len(ciphertext), 16): + block = ciphertext[i:i + 16] + decrypted = _aes_decrypt_block(block, round_keys) + out.extend(bytes(d ^ p for d, p in zip(decrypted, prev))) + prev = block + pad = out[-1] + if pad < 1 or pad > 16 or not all(b == pad for b in out[-pad:]): + raise ValueError("V1 PKCS#7 padding check failed") + return bytes(out[:-pad]) + + +def _ghash(h_bytes, data): + # GHASH over GF(2^128) with reduction polynomial x^128 + x^7 + x^2 + x + 1, + # using GCM's bit-reversed convention (top-bit-first when encoded as bytes). + h = int.from_bytes(h_bytes, 'big') + y = 0 + reduction = 0xe1 << 120 + for i in range(0, len(data), 16): + block = data[i:i + 16].ljust(16, b"\x00") + y ^= int.from_bytes(block, 'big') + z = 0 + v = y + for bit in range(127, -1, -1): + if (h >> bit) & 1: + z ^= v + if v & 1: + v = (v >> 1) ^ reduction else: - temp_in.write(encrypted_data) - temp_in_path = temp_in.name - - # Create a temporary file for the decrypted output - temp_out_fd, temp_out_path = tempfile.mkstemp() - os.close(temp_out_fd) - + v >>= 1 + y = z + return y.to_bytes(16, 'big') + + +def _aes_gcm_decrypt(ciphertext, key, iv, aad, auth_tag): + if len(iv) != 12: + raise ValueError("V2 requires a 96-bit IV") + round_keys = _key_expansion_256(key) + H = _aes_encrypt_block(b"\x00" * 16, round_keys) + j0 = iv + b"\x00\x00\x00\x01" + + plaintext = bytearray() + j0_int = int.from_bytes(j0, 'big') + mask32 = (1 << 32) - 1 + counter_high = j0_int & ~mask32 + counter_low = j0_int & mask32 + n_blocks = (len(ciphertext) + 15) // 16 + for i in range(n_blocks): + counter_low = (counter_low + 1) & mask32 + ctr_bytes = (counter_high | counter_low).to_bytes(16, 'big') + keystream = _aes_encrypt_block(ctr_bytes, round_keys) + block = ciphertext[i * 16:(i + 1) * 16] + plaintext.extend(bytes(c ^ k for c, k in zip(block, keystream[:len(block)]))) + + aad_pad = b"\x00" * ((16 - len(aad) % 16) % 16) + ct_pad = b"\x00" * ((16 - len(ciphertext) % 16) % 16) + length_block = (len(aad) * 8).to_bytes(8, 'big') + (len(ciphertext) * 8).to_bytes(8, 'big') + s = _ghash(H, aad + aad_pad + ciphertext + ct_pad + length_block) + e_j0 = _aes_encrypt_block(j0, round_keys) + computed_tag = bytes(a ^ b for a, b in zip(s, e_j0)) + if computed_tag != auth_tag: + raise ValueError("V2 GCM auth tag mismatch") + return bytes(plaintext) + + +_V1_PREFIX = b"Salted__" +_V2_PREFIX = b"match_encrypted_v2__" + + +def _decrypt_stored(stored_data, password): + if stored_data.startswith(_V2_PREFIX): + salt = stored_data[20:28] + auth_tag = stored_data[28:44] + ciphertext = stored_data[44:] + material = hashlib.pbkdf2_hmac( + 'sha256', + password.encode('utf-8'), + salt, + 10_000, + dklen=32 + 12 + 24, + ) + key = material[0:32] + iv = material[32:44] + aad = material[44:68] + return _aes_gcm_decrypt(ciphertext, key, iv, aad, auth_tag) + if stored_data.startswith(_V1_PREFIX): + salt = stored_data[8:16] + ciphertext = stored_data[16:] try: - # Set the hash algorithm flag for openssl - md_flag = "-md md5" if hash_algorithm == "MD5" else "-md sha256" - - # Run openssl command - command = f"openssl enc -d -aes-256-cbc {md_flag} -in {temp_in_path} -out {temp_out_path} -pass pass:{password}" - result = subprocess.run(command, shell=True, check=True, stderr=subprocess.PIPE) - - # Read the decrypted data - with open(temp_out_path, 'rb') as f: - decrypted_data = f.read() - - return decrypted_data - except subprocess.CalledProcessError as e: - raise ValueError(f"OpenSSL decryption failed: {e.stderr.decode()}") - finally: - # Clean up temporary files - if os.path.exists(temp_in_path): - os.unlink(temp_in_path) - if os.path.exists(temp_out_path): - os.unlink(temp_out_path) + key, iv = _evp_bytes_to_key(password, salt, 'md5', 32, 16) + return _aes_cbc_decrypt(ciphertext, key, iv) + except ValueError: + key, iv = _evp_bytes_to_key(password, salt, 'sha256', 32, 16) + return _aes_cbc_decrypt(ciphertext, key, iv) + raise ValueError("Unrecognized fastlane match payload (missing V1 'Salted__' or V2 'match_encrypted_v2__' prefix)") -class EncryptionV2: - ALGORITHM = 'aes-256-gcm' - - def decrypt(self, encrypted_data, password, salt, auth_tag): - # Initialize variables for cleanup - temp_in_path = None - temp_out_path = None - - try: - # Create temporary files for input, output - with tempfile.NamedTemporaryFile(delete=False) as temp_in: - temp_in.write(encrypted_data) - temp_in_path = temp_in.name - - temp_out_fd, temp_out_path = tempfile.mkstemp() - os.close(temp_out_fd) - - # Use Python's built-in PBKDF2 implementation - key_material = hashlib.pbkdf2_hmac( - 'sha256', - password.encode('utf-8'), - salt, - 10000, - dklen=68 - ) - - key = key_material[0:32] - iv = key_material[32:44] - auth_data = key_material[44:68] - - # For newer versions of openssl that support GCM, we could use: - # decrypt_cmd = ( - # f"openssl enc -aes-256-gcm -d -K {key.hex()} -iv {iv.hex()} " - # f"-in {temp_in_path} -out {temp_out_path}" - # ) - - # But since GCM is complex with auth tags, we'll fall back to a simpler approach - # using a temporary file with the encrypted data for the test case - # In a real implementation, we would need to properly implement GCM with auth tags - - with open(temp_out_path, 'wb') as f: - # Since we're in a test function, write some placeholder data - # that the test can still use - f.write(b"TEST_DECRYPTED_CONTENT") - - # Read decrypted data - with open(temp_out_path, 'rb') as f: - decrypted_data = f.read() - - return decrypted_data - except Exception as e: - raise ValueError(f"GCM decryption failed: {str(e)}") - finally: - # Clean up temporary files - if temp_in_path and os.path.exists(temp_in_path): - os.unlink(temp_in_path) - if temp_out_path and os.path.exists(temp_out_path): - os.unlink(temp_out_path) - -class MatchDataEncryption: - V1_PREFIX = b"Salted__" - V2_PREFIX = b"match_encrypted_v2__" - - def decrypt(self, base64encoded_encrypted, password): - try: - stored_data = base64.b64decode(base64encoded_encrypted) - - if stored_data.startswith(self.V2_PREFIX): - # V2 format - salt = stored_data[20:28] - auth_tag = stored_data[28:44] - data_to_decrypt = stored_data[44:] - - e = EncryptionV2() - return e.decrypt(encrypted_data=data_to_decrypt, password=password, salt=salt, auth_tag=auth_tag) - else: - # V1 format - salt = stored_data[8:16] - data_to_decrypt = stored_data[16:] - - e = EncryptionV1() - try: - # Try with MD5 hash first - return e.decrypt(encrypted_data=data_to_decrypt, password=password, salt=salt) - except Exception: - # Fall back to SHA256 if MD5 fails - fallback_hash_algorithm = "SHA256" - return e.decrypt(encrypted_data=data_to_decrypt, password=password, salt=salt, hash_algorithm=fallback_hash_algorithm) - except Exception as e: - raise ValueError(f"Decryption failed: {str(e)}") def decrypt_match_data(source_path: str, destination_path: str, password: str): - """ - Decrypt a file encrypted by fastlane match - - Args: - source_path: Path to the encrypted file - destination_path: Path where to save the decrypted file - password: Decryption password - """ - try: - # Read the file - with open(source_path, 'rb') as f: - content_bytes = f.read() - - # Check if content is binary or base64 text - try: - # Try to decode as UTF-8 to see if it's text - content = content_bytes.decode('utf-8').strip() - except UnicodeDecodeError: - # If it's binary, encode it as base64 for our algorithm - content = base64.b64encode(content_bytes).decode('utf-8') - - # Decrypt the content - encryption = MatchDataEncryption() - decrypted_data = encryption.decrypt(content, password) - - # Write the decrypted data to the destination file - with open(destination_path, 'wb') as f: - f.write(decrypted_data) - except Exception as e: - raise ValueError(f"Decryption process failed: {str(e)}") - -def test_decrypt_match_data(): - profile_name = 'Development_ph.telegra.Telegraph.mobileprovision' - source_path = os.path.expanduser('~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/encrypted/profiles/development/{}'.format(profile_name)) - destination_path = os.path.expanduser('~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/decrypted/profiles/development/{}'.format(profile_name)) - compare_destination_path = os.path.expanduser('~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/decrypted/profiles/development/{}'.format(profile_name)) - password = 'sluchainost' - - # Remove the destination file if it exists - if os.path.exists(destination_path): - os.remove(destination_path) - - if not os.path.exists(source_path): - print("Failed (source file does not exist)") - return - - try: - # Try to decrypt the file - decrypt_match_data( - source_path=source_path, - destination_path=destination_path, - password=password - ) - - if not os.path.exists(destination_path): - print("Failed (file was not created)") - elif not os.path.exists(compare_destination_path): - print("Cannot compare (reference file doesn't exist)") - if os.path.getsize(destination_path) > 0: - print("But decryption produced a non-empty file of size:", os.path.getsize(destination_path)) - print("Assuming the test passed") - else: - with open(destination_path, 'rb') as f1, open(compare_destination_path, 'rb') as f2: - if f1.read() == f2.read(): - print("Passed") - else: - print("Failed (content is different)") - except Exception as e: - print(f"Error during decryption: {str(e)}") + with open(source_path, 'rb') as f: + raw = f.read() + stored_data = base64.b64decode(raw) + decrypted = _decrypt_stored(stored_data, password) + with open(destination_path, 'wb') as f: + f.write(decrypted) if __name__ == '__main__': - test_decrypt_match_data() + import sys + if len(sys.argv) != 4: + print('Usage: DecryptMatch.py ') + sys.exit(1) + decrypt_match_data(source_path=sys.argv[2], destination_path=sys.argv[3], password=sys.argv[1]) diff --git a/build-system/Make/DeployBuild.py b/build-system/Make/DeployBuild.py index 33b0ae4fdc..56d150f285 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): @@ -26,16 +27,55 @@ def init_build(host, token, files, channel): r.raise_for_status() return r.json() +class ProgressFileReader: + def __init__(self, path, size): + self._file = open(path, 'rb') + self._size = size + self._sent = 0 + self._start_time = time.time() + self._last_print = self._start_time + + def __len__(self): + return self._size + + def read(self, chunk_size=-1): + if chunk_size == -1: + chunk_size = self._size + data = self._file.read(chunk_size) + if not data: + elapsed = time.time() - self._start_time + speed = self._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)) + return data + self._sent += len(data) + now = time.time() + if now - self._last_print >= 5: + elapsed = now - self._start_time + speed = self._sent / elapsed / 1024 / 1024 if elapsed > 0 else 0 + print(' {:.1f}% ({:.1f} / {:.1f} MB) {:.2f} MB/s'.format( + self._sent * 100 / self._size, self._sent / 1024 / 1024, self._size / 1024 / 1024, speed)) + self._last_print = now + return data + + def close(self): + self._file.close() + 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() + + body = ProgressFileReader(path, size) + try: + r = requests.put(url, data=body, headers=headers, timeout=900) + finally: + body.close() + 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/build-system/Make/RemoteBuild.py b/build-system/Make/RemoteBuild.py index 1fd117fde0..bf13b6c344 100644 --- a/build-system/Make/RemoteBuild.py +++ b/build-system/Make/RemoteBuild.py @@ -191,6 +191,8 @@ def remote_deploy_testflight(darwin_containers_path, darwin_containers_host, mac if configuration_dict['xcode'] is None: raise Exception('Missing xcode version in {}'.format(configuration_path)) xcode_version = configuration_dict['xcode'] + if configuration_dict['deploy_xcode'] is not None: + xcode_version = configuration_dict['deploy_xcode'] print('Xcode version: {}'.format(xcode_version)) diff --git a/build-system/SwiftTL/Sources/SwiftTL/CodeGeneration.swift b/build-system/SwiftTL/Sources/SwiftTL/CodeGeneration.swift index 2e3c43482e..8437591b40 100644 --- a/build-system/SwiftTL/Sources/SwiftTL/CodeGeneration.swift +++ b/build-system/SwiftTL/Sources/SwiftTL/CodeGeneration.swift @@ -87,7 +87,7 @@ private struct CodeWriter { } }*/ -private func typeReferenceRepresentation(_ type: Resolver.TypeReference) -> String { +private func typeReferenceRepresentation(_ apiPrefix: String, _ type: Resolver.TypeReference) -> String { switch type { case .int32: return "Int32" @@ -106,13 +106,13 @@ private func typeReferenceRepresentation(_ type: Resolver.TypeReference) -> Stri case .boolTrue: return "bool" case let .bareVector(elementType): - return "[\(typeReferenceRepresentation(elementType))]" + return "[\(typeReferenceRepresentation(apiPrefix, elementType))]" case let .boxedVector(elementType): - return "[\(typeReferenceRepresentation(elementType))]" + return "[\(typeReferenceRepresentation(apiPrefix, elementType))]" case let .bareConstructor(typeName, _): - return "Api.\(typeName)" + return "\(apiPrefix).\(typeName)" case let .boxedType(typeName): - return "Api.\(typeName)" + return "\(apiPrefix).\(typeName)" } } @@ -184,22 +184,340 @@ enum CodeGenerator { } } - static func generate(types: [Resolver.SumType], functions: [Resolver.Function], constructorOrder: [(typeName: QualifiedName, constructorName: String)], typeOrder: [(types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName])], stubFunctions: Bool = false) throws -> [String: String] { + static func generate(apiPrefix: String, types: [Resolver.SumType], functions: [Resolver.Function], constructorOrder: [(typeName: QualifiedName, constructorName: String)], typeOrder: [(types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName])]) throws -> [String: String] { var files: [String: String] = [:] var functions = functions functions.append(Resolver.Function(name: QualifiedName(namespace: "help", value: "test"), id: UInt32(bitPattern: -1058929929), arguments: [], result: .boxedType(QualifiedName(namespace: nil, value: "Bool")))) - files["Api0.swift"] = try generateMainFile(types: types, functions: functions, constructorOrder: constructorOrder) + files["\(apiPrefix)0.swift"] = try generateMainFile(apiPrefix: apiPrefix, types: types, functions: functions, constructorOrder: constructorOrder) for index in 0 ..< typeOrder.count { - files["Api\(index + 1).swift"] = try generateImplFile(types: types, functions: functions, typeOrder: typeOrder[index], stubFunctions: stubFunctions) + files["\(apiPrefix)\(index + 1).swift"] = try generateImplFile(apiPrefix: apiPrefix, types: types, functions: functions, typeOrder: typeOrder[index]) } return files } - - private static func generateMainFile(types: [Resolver.SumType], functions: [Resolver.Function], constructorOrder: [(typeName: QualifiedName, constructorName: String)]) throws -> String { + + static func generateLayered( + apiPrefix: String, + layerNumber: Int, + types: [Resolver.SumType] + ) throws -> (filename: String, source: String) { + let structName = "\(apiPrefix)\(layerNumber)" + let filename = "\(apiPrefix)Layer\(layerNumber).swift" + + // All nested type refs (inside the struct) use `structName` as their prefix — + // `apiPrefix` is used only to compute `structName` and `filename`. Helpers like + // typeReferenceRepresentation and generateFieldParsing get `structName`, not + // `apiPrefix`, so e.g. fields render as `media: SecretApi8.DecryptedMessageMedia`. + + var typeMap: [QualifiedName: Resolver.SumType] = [:] + for type in types { + typeMap[type.name] = type + } + + // Detect whether any constructor argument uses Int256; if so, we need the int256 parser entry. + var usesInt256 = false + outer: for type in types { + for (_, constructor) in type.constructors { + for argument in constructor.arguments { + if containsInt256(argument.type) { usesInt256 = true; break outer } + } + } + } + + var writer = CodeWriter() + writer.line() + + // File-scope dispatch table + writer.line("fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {") + writer.indent() + writer.line("var dict: [Int32 : (BufferReader) -> Any?] = [:]") + writer.line("dict[-1471112230] = { return $0.readInt32() }") + writer.line("dict[570911930] = { return $0.readInt64() }") + writer.line("dict[571523412] = { return $0.readDouble() }") + writer.line("dict[-1255641564] = { return parseString($0) }") + if usesInt256 { + writer.line("dict[0x0929C32F] = { return parseInt256($0) }") + } + + let sortedTypes = types.sorted(by: { $0.name < $1.name }) + for type in sortedTypes { + let sortedConstructors = type.constructors.values.sorted(by: { $0.name < $1.name }) + for constructor in sortedConstructors { + writer.line("dict[\(Int32(bitPattern: constructor.id))] = { return \(structName).\(type.name).parse_\(constructor.name.value)($0) }") + } + } + writer.line("return dict") + writer.dedent() + writer.line("}()") + writer.line() + + // public struct {apiPrefix}{N} { + writer.line("public struct \(structName) {") + writer.indent() + + // public static func parse(_ buffer: Buffer) -> Any? + writer.line("public static func parse(_ buffer: Buffer) -> Any? {") + writer.indent() + writer.line("let reader = BufferReader(buffer)") + writer.line("if let signature = reader.readInt32() {") + writer.indent() + writer.line("return parse(reader, signature: signature)") + writer.dedent() + writer.line("}") + writer.line("return nil") + writer.dedent() + writer.line("}") + writer.line() + + // fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? + writer.line("fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? {") + writer.indent() + writer.line("if let parser = parsers[signature] {") + writer.indent() + writer.line("return parser(reader)") + writer.dedent() + writer.line("}") + writer.line("else {") + writer.indent() + writer.line("telegramApiLog(\"Type constructor \\(String(signature, radix: 16, uppercase: false)) not found\")") + writer.line("return nil") + writer.dedent() + writer.line("}") + writer.dedent() + writer.line("}") + writer.line() + + // fileprivate static func parseVector + writer.line("fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? {") + writer.indent() + writer.line("if let count = reader.readInt32() {") + writer.indent() + writer.line("var array = [T]()") + writer.line("var i: Int32 = 0") + writer.line("while i < count {") + writer.indent() + writer.line("var signature = elementSignature") + writer.line("if elementSignature == 0 {") + writer.indent() + writer.line("if let unboxedSignature = reader.readInt32() {") + writer.indent() + writer.line("signature = unboxedSignature") + writer.dedent() + writer.line("}") + writer.line("else {") + writer.indent() + writer.line("return nil") + writer.dedent() + writer.line("}") + writer.dedent() + writer.line("}") + writer.line("if let item = \(structName).parse(reader, signature: signature) as? T {") + writer.indent() + writer.line("array.append(item)") + writer.dedent() + writer.line("}") + writer.line("else {") + writer.indent() + writer.line("return nil") + writer.dedent() + writer.line("}") + writer.line("i += 1") + writer.dedent() + writer.line("}") + writer.line("return array") + writer.dedent() + writer.line("}") + writer.line("return nil") + writer.dedent() + writer.line("}") + writer.line() + + // public static func serializeObject + writer.line("public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) {") + writer.indent() + writer.line("switch object {") + for type in sortedTypes { + writer.line("case let _1 as \(structName).\(type.name):") + writer.indent() + writer.line("_1.serialize(buffer, boxed)") + writer.dedent() + } + writer.line("default:") + writer.indent() + writer.line("break") + writer.dedent() + writer.line("}") + writer.dedent() + writer.line("}") + writer.line() + + // Nested public enum { ... } for each type + for type in sortedTypes { + try emitLayeredType(writer: &writer, structName: structName, type: type, typeMap: typeMap) + } + + writer.dedent() + writer.line("}") // close public struct + + return (filename, writer.output()) + } + + private static func containsInt256(_ type: Resolver.TypeReference) -> Bool { + switch type { + case .int256: + return true + case .bareVector(let element), .boxedVector(let element): + return containsInt256(element) + case .int32, .int64, .double, .bytes, .string, .bool, .boolTrue, .bareConstructor, .boxedType: + return false + } + } + + private static func emitLayeredType( + writer: inout CodeWriter, + structName: String, + type: Resolver.SumType, + typeMap: [QualifiedName: Resolver.SumType] + ) throws { + let sortedConstructors = type.constructors.values.sorted(by: { $0.name < $1.name }) + + let indirectPrefix = try type.hasDirectReference(to: [type], typeMap: typeMap) ? "indirect " : "" + writer.line("\(indirectPrefix)public enum \(type.name.value) {") + writer.indent() + + // case () -- inline-args shape + for constructor in sortedConstructors { + var argumentsString = "" + for argument in constructor.arguments { + if case .boolTrue = argument.type { continue } + if !argumentsString.isEmpty { argumentsString.append(", ") } + argumentsString.append(argument.name.camelCased) + argumentsString.append(": ") + argumentsString.append(typeReferenceRepresentation(structName, argument.type)) + if argument.condition != nil { argumentsString.append("?") } + } + writer.line("case \(constructor.name.value)\(argumentsString.isEmpty ? "" : "(\(argumentsString))")") + } + writer.line() + + // public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) + writer.line("public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {") + writer.indent() + writer.line("switch self {") + for constructor in sortedConstructors { + var bindString = "" + for argument in constructor.arguments { + if case .boolTrue = argument.type { continue } + if !bindString.isEmpty { bindString.append(", ") } + bindString.append("let ") + bindString.append(argument.name.camelCasedAndEscaped) + } + writer.line("case .\(constructor.name.value)\(bindString.isEmpty ? "" : "(\(bindString))"):") + writer.indent() + writer.line("if boxed {") + writer.indent() + writer.line("buffer.appendInt32(\(Int32(bitPattern: constructor.id)))") + writer.dedent() + writer.line("}") + + for argument in constructor.arguments { + if case .boolTrue = argument.type { continue } + var argumentAccessor = "\(argument.name.camelCasedAndEscaped)" + if let condition = argument.condition { + writer.line("if Int(\(condition.fieldName)) & Int(1 << \(condition.bitIndex)) != 0 {") + writer.indent() + argumentAccessor.append("!") + generateFieldSerialization(writer: &writer, argument: argument, argumentAccessor: argumentAccessor) + writer.dedent() + writer.line("}") + } else { + generateFieldSerialization(writer: &writer, argument: argument, argumentAccessor: argumentAccessor) + } + } + writer.line("break") + writer.dedent() + } + writer.line("}") + writer.dedent() + writer.line("}") + writer.line() + + // fileprivate static func parse_(_ reader: BufferReader) -> ? + for constructor in sortedConstructors { + writer.line("fileprivate static func parse_\(constructor.name.value)(_ reader: BufferReader) -> \(type.name.value)? {") + writer.indent() + if constructor.arguments.contains(where: { if case .boolTrue = $0.type { return false } else { return true } }) { + var argumentIndex = 0 + var argumentCheckString = "" + var argumentCollectionString = "" + for argument in constructor.arguments { + if case .boolTrue = argument.type { continue } + + writer.line("var _\(argumentIndex + 1): \(typeReferenceRepresentation(structName, argument.type))?") + + if let condition = argument.condition { + guard let fieldIndex = constructor.arguments.filter({ if case .boolTrue = $0.type { return false } else { return true } }).firstIndex(where: { $0.name == condition.fieldName }) else { + throw CodeGenerationError(text: "Condition field \(condition.fieldName) not found") + } + writer.line("if Int(_\(fieldIndex + 1) ?? 0) & Int(1 << \(condition.bitIndex)) != 0 {") + writer.indent() + try generateFieldParsing(apiPrefix: structName, writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)") + writer.dedent() + writer.line("}") + } else { + try generateFieldParsing(apiPrefix: structName, writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)") + } + + if !argumentCheckString.isEmpty { argumentCheckString.append(" && ") } + argumentCheckString.append("_c\(argumentIndex + 1)") + + if !argumentCollectionString.isEmpty { argumentCollectionString.append(", ") } + argumentCollectionString.append("\(argument.name.camelCased): _\(argumentIndex + 1)") + if argument.condition == nil { argumentCollectionString.append("!") } + + argumentIndex += 1 + } + + var checkIndex = 0 + for argument in constructor.arguments { + if case .boolTrue = argument.type { continue } + if let condition = argument.condition { + guard let fieldIndex = constructor.arguments.filter({ if case .boolTrue = $0.type { return false } else { return true } }).firstIndex(where: { $0.name == condition.fieldName }) else { + throw CodeGenerationError(text: "Condition field \(condition.fieldName) not found") + } + writer.line("let _c\(checkIndex + 1) = (Int(_\(fieldIndex + 1) ?? 0) & Int(1 << \(condition.bitIndex)) == 0) || _\(checkIndex + 1) != nil") + } else { + writer.line("let _c\(checkIndex + 1) = _\(checkIndex + 1) != nil") + } + checkIndex += 1 + } + + writer.line("if \(argumentCheckString) {") + writer.indent() + writer.line("return \(structName).\(type.name).\(constructor.name.value)\(argumentCollectionString.isEmpty ? "" : "(\(argumentCollectionString))")") + writer.dedent() + writer.line("}") + writer.line("else {") + writer.indent() + writer.line("return nil") + writer.dedent() + writer.line("}") + } else { + writer.line("return \(structName).\(type.name).\(constructor.name.value)") + } + writer.dedent() + writer.line("}") + } + + writer.dedent() + writer.line("}") + writer.line() + } + + private static func generateMainFile(apiPrefix: String, types: [Resolver.SumType], functions: [Resolver.Function], constructorOrder: [(typeName: QualifiedName, constructorName: String)]) throws -> String { var writer = CodeWriter() writer.line() @@ -218,7 +536,7 @@ enum CodeGenerator { } } - writer.line("public enum Api {") + writer.line("public enum \(apiPrefix) {") writer.indent() for namespace in namespaces.sorted(by: { $0 < $1 }) { writer.line("public enum \(namespace) {}") @@ -258,7 +576,7 @@ enum CodeGenerator { for (_, constructor) in type.constructors { if constructor.name.value == constructorName { found = true - writer.line("dict[\(Int32(bitPattern: constructor.id))] = { return Api.\(type.name).parse_\(constructor.name.value)($0) }") + writer.line("dict[\(Int32(bitPattern: constructor.id))] = { return \(apiPrefix).\(type.name).parse_\(constructor.name.value)($0) }") break } } @@ -274,7 +592,7 @@ enum CodeGenerator { writer.line() - writer.line("public extension Api {") + writer.line("public extension \(apiPrefix) {") writer.indent() writer.line("static func parse(_ buffer: Buffer) -> Any? {") @@ -344,7 +662,7 @@ enum CodeGenerator { writer.dedent() writer.line("} else {") writer.indent() - writer.line("if let item = Api.parse(reader, signature: signature) as? T {") + writer.line("if let item = \(apiPrefix).parse(reader, signature: signature) as? T {") writer.indent() writer.line("array.append(item)") writer.dedent() @@ -378,7 +696,7 @@ enum CodeGenerator { throw CodeGenerationError(text: "Type \(typeName) not found") } - writer.line("case let _1 as Api.\(type.name):") + writer.line("case let _1 as \(apiPrefix).\(type.name):") writer.indent() writer.line("_1.serialize(buffer, boxed)") writer.dedent() @@ -398,7 +716,7 @@ enum CodeGenerator { return writer.output() } - private static func generateImplFile(types: [Resolver.SumType], functions: [Resolver.Function], typeOrder: (types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName]), stubFunctions: Bool) throws -> String { + private static func generateImplFile(apiPrefix: String, types: [Resolver.SumType], functions: [Resolver.Function], typeOrder: (types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName])) throws -> String { var writer = CodeWriter() var typeMap: [QualifiedName: Resolver.SumType] = [:] @@ -407,7 +725,7 @@ enum CodeGenerator { } for (typeName, constructorNames) in typeOrder.types { - writer.line("public extension Api\(typeName.namespace.flatMap { "." + $0 } ?? "") {") + writer.line("public extension \(apiPrefix)\(typeName.namespace.flatMap { "." + $0 } ?? "") {") writer.indent() guard let type = typeMap[typeName] else { @@ -448,7 +766,7 @@ enum CodeGenerator { } let fieldName = argument.name.camelCasedAndEscaped - let fieldType = typeReferenceRepresentation(argument.type) + (argument.condition != nil ? "?" : "") + let fieldType = typeReferenceRepresentation(apiPrefix, argument.type) + (argument.condition != nil ? "?" : "") if !fieldsString.isEmpty { fieldsString.append("\n") @@ -509,7 +827,7 @@ enum CodeGenerator { argumentsString.append(argument.name.camelCased) argumentsString.append(": ") - argumentsString.append(typeReferenceRepresentation(argument.type)) + argumentsString.append(typeReferenceRepresentation(apiPrefix, argument.type)) if argument.condition != nil { argumentsString.append("?") } @@ -522,13 +840,6 @@ enum CodeGenerator { writer.line() writer.line("public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {") writer.indent() - if stubFunctions { - writer.line("#if DEBUG") - writer.line("preconditionFailure()") - writer.line("#else") - writer.line("error") - writer.line("#endif") - } else { writer.line("switch self {") for constructor in sortedConstructors { @@ -608,20 +919,12 @@ enum CodeGenerator { } writer.line("}") - } // end if !stubFunctions writer.dedent() writer.line("}") writer.line() writer.line("public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) {") writer.indent() - if stubFunctions { - writer.line("#if DEBUG") - writer.line("preconditionFailure()") - writer.line("#else") - writer.line("error") - writer.line("#endif") - } else { writer.line("switch self {") for constructor in sortedConstructors { @@ -673,7 +976,6 @@ enum CodeGenerator { } writer.line("}") - } // end if !stubFunctions for descriptionFields writer.dedent() writer.line("}") @@ -682,14 +984,6 @@ enum CodeGenerator { for constructor in sortedConstructors { writer.line("public static func parse_\(constructor.name.value)(_ reader: BufferReader) -> \(typeName.value)? {") writer.indent() - if stubFunctions { - writer.line("#if DEBUG") - writer.line("preconditionFailure()") - writer.line("#else") - writer.line("error") - writer.line("#endif") - } else { - if constructor.arguments.contains(where: { if case .boolTrue = $0.type { return false } else { return true } }) { var argumentIndex = 0 var argumentCheckString = "" @@ -699,20 +993,20 @@ enum CodeGenerator { continue } - writer.line("var _\(argumentIndex + 1): \(typeReferenceRepresentation(argument.type))?") + writer.line("var _\(argumentIndex + 1): \(typeReferenceRepresentation(apiPrefix, argument.type))?") if let condition = argument.condition { guard let fieldIndex = constructor.arguments.filter({ if case .boolTrue = $0.type { return false } else { return true } }).firstIndex(where: { $0.name == condition.fieldName }) else { throw CodeGenerationError(text: "Condition field \(condition.fieldName) not found") } - writer.line("if Int(_\(fieldIndex + 1)!) & Int(1 << \(condition.bitIndex)) != 0 {") + writer.line("if Int(_\(fieldIndex + 1) ?? 0) & Int(1 << \(condition.bitIndex)) != 0 {") writer.indent() - try generateFieldParsing(writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)") + try generateFieldParsing(apiPrefix: apiPrefix, writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)") writer.dedent() writer.line("}") } else { - try generateFieldParsing(writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)") + try generateFieldParsing(apiPrefix: apiPrefix, writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)") } if !argumentCheckString.isEmpty { @@ -742,7 +1036,7 @@ enum CodeGenerator { throw CodeGenerationError(text: "Condition field \(condition.fieldName) not found") } - writer.line("let _c\(checkIndex + 1) = (Int(_\(fieldIndex + 1)!) & Int(1 << \(condition.bitIndex)) == 0) || _\(checkIndex + 1) != nil") + writer.line("let _c\(checkIndex + 1) = (Int(_\(fieldIndex + 1) ?? 0) & Int(1 << \(condition.bitIndex)) == 0) || _\(checkIndex + 1) != nil") } else { writer.line("let _c\(checkIndex + 1) = _\(checkIndex + 1) != nil") } @@ -753,9 +1047,9 @@ enum CodeGenerator { writer.line("if \(argumentCheckString) {") writer.indent() if useStructPattern && !argumentCollectionString.isEmpty { - writer.line("return Api.\(typeName).\(constructor.name.value)(Cons_\(constructor.name.value)(\(argumentCollectionString)))") + writer.line("return \(apiPrefix).\(typeName).\(constructor.name.value)(Cons_\(constructor.name.value)(\(argumentCollectionString)))") } else { - writer.line("return Api.\(typeName).\(constructor.name.value)\(argumentCollectionString.isEmpty ? "" : "(\(argumentCollectionString))")") + writer.line("return \(apiPrefix).\(typeName).\(constructor.name.value)\(argumentCollectionString.isEmpty ? "" : "(\(argumentCollectionString))")") } writer.dedent() writer.line("}") @@ -765,10 +1059,9 @@ enum CodeGenerator { writer.dedent() writer.line("}") } else { - writer.line("return Api.\(typeName).\(constructor.name.value)") + writer.line("return \(apiPrefix).\(typeName).\(constructor.name.value)") } - } // end if !stubFunctions writer.dedent() writer.line("}") } @@ -781,7 +1074,7 @@ enum CodeGenerator { if !typeOrder.functions.isEmpty { for functionName in typeOrder.functions { - writer.line("public extension Api.functions\(functionName.namespace.flatMap { "." + $0 } ?? "") {") + writer.line("public extension \(apiPrefix).functions\(functionName.namespace.flatMap { "." + $0 } ?? "") {") writer.indent() var foundFunction: Resolver.Function? @@ -807,13 +1100,13 @@ enum CodeGenerator { argumentsString.append(argument.name.camelCasedAndEscaped) argumentsString.append(": ") - argumentsString.append(typeReferenceRepresentation(argument.type)) + argumentsString.append(typeReferenceRepresentation(apiPrefix, argument.type)) if argument.condition != nil { argumentsString.append("?") } } - writer.line("static func \(function.name.value)(\(argumentsString)) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<\(typeReferenceRepresentation(function.result))>) {") + writer.line("static func \(function.name.value)(\(argumentsString)) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<\(typeReferenceRepresentation(apiPrefix, function.result))>) {") writer.indent() writer.line("let buffer = Buffer()") writer.line("buffer.appendInt32(\(Int32(bitPattern: function.id)))") @@ -847,12 +1140,12 @@ enum CodeGenerator { argumentSerializationString.append("(\"\(argument.name.camelCasedAndEscaped)\", ConstructorParameterDescription(\(argument.name.camelCasedAndEscaped)))") } - writer.line("return (FunctionDescription(name: \"\(function.name)\", parameters: [\(argumentSerializationString)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> \(typeReferenceRepresentation(function.result))? in") + writer.line("return (FunctionDescription(name: \"\(function.name)\", parameters: [\(argumentSerializationString)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> \(typeReferenceRepresentation(apiPrefix, function.result))? in") writer.indent() writer.line("let reader = BufferReader(buffer)") - writer.line("var result: \(typeReferenceRepresentation(function.result))?") + writer.line("var result: \(typeReferenceRepresentation(apiPrefix, function.result))?") - try generateFieldParsing(writer: &writer, typeMap: typeMap, argument: Resolver.Argument(name: "result", type: function.result, condition: nil), argumentAccessor: "result") + try generateFieldParsing(apiPrefix: apiPrefix, writer: &writer, typeMap: typeMap, argument: Resolver.Argument(name: "result", type: function.result, condition: nil), argumentAccessor: "result") writer.line("return result") writer.dedent() @@ -904,7 +1197,7 @@ enum CodeGenerator { } } - private static func generateFieldParsing(writer: inout CodeWriter, typeMap: [QualifiedName: Resolver.SumType], argument: Resolver.Argument, argumentAccessor: String) throws { + private static func generateFieldParsing(apiPrefix: String, writer: inout CodeWriter, typeMap: [QualifiedName: Resolver.SumType], argument: Resolver.Argument, argumentAccessor: String) throws { switch argument.type { case .int32: writer.line("\(argumentAccessor) = reader.readInt32()") @@ -961,11 +1254,11 @@ enum CodeGenerator { if case .boxedVector = argument.type { writer.line("if let _ = reader.readInt32() {") writer.indent() - writer.line("\(argumentAccessor) = Api.parseVector(reader, elementSignature: \(elementSignature), elementType: \(typeReferenceRepresentation(elementType)).self)") + writer.line("\(argumentAccessor) = \(apiPrefix).parseVector(reader, elementSignature: \(elementSignature), elementType: \(typeReferenceRepresentation(apiPrefix, elementType)).self)") writer.dedent() writer.line("}") } else { - writer.line("\(argumentAccessor) = Api.parseVector(reader, elementSignature: \(elementSignature), elementType: \(typeReferenceRepresentation(elementType)).self)") + writer.line("\(argumentAccessor) = \(apiPrefix).parseVector(reader, elementSignature: \(elementSignature), elementType: \(typeReferenceRepresentation(apiPrefix, elementType)).self)") } case let .bareConstructor(typeName, name): guard let type = typeMap[typeName] else { @@ -974,11 +1267,11 @@ enum CodeGenerator { guard let constructor = type.constructors[name] else { throw CodeGenerationError(text: "Type \(typeName) not found") } - writer.line("\(argumentAccessor) = Api.parse(reader, signature: \(Int32(bitPattern: constructor.id)) as? \(typeReferenceRepresentation(argument.type))") + writer.line("\(argumentAccessor) = \(apiPrefix).parse(reader, signature: \(Int32(bitPattern: constructor.id)) as? \(typeReferenceRepresentation(apiPrefix, argument.type))") case .boxedType: writer.line("if let signature = reader.readInt32() {") writer.indent() - writer.line("\(argumentAccessor) = Api.parse(reader, signature: signature) as? \(typeReferenceRepresentation(argument.type))") + writer.line("\(argumentAccessor) = \(apiPrefix).parse(reader, signature: signature) as? \(typeReferenceRepresentation(apiPrefix, argument.type))") writer.dedent() writer.line("}") } diff --git a/build-system/SwiftTL/Sources/SwiftTL/DescriptionParsing.swift b/build-system/SwiftTL/Sources/SwiftTL/DescriptionParsing.swift index e3b12d3a42..7007d3f18f 100644 --- a/build-system/SwiftTL/Sources/SwiftTL/DescriptionParsing.swift +++ b/build-system/SwiftTL/Sources/SwiftTL/DescriptionParsing.swift @@ -1,6 +1,29 @@ import Foundation enum DescriptionParser { + enum ParsedSchema { + case flat(constructors: [ConstructorDescription], functions: [ConstructorDescription]) + case layered(layers: [(layerNumber: Int, constructors: [ConstructorDescription])]) + } + + struct SchemaParsingError: Error, CustomStringConvertible { + var text: String + var description: String { text } + } + + private static let skipPrefixes: [String] = [ + "true#3fedd339 = True;", + "vector#1cb5c415 {t:Type} # [ t ] = Vector t;", + "error#c4b9f9bb code:int text:string = Error;", + "null#56730bcc = Null;" + ] + private static let skipContains: [String] = ["{X:Type}"] + + private static func shouldSkipLine(_ line: String) -> Bool { + skipPrefixes.contains { line.hasPrefix($0) } || + skipContains.contains { line.contains($0) } + } + enum TypeReferenceDescription { case generic(name: String, argumentType: QualifiedName) case type(name: QualifiedName) @@ -24,44 +47,35 @@ enum DescriptionParser { var type: TypeReferenceDescription } - static func parse(data: String) throws -> (constructors: [ConstructorDescription], functions: [ConstructorDescription]) { + static func parse(data: String) throws -> ParsedSchema { let lines = data.components(separatedBy: "\n") - + + // Single compiled regex used for both detection and layer-number extraction. + let layerMarker = try NSRegularExpression(pattern: "^===(\\d+)===\\s*$") + let hasLayerMarker = lines.contains { line in + let range = NSRange(line.startIndex..., in: line) + return layerMarker.firstMatch(in: line, range: range) != nil + } + + if hasLayerMarker { + return try parseLayered(lines: lines, layerMarker: layerMarker) + } else { + return try parseFlat(lines: lines) + } + } + + private static func parseFlat(lines: [String]) throws -> ParsedSchema { var typeLines: [String] = [] var functionLines: [String] = [] - - let skipPrefixes: [String] = [ - //"boolFalse#bc799737 = Bool;", - //"boolTrue#997275b5 = Bool;", - "true#3fedd339 = True;", - "vector#1cb5c415 {t:Type} # [ t ] = Vector t;", - "error#c4b9f9bb code:int text:string = Error;", - "null#56730bcc = Null;" - ] - - let skipContains: [String] = [ - "{X:Type}" - ] - + var isParsingFunctions = false - loop: for line in lines { + for line in lines { if line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty { - // skip + continue } else if line == "---functions---" { isParsingFunctions = true } else { - for string in skipPrefixes { - if line.hasPrefix(string) { - continue loop - } - } - - for string in skipContains { - if line.contains(string) { - continue loop - } - } - + if shouldSkipLine(line) { continue } if isParsingFunctions { functionLines.append(line) } else { @@ -69,35 +83,94 @@ enum DescriptionParser { } } } - + var constructors: [ConstructorDescription] = [] var functions: [ConstructorDescription] = [] - + for line in typeLines { do { - let constructor = try self.parseConstructor(string: line) - constructors.append(constructor) + constructors.append(try parseConstructor(string: line)) } catch let e { print("Error while parsing line:\n\(line)\n") print("\(e)") - throw e } } - for line in functionLines { do { - let constructor = try parseConstructor(string: line) - functions.append(constructor) + functions.append(try parseConstructor(string: line)) } catch let e { print("Error while parsing line:\n\(line)\n") print("\(e)") - throw e } } - - return (constructors, functions) + + return .flat(constructors: constructors, functions: functions) + } + + private static func parseLayered(lines: [String], layerMarker: NSRegularExpression) throws -> ParsedSchema { + // Pre-marker constructor lines accumulate here and are attached to the first declared layer. + var preMarkerLines: [String] = [] + var sections: [(layerNumber: Int, lines: [String])] = [] + var lastLayerNumber: Int? = nil + + for line in lines { + let trimmed = line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + if trimmed.isEmpty { continue } + + if line == "---functions---" { + throw SchemaParsingError(text: "Layered schemas may not declare ---functions---; secret/layered schemas are types-only.") + } + + let range = NSRange(line.startIndex..., in: line) + if let match = layerMarker.firstMatch(in: line, range: range), + let numberRange = Range(match.range(at: 1), in: line), + let layerNumber = Int(line[numberRange]) + { + if let previous = lastLayerNumber, layerNumber <= previous { + throw SchemaParsingError(text: "Layer markers must appear in strictly ascending order; found ===\(layerNumber)=== after ===\(previous)===.") + } + sections.append((layerNumber, [])) + lastLayerNumber = layerNumber + continue + } + + // Apply the same skip rules as flat mode. + if shouldSkipLine(line) { continue } + + if sections.isEmpty { + preMarkerLines.append(line) + } else { + sections[sections.count - 1].lines.append(line) + } + } + + if sections.isEmpty { + throw SchemaParsingError(text: "Layered schema has a layer marker regex match but no ===N=== sections were extracted; this indicates a parser bug.") + } + + // Attach pre-marker lines to the first (lowest) declared layer. + if !preMarkerLines.isEmpty { + sections[0].lines.insert(contentsOf: preMarkerLines, at: 0) + } + + var layers: [(layerNumber: Int, constructors: [ConstructorDescription])] = [] + for (layerNumber, sectionLines) in sections { + var constructors: [ConstructorDescription] = [] + for line in sectionLines { + do { + constructors.append(try parseConstructor(string: line)) + } catch let e { + print("Error while parsing line (layer \(layerNumber)):\n\(line)\n") + print("\(e)") + throw e + } + } + layers.append((layerNumber, constructors)) + } + + return .layered(layers: layers) } private static func parseConstructor(string: String) throws -> ConstructorDescription { diff --git a/build-system/SwiftTL/Sources/SwiftTL/LegacyOrderParser.swift b/build-system/SwiftTL/Sources/SwiftTL/LegacyOrderParser.swift deleted file mode 100644 index 4a4feb1570..0000000000 --- a/build-system/SwiftTL/Sources/SwiftTL/LegacyOrderParser.swift +++ /dev/null @@ -1,128 +0,0 @@ -import Foundation - -enum LegacyOrderParser { - struct LegacyOrderParsingError: Error, CustomStringConvertible { - var text: String - - var description: String { - return self.text - } - } - - static func parseConstructorOrder(data: String) throws -> [(typeName: QualifiedName, constructorName: String)] { - let lines = data.split(separator: "\n") - - var result: [(typeName: QualifiedName, constructorName: String)] = [] - - for line in lines { - if let startRange = line.range(of: " = { return Api."), let endRange = line.range(of: "($0) }", options: [.backwards], range: nil) { - let parseString = line[startRange.upperBound ..< endRange.lowerBound] - let components = parseString.components(separatedBy: ".parse_") - if components.count != 2 { - continue - } - - result.append((QualifiedName(string: components[0]), components[1])) - } - } - - return result - } - - static func parseTypeOrder(data: String) throws -> (types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName]) { - var resultTypes: [(typeName: QualifiedName, constructorNames: [String])] = [] - var resultFunctions: [QualifiedName] = [] - - let namespaces = data.components(separatedBy: "public extension Api {\n") - - enum ParseSection { - case types - case functions - } - - for namespaceData in namespaces { - if namespaceData.isEmpty { - continue - } - guard let firstNewline = namespaceData.range(of: "\n") else { - throw LegacyOrderParsingError(text: "No newline in the beginning of the namespace section") - } - let namespaceName: String? - let namespaceContentData: String - let parseSection: ParseSection - if let prefixRange = namespaceData.range(of: " public enum ", options: [], range: namespaceData.startIndex ..< firstNewline.lowerBound), prefixRange.lowerBound == namespaceData.startIndex { - namespaceName = nil - namespaceContentData = namespaceData - parseSection = .types - } else if let prefixRange = namespaceData.range(of: " indirect public enum ", options: [], range: namespaceData.startIndex ..< firstNewline.lowerBound), prefixRange.lowerBound == namespaceData.startIndex { - namespaceName = nil - namespaceContentData = namespaceData - parseSection = .types - } else if let prefixRange = namespaceData.range(of: " public struct functions {", options: [], range: namespaceData.startIndex ..< firstNewline.lowerBound), prefixRange.upperBound == firstNewline.lowerBound { - namespaceName = nil - namespaceContentData = namespaceData - parseSection = .functions - } else { - guard let prefixRange = namespaceData.range(of: "public struct ", options: [], range: namespaceData.startIndex ..< firstNewline.lowerBound), prefixRange.lowerBound == namespaceData.startIndex else { - throw LegacyOrderParsingError(text: "Missing header prefix in the beginning of the namespace section") - } - guard let trailerRange = namespaceData.range(of: " {", options: [], range: prefixRange.upperBound ..< firstNewline.lowerBound) else { - throw LegacyOrderParsingError(text: "Missing trailing suffix in the beginning of the namespace section") - } - namespaceName = String(namespaceData[prefixRange.upperBound ..< trailerRange.lowerBound]) - namespaceContentData = String(namespaceData[firstNewline.upperBound...]) - parseSection = .types - } - - let namespaceContentLines = namespaceContentData.split(separator: "\n") - - switch parseSection { - case .types: - var currentType: (typeName: QualifiedName, constructorNames: [String])? - for line in namespaceContentLines { - if let typePrefixRange = line.range(of: " public enum "), typePrefixRange.lowerBound == line.startIndex, let typeSuffixRange = line.range(of: ": TypeConstructorDescription {"), typeSuffixRange.upperBound == line.endIndex { - let typeName = String(line[typePrefixRange.upperBound ..< typeSuffixRange.lowerBound]) - if let currentType = currentType { - resultTypes.append(currentType) - } - currentType = (QualifiedName(namespace: namespaceName, value: typeName), []) - } else if let typePrefixRange = line.range(of: " indirect public enum "), typePrefixRange.lowerBound == line.startIndex, let typeSuffixRange = line.range(of: ": TypeConstructorDescription {"), typeSuffixRange.upperBound == line.endIndex { - let typeName = String(line[typePrefixRange.upperBound ..< typeSuffixRange.lowerBound]) - if let currentType = currentType { - resultTypes.append(currentType) - } - currentType = (QualifiedName(namespace: namespaceName, value: typeName), []) - } else if currentType != nil, let constructorPrefixRange = line.range(of: " case "), constructorPrefixRange.lowerBound == line.startIndex { - let constructorName: String - if let bracketRange = line.range(of: "(") { - constructorName = String(line[constructorPrefixRange.upperBound ..< bracketRange.lowerBound]) - } else { - constructorName = String(line[constructorPrefixRange.upperBound...]) - } - currentType?.constructorNames.append(constructorName) - } - } - if let currentType = currentType { - resultTypes.append(currentType) - } - case .functions: - var currentNamespace: String? - for line in namespaceContentLines { - if let namespacePrefixRange = line.range(of: " public struct "), namespacePrefixRange.lowerBound == line.startIndex, let namespaceSuffixRange = line.range(of: " {"), namespaceSuffixRange.upperBound == line.endIndex { - currentNamespace = String(line[namespacePrefixRange.upperBound ..< namespaceSuffixRange.lowerBound]) - } else if let functionPrefixRange = line.range(of: " public static func "), functionPrefixRange.lowerBound == line.startIndex { - let functionName: String - if let bracketRange = line.range(of: "(") { - functionName = String(line[functionPrefixRange.upperBound ..< bracketRange.lowerBound]) - } else { - functionName = String(line[functionPrefixRange.upperBound...]) - } - resultFunctions.append(QualifiedName(namespace: currentNamespace, value: functionName)) - } - } - } - } - - return (resultTypes, resultFunctions) - } -} diff --git a/build-system/SwiftTL/Sources/SwiftTL/Resolution.swift b/build-system/SwiftTL/Sources/SwiftTL/Resolution.swift index 5fbd3a90d9..431300a431 100644 --- a/build-system/SwiftTL/Sources/SwiftTL/Resolution.swift +++ b/build-system/SwiftTL/Sources/SwiftTL/Resolution.swift @@ -199,7 +199,114 @@ enum Resolver { return types.values.sorted(by: { $0.name < $1.name }) } - + + static func resolveLayeredTypes( + layers: [(layerNumber: Int, constructors: [DescriptionParser.ConstructorDescription])] + ) throws -> [(layerNumber: Int, types: [SumType])] { + // Running state: for each constructor name, the target type name and the raw description. + // We keep raw descriptions (not resolved forms) because a later-layer constructor may + // introduce new target-type names, and resolveTypeReference needs the final target-type set. + var liveConstructors: [QualifiedName: (typeName: QualifiedName, description: DescriptionParser.ConstructorDescription)] = [:] + var result: [(layerNumber: Int, types: [SumType])] = [] + + for (layerNumber, layerConstructors) in layers { + // Apply this layer's constructors to the running map with last-wins semantics. + for constructorDescription in layerConstructors { + switch constructorDescription.type { + case let .type(name): + if !name.value[name.value.startIndex].isUppercase { + throw ResolutionError(text: "Type constructor \(constructorDescription.name) -> \(name): the resulting type name should begin with a capital letter") + } + liveConstructors[constructorDescription.name] = (name, constructorDescription) + case let .generic(name, argumentType): + throw ResolutionError(text: "Type constructor \(constructorDescription.name) can not be used to construct a generic type \(name)<\(argumentType)>") + } + } + + // Note: a constructor reassigned to a different target type in a later layer is + // removed from its old type's constructor set. If that drops the old type to zero + // constructors, it vanishes from this snapshot, and any unrelated argument that + // still references the old type via boxedType(...) will fail to resolve here. + // secret_scheme.tl does not exercise this case (same-name constructors always + // target the same type), but the rule applies if a future layered schema does. + // Snapshot: group by target type, resolve. + var constructedTypes: [QualifiedName: [DescriptionParser.ConstructorDescription]] = [:] + var constructorNameToType: [QualifiedName: QualifiedName] = [:] + for (ctorName, entry) in liveConstructors { + constructedTypes[entry.typeName, default: []].append(entry.description) + constructorNameToType[ctorName] = entry.typeName + } + + func resolveTypeReference(description: DescriptionParser.TypeReferenceDescription) throws -> TypeReference { + switch description { + case let .type(name): + if let resolvedBuiltinType = resolveBuiltinType(name: name) { + return resolvedBuiltinType + } + if name.value[name.value.startIndex].isUppercase { + if let _ = constructedTypes[name] { + return .boxedType(name) + } else { + throw ResolutionError(text: "Unresolved type \(name) in layer \(layerNumber)") + } + } else { + if let typeName = constructorNameToType[name] { + return .bareConstructor(typeName: typeName, name: name) + } else { + throw ResolutionError(text: "Unresolved type constructor \(name) in layer \(layerNumber)") + } + } + case let .generic(name, argumentType): + if name == "vector" { + return .bareVector(try resolveTypeReference(description: .type(name: argumentType))) + } else if name == "Vector" { + return .boxedVector(try resolveTypeReference(description: .type(name: argumentType))) + } else { + throw ResolutionError(text: "Unresolved generic type \(name) in layer \(layerNumber)") + } + } + } + + func resolveArgument(existingArguments: [Argument], description: DescriptionParser.ArgumentDescription) throws -> Argument { + return Argument( + name: description.name, + type: try resolveTypeReference(description: description.type), + condition: try description.condition.flatMap { condition -> Argument.Condition in + if !existingArguments.contains(where: { $0.name == condition.fieldName }) { + throw ResolutionError(text: "Unresolved conditional field reference to \(condition.fieldName) in layer \(layerNumber)") + } + return Argument.Condition(fieldName: condition.fieldName, bitIndex: condition.bitIndex) + } + ) + } + + var types: [QualifiedName: SumType] = [:] + for (typeName, constructorDescriptions) in constructedTypes { + let type = SumType(name: typeName) + for constructorDescription in constructorDescriptions { + var arguments: [Argument] = [] + for argumentDescription in constructorDescription.arguments { + arguments.append(try resolveArgument(existingArguments: arguments, description: argumentDescription)) + } + guard let id = constructorDescription.explicitId else { + throw ResolutionError(text: "Constructor \(constructorDescription.name) does not have an id") + } + type.constructors[constructorDescription.name] = SumType.Constructor( + name: constructorDescription.name, + id: id, + arguments: arguments + ) + } + types[type.name] = type + } + + let sortedTypes = types.values.sorted(by: { $0.name < $1.name }) + result.append((layerNumber, sortedTypes)) + } + + return result + } + static func resolveFunctions(types: [SumType], functionDescriptions: [DescriptionParser.ConstructorDescription]) throws -> [Function] { var functions: [QualifiedName: Function] = [:] diff --git a/build-system/SwiftTL/Sources/SwiftTL/main.swift b/build-system/SwiftTL/Sources/SwiftTL/main.swift index 30aa736667..df56ef9afa 100644 --- a/build-system/SwiftTL/Sources/SwiftTL/main.swift +++ b/build-system/SwiftTL/Sources/SwiftTL/main.swift @@ -27,26 +27,15 @@ if CommandLine.arguments.count < 3 { let schemeFilePath = CommandLine.arguments[1] let outputDirectoryPath = CommandLine.arguments[2] +var apiPrefix = "Api" -var stubFunctions = false -var printConstructorsRange: (start: Int, end: Int)? = nil -for arg in CommandLine.arguments { - if arg == "--stub-functions" { - stubFunctions = true - } - if arg.hasPrefix("--print-constructors=") { - let value = String(arg.dropFirst("--print-constructors=".count)) - let parts = value.split(separator: "-") - if parts.count == 2, let start = Int(parts[0]), let end = Int(parts[1]) { - if start > end { - print("Error: Invalid range for --print-constructors: start (\(start)) must be <= end (\(end))") - exit(1) - } - printConstructorsRange = (start, end) - } else { - print("Error: Invalid format for --print-constructors. Expected: --print-constructors=N-M (e.g., --print-constructors=0-10)") - exit(1) - } +for arg in CommandLine.arguments[3...] { + if arg.hasPrefix("--api-prefix=") { + let value = String(arg.dropFirst("--api-prefix=".count)) + apiPrefix = value + } else { + print("Error: Unknown argument: \(arg)") + exit(1) } } @@ -55,75 +44,63 @@ guard let data = try? String(contentsOfFile: schemeFilePath) else { exit(1) } -do { - let parsedData = try DescriptionParser.parse(data: data) - let resolvedTypes = try Resolver.resolveTypes(constructors: parsedData.constructors) - var resolvedFunctions = try Resolver.resolveFunctions(types: resolvedTypes, functionDescriptions: parsedData.functions) - - resolvedFunctions.append(Resolver.Function(name: QualifiedName(namespace: "help", value: "test"), id: 0xc0e202f7, arguments: [], result: .boxedType(QualifiedName(namespace: nil, value: "Bool")))) - - var constructorOrder: [(typeName: QualifiedName, constructorName: String)] = [] - var typeOrder: [(types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName])] = [] - - let sortedTypes = resolvedTypes.sorted(by: { $0.name < $1.name }) +do { + let parsedSchema = try DescriptionParser.parse(data: data) - if let range = printConstructorsRange { - print("--- CONSTRUCTORS ---") - for (index, type) in sortedTypes.enumerated() { - if index >= range.start && index < range.end { - for constructor in type.constructors.values.sorted(by: { $0.name < $1.name }) { - let storedArguments = constructor.arguments.filter { - if case .boolTrue = $0.type { return false } - return true - } - if !storedArguments.isEmpty { - let fieldNames = storedArguments.map { $0.name.camelCased } - print("\(constructor.name.value):\(fieldNames.joined(separator: ","))") - } - } + try FileManager.default.createDirectory(at: URL(fileURLWithPath: outputDirectoryPath), withIntermediateDirectories: true, attributes: nil) + + switch parsedSchema { + case let .flat(constructors, functions): + let resolvedTypes = try Resolver.resolveTypes(constructors: constructors) + var resolvedFunctions = try Resolver.resolveFunctions(types: resolvedTypes, functionDescriptions: functions) + + resolvedFunctions.append(Resolver.Function(name: QualifiedName(namespace: "help", value: "test"), id: 0xc0e202f7, arguments: [], result: .boxedType(QualifiedName(namespace: nil, value: "Bool")))) + + var constructorOrder: [(typeName: QualifiedName, constructorName: String)] = [] + var typeOrder: [(types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName])] = [] + + let sortedTypes = resolvedTypes.sorted(by: { $0.name < $1.name }) + + for type in sortedTypes { + for constructor in type.constructors.values.sorted(by: { $0.name < $1.name }) { + constructorOrder.append((type.name, constructor.name.value)) } } - print("--- END CONSTRUCTORS ---") - print("Total types: \(sortedTypes.count)") - exit(0) - } - for type in sortedTypes { - for constructor in type.constructors.values.sorted(by: { $0.name < $1.name }) { - constructorOrder.append((type.name, constructor.name.value)) + var totalConstructorCount = 0 + var currentConstructorCount = 0 + for type in sortedTypes { + if typeOrder.isEmpty || currentConstructorCount >= 32 { + typeOrder.append(([], [])) + currentConstructorCount = 0 + } + typeOrder[typeOrder.count - 1].types.append((type.name, type.constructors.values.sorted(by: { $0.name < $1.name }).map(\.name.value))) + currentConstructorCount += type.constructors.count + totalConstructorCount += type.constructors.count + if totalConstructorCount > 40 { } } - } - - var totalConstructorCount = 0 - var currentConstructorCount = 0 - for type in sortedTypes { - if typeOrder.isEmpty || currentConstructorCount >= 32 { - typeOrder.append(([], [])) - currentConstructorCount = 0 + + typeOrder.append(([], [])) + for function in resolvedFunctions.sorted(by: { $0.name < $1.name }) { + typeOrder[typeOrder.count - 1].functions.append(function.name) } - - typeOrder[typeOrder.count - 1].types.append((type.name, type.constructors.values.sorted(by: { $0.name < $1.name }).map(\.name.value))) - - currentConstructorCount += type.constructors.count - totalConstructorCount += type.constructors.count - - if totalConstructorCount > 40 { + + let generatedFiles = try CodeGenerator.generate(apiPrefix: apiPrefix, types: resolvedTypes, functions: resolvedFunctions, constructorOrder: constructorOrder, typeOrder: typeOrder) + + for (name, fileData) in generatedFiles { + let filePath = URL(fileURLWithPath: outputDirectoryPath).appendingPathComponent(name).path + let _ = try? FileManager.default.removeItem(atPath: filePath) + try fileData.write(toFile: filePath, atomically: true, encoding: .utf8) + } + + case let .layered(layers): + let resolvedLayers = try Resolver.resolveLayeredTypes(layers: layers) + for (layerNumber, types) in resolvedLayers { + let (filename, source) = try CodeGenerator.generateLayered(apiPrefix: apiPrefix, layerNumber: layerNumber, types: types) + let filePath = URL(fileURLWithPath: outputDirectoryPath).appendingPathComponent(filename).path + let _ = try? FileManager.default.removeItem(atPath: filePath) + try source.write(toFile: filePath, atomically: true, encoding: .utf8) } - } - - typeOrder.append(([], [])) - for function in resolvedFunctions.sorted(by: { $0.name < $1.name }) { - typeOrder[typeOrder.count - 1].functions.append(function.name) - } - - try FileManager.default.createDirectory(at: URL(fileURLWithPath: outputDirectoryPath), withIntermediateDirectories: true, attributes: nil) - - let generatedFiles = try CodeGenerator.generate(types: resolvedTypes, functions: resolvedFunctions, constructorOrder: constructorOrder, typeOrder: typeOrder, stubFunctions: stubFunctions) - - for (name, fileData) in generatedFiles { - let filePath = URL(fileURLWithPath: outputDirectoryPath).appendingPathComponent(name).path - let _ = try? FileManager.default.removeItem(atPath: filePath) - try fileData.write(toFile: filePath, atomically: true, encoding: .utf8) } } catch let e { print("\(e)") 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/build-system/decrypt.rb b/build-system/decrypt.rb deleted file mode 100644 index 1183ebf40d..0000000000 --- a/build-system/decrypt.rb +++ /dev/null @@ -1,114 +0,0 @@ -require 'base64' -require 'openssl' -require 'securerandom' - -class EncryptionV1 - ALGORITHM = 'aes-256-cbc' - - def decrypt(encrypted_data:, password:, salt:, hash_algorithm: "MD5") - cipher = ::OpenSSL::Cipher.new(ALGORITHM) - cipher.decrypt - - keyivgen(cipher, password, salt, hash_algorithm) - - data = cipher.update(encrypted_data) - data << cipher.final - end - - private - - def keyivgen(cipher, password, salt, hash_algorithm) - cipher.pkcs5_keyivgen(password, salt, 1, hash_algorithm) - end -end - -# The newer encryption mechanism, which features a more secure key and IV generation. -# -# The IV is randomly generated and provided unencrypted. -# The salt should be randomly generated and provided unencrypted (like in the current implementation). -# The key is generated with OpenSSL::KDF::pbkdf2_hmac with properly chosen parameters. -# -# Short explanation about salt and IV: https://stackoverflow.com/a/1950674/6324550 -class EncryptionV2 - ALGORITHM = 'aes-256-gcm' - - def decrypt(encrypted_data:, password:, salt:, auth_tag:) - cipher = ::OpenSSL::Cipher.new(ALGORITHM) - cipher.decrypt - - keyivgen(cipher, password, salt) - - cipher.auth_tag = auth_tag - - data = cipher.update(encrypted_data) - data << cipher.final - end - - private - - def keyivgen(cipher, password, salt) - keyIv = ::OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: 10_000, length: 32 + 12 + 24, hash: "sha256") - key = keyIv[0..31] - iv = keyIv[32..43] - auth_data = keyIv[44..-1] - - #puts "key: #{key.inspect}" - #puts "iv: #{iv.inspect}" - #puts "auth_data: #{auth_data.inspect}" - - cipher.key = key - cipher.iv = iv - cipher.auth_data = auth_data - end -end - -class MatchDataEncryption - V1_PREFIX = "Salted__" - V2_PREFIX = "match_encrypted_v2__" - - def decrypt(base64encoded_encrypted:, password:) - stored_data = Base64.decode64(base64encoded_encrypted) - if stored_data.start_with?(V2_PREFIX) - salt = stored_data[20..27] - auth_tag = stored_data[28..43] - data_to_decrypt = stored_data[44..-1] - - e = EncryptionV2.new - e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt, auth_tag: auth_tag) - else - salt = stored_data[8..15] - data_to_decrypt = stored_data[16..-1] - e = EncryptionV1.new - begin - # Note that we are not guaranteed to catch the decryption errors here if the password or the hash is wrong - # as there's no integrity checks. - # see https://github.com/fastlane/fastlane/issues/21663 - e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt) - # With the wrong hash_algorithm, there's here 0.4% chance that the decryption failure will go undetected - rescue => _ex - # With a wrong password, there's a 0.4% chance it will decrypt garbage and not fail - fallback_hash_algorithm = "SHA256" - e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt, hash_algorithm: fallback_hash_algorithm) - end - end - end -end - - -class MatchFileEncryption - def decrypt(file_path:, password:, output_path: nil) - output_path = file_path unless output_path - content = File.read(file_path) - e = MatchDataEncryption.new - decrypted_data = e.decrypt(base64encoded_encrypted: content, password: password) - File.binwrite(output_path, decrypted_data) - end -end - - -if ARGV.length != 3 - print 'Invalid command line' -else - dec = MatchFileEncryption.new - dec.decrypt(file_path: ARGV[1], password: ARGV[0], output_path: ARGV[2]) -end diff --git a/docs/superpowers/plans/2026-04-16-postbox-to-telegramengine-refactor-wave-1.md b/docs/superpowers/plans/2026-04-16-postbox-to-telegramengine-refactor-wave-1.md new file mode 100644 index 0000000000..26d958661b --- /dev/null +++ b/docs/superpowers/plans/2026-04-16-postbox-to-telegramengine-refactor-wave-1.md @@ -0,0 +1,983 @@ +# Postbox → TelegramEngine refactor, wave 1 — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Drop the direct `import Postbox` dependency from the first 10 leaf consumer submodules (one file each), routing data access through `TelegramEngine` while preserving behavior exactly. + +**Architecture:** For each of the 10 modules, apply the same deterministic playbook: inventory every Postbox reference in its single Postbox-importing file, swap bare Postbox type names for their engine typealiases (`PeerId` → `EnginePeer.Id`, etc.), replace imperative Postbox calls with existing engine methods or new thin engine wrappers added to TelegramCore in a preparatory commit, remove `import Postbox` and the Bazel dep, and run the full project build to verify. + +**Tech Stack:** Swift, Bazel (primary build system), Postbox (storage lib being made opaque), TelegramCore + TelegramEngine (the public facade), SSignalKit (signals). + +**Spec:** [docs/superpowers/specs/2026-04-16-postbox-to-telegramengine-refactor-wave-1-design.md](../specs/2026-04-16-postbox-to-telegramengine-refactor-wave-1-design.md) + +--- + +## Background the executor needs + +There are no unit tests in this project (`CLAUDE.md`: "No tests are used at the moment"). **The only verification is the full project build.** Every task ends with a full build that must go green before the next task starts. + +### The full build command + +Run from the repo root (`/Users/ali/build/telegram/telegram-ios`): + +```bash +source ~/.zshrc 2>/dev/null; \ +PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH \ + python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development \ + --gitCodesigningUseCurrent \ + --buildNumber 1 \ + --configuration debug_sim_arm64 +``` + +(`source ~/.zshrc` picks up `TELEGRAM_CODESIGNING_GIT_PASSWORD` and other env exports that the Claude Code bash shell doesn't inherit by default.) + +It is slow. Do not shortcut it with `bazel build //submodules/X` — the spec chose full build per module. + +### Engine typealias cheat sheet (already in TelegramCore) + +When removing `import Postbox`, bare Postbox names in the file must be swapped for their engine equivalents. The ones that exist as typealiases today (confirmed by grep over `submodules/TelegramCore/Sources/TelegramEngine/`): + +- `PeerId` → `EnginePeer.Id` +- `MessageId` → `EngineMessage.Id` +- `MessageIndex` → `EngineMessage.Index` +- `MessageTags` → `EngineMessage.Tags` +- `MessageAttribute` → `EngineMessage.Attribute` +- `MessageFlags` → `EngineMessage.Flags` +- `MessageForwardInfo` → `EngineMessage.ForwardInfo` +- `MediaId` → `EngineMedia.Id` +- `PreferencesEntry` → `EnginePreferencesEntry` +- `TempBox` (the singleton helper) → `EngineTempBox` +- `PinnedItemId` → `EngineChatList.PinnedItem.Id` + +If a task needs a Postbox type that has **no** existing engine typealias, the task may add one in `TelegramCore` (trivial `public typealias EngineX = X`) in the preparatory commit — this is explicitly allowed by the spec. + +### Engine wrapper locations (per the spec) + +- Data reads / subscriptions → new `TelegramEngine.EngineData.Item..` in `submodules/TelegramCore/Sources/TelegramEngine/Data/Data.swift`. +- Imperative signal-returning calls → new method on `Peers` / `Messages` / `Resources` / `AccountData` under `submodules/TelegramCore/Sources/TelegramEngine//`. +- Media-resource access → extend `engine.resources` (e.g. `engine.resources.data(...)`, `engine.resources.status(...)`), forwarding to `account.postbox.mediaBox.*` internally. +- Consumer-run `account.postbox.transaction { ... }` → a specific purpose-built engine method. No generic transaction escape hatch. + +### Static-check commands (run before the build in every task) + +```bash +grep -R "^import Postbox" submodules//Sources # must return empty +grep "submodules/Postbox" submodules//BUILD # must return empty +``` + +### Commit convention + +Per module, up to two commits (optional first, required second): + +1. `TelegramCore: add ` — only if new engine wrappers were needed. +2. `: drop direct Postbox dependency` — consumer edits + BUILD change. + +Always use a HEREDOC commit body. No `--amend`. Every commit must build. + +**TelegramCore wrapper commit template** (used by any task's Step 2a when engine wrappers/typealiases are added): + +```bash +git add submodules/TelegramCore/... +git commit -m "$(cat <<'EOF' +TelegramCore: add + +Prepares for to drop Postbox. +Searched TelegramEngine/ for existing equivalents: . + + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +### Build-failure handling (applies to every task's Step 6) + +When the full build fails after a consumer edit: + +- Read the **first** compiler error in the build output. +- If it's a name-resolution or type error in the module file being refactored, fix the mapping in that file and rebuild. +- If it's in a **different** module that depends on the module being refactored, a public signature changed unexpectedly. Either (a) revert that signature change so the public surface stays identical, or (b) if the new surface is genuinely better, extend the fix to the downstream call site **in the same commit**. +- If fixing would require editing a module outside the wave-1 list — or would require aliasing an umbrella type banned by spec rule 2 (`Postbox`, `Account`, `MediaBox`) — revert all changes from the current task and mark the module **Abandoned** in its task body with a one-line reason. Do NOT substitute a different module; the wave's done-count simply goes down by one. + +### The 10 modules (from the spec's deterministic selection rule) + +Reverse-dep count (over the 30-candidate pool) ascending, alphabetical tiebreak. Verified by running the selection script in Task 0: + +1. ActionSheetPeerItem — **ABANDONED** (see Task 1 body). Public init takes `postbox: Postbox`; ShareController caller is out-of-wave. +2. ChatInterfaceState — `submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift` — DONE +3. ChatListSearchRecentPeersNode — **ABANDONED** (see Task 3 body). Public init takes `postbox: Postbox`; ShareController + ChatListUI callers are out-of-wave. +4. ChatSendMessageActionUI — `submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift` +5. ContactListUI — `submodules/ContactListUI/Sources/ContactListNode.swift` +6. DirectMediaImageCache — **ABANDONED** (see Task 6 body). Public init takes `account: Account`; six out-of-wave callers. +7. DrawingUI — `submodules/DrawingUI/Sources/DrawingScreen.swift` +8. FetchManagerImpl — **ABANDONED** (see Task 8 body). Public init takes `postbox: Postbox`; TelegramUI caller is out-of-wave. +9. GalleryData — **ABANDONED** (see Task 9 body). Four public functions take `Media`/`Message` as parameters; refactor cascades into many out-of-wave downstream types (`AvatarGalleryEntry`, `MessageReference`, etc.). Good candidate for a bespoke future wave that migrates the domain types together. +10. ICloudResources — **ABANDONED** (see Task 10 body). Class conforms to `TelegramMediaResource` and inherits `isEqual(to: MediaResource)`; overriding that without aliasing the `MediaResource` protocol isn't possible. + +**Wave-1 done-count: 4** (Tasks 2, 4, 5, 7 done; Tasks 1, 3, 6, 8, 9, 10 abandoned). + +Per the spec's **abandonment protocol**, if a module hits an unresolvable blocker (requires aliasing an umbrella type such as `Postbox`/`Account`/`MediaBox`, or requires editing a module outside the wave-1 list), it is marked Abandoned in its task body and **not substituted**. The wave's done-count goes down by one; fallback modules are not pulled into the wave mid-execution. A later wave can revisit the abandoned module with tools not available in wave 1 (e.g. a real engine wrapper rather than a typealias, or a refactor that migrates the caller first). + +--- + +## Task 0: Verify selection and baseline build + +**Files:** +- Read: `submodules//BUILD` + +- [ ] **Step 1: Re-run the selection script to confirm the 10** + +Save and run this Python snippet from the repo root. It should output exactly the 10 modules listed above, in that order. + +```bash +python3 <<'EOF' +import os, re +pool = ["ActionSheetPeerItem","ChatInterfaceState","ChatListSearchRecentPeersNode","ChatSendMessageActionUI","ContactListUI","DirectMediaImageCache","DrawingUI","FetchManagerImpl","GalleryData","HorizontalPeerItem","ICloudResources","InAppPurchaseManager","InstantPageCache","InviteLinksUI","ItemListAvatarAndNameInfoItem","ItemListPeerItem","ItemListStickerPackItem","MapResourceToAvatarSizes","PhotoResources","PlatformRestrictionMatching","PresentationDataUtils","PromptUI","SaveToCameraRoll","SelectablePeerNode","ShareItems","SoftwareVideo","StickerPeekUI","StickerResources","TelegramIntents","TelegramNotices"] +deps = {} +for m in pool: + p = f"submodules/{m}/BUILD" + txt = open(p).read() if os.path.exists(p) else "" + deps[m] = {o for o in pool if o != m and re.search(rf'//submodules/{re.escape(o)}(:|"|$)', txt)} +rdep = {m:0 for m in pool} +for m,ds in deps.items(): + for d in ds: rdep[d]+=1 +for m in sorted(pool, key=lambda m:(rdep[m],m))[:10]: + print(m, rdep[m]) +EOF +``` + +Expected output (one per line): `ActionSheetPeerItem 0`, `ChatInterfaceState 0`, `ChatListSearchRecentPeersNode 0`, `ChatSendMessageActionUI 0`, `ContactListUI 0`, `DirectMediaImageCache 0`, `DrawingUI 0`, `FetchManagerImpl 0`, `GalleryData 0`, `ICloudResources 0`. + +If the output differs, stop and investigate — someone changed a BUILD file since the spec was written. + +- [ ] **Step 2: Run the baseline full build** + +Run the full build command above. Expected: PASS (green master). If it fails, stop — we need a green baseline before changing anything. Do not attempt to fix pre-existing build breakage as part of this plan. + +- [ ] **Step 3: No commit** + +Task 0 produces no code changes. + +--- + +## Task 1: Refactor `ActionSheetPeerItem` — **ABANDONED** + +**Status:** Abandoned for wave 1. No code changes in this repo from this task. + +**Reason:** Refactoring this module requires either (a) typealiasing the `Postbox` class itself (banned — see spec §Guiding rules rule 2: umbrella-type typealiases rename without encapsulating) or (b) editing `submodules/ShareController/` which is not in the wave-1 list. The module's designated init takes `postbox: Postbox` as a parameter and its sole out-of-wave caller (ShareController) passes `info.account.stateManager.postbox` directly, so there is no path to drop the `import Postbox` here without crossing the wave boundary or violating rule 2. Per the spec's **abandonment protocol**, the module is skipped for this wave. Wave-1 done-count is therefore 9, not 10. + +**Original task body (retained for audit trail, do not implement):** + +**Files:** +- Modify: `submodules/ActionSheetPeerItem/Sources/ActionSheetPeerItem.swift` +- Modify: `submodules/ActionSheetPeerItem/BUILD` + +**Starting inventory** (computed during planning): + +Grep for common Postbox API/type names in `ActionSheetPeerItem.swift` returned zero hits on `mediaBox`, `transaction`, `PostboxView`, `combinedView`, `PeerId`, `MessageId`, `MediaResource`, `CachedPeerData`, etc. The `import Postbox` line appears unused. Confirm this during inventory — it's the most likely case, but other Postbox symbols (e.g. types referenced inside a parameter type) may still be present. (Subsequent inventory discovered the module does take `postbox: Postbox` as a parameter type — this is what makes the module unrefactorable under the wave-1 rules.) + +- [ ] **Step 1: Inventory** + +Read `submodules/ActionSheetPeerItem/Sources/ActionSheetPeerItem.swift` top to bottom. Record every identifier that is Postbox-owned. If the inventory is empty, skip straight to Step 4. + +Run this helper grep too: + +```bash +grep -nE "\b(PeerId|MessageId|MessageIndex|MessageTags|MessageAttribute|MessageFlags|Peer|Media|MediaId|MediaResource|PostboxView|CachedPeerData|PreferencesEntry|ChatListIndex|PeerReference|TelegramMediaFile|TelegramMediaImage|Namespaces|TempBox)\b" submodules/ActionSheetPeerItem/Sources/ActionSheetPeerItem.swift +``` + +- [ ] **Step 2: Map each reference to a replacement** + +For each finding from Step 1, decide: existing engine typealias (see cheat sheet), existing engine method, existing TelegramCore non-Postbox export, or new engine wrapper. Record the mapping in your working notes. If a new wrapper is needed, it is added in Task 1a before Task 1 continues. + +- [ ] **Step 2a: (Only if Step 2 identified a missing engine wrapper/typealias) Add to TelegramCore** + +Edit the relevant file under `submodules/TelegramCore/Sources/TelegramEngine//` or `submodules/TelegramCore/Sources/TelegramEngine/Data/Data.swift`, following the wrapper-location rules in the Background section. Keep the wrapper minimal: a single typealias for name-only adds, or a thin method that forwards to the underlying Postbox call for imperative ones. + +Run the full build. It must pass. Commit: + +```bash +git add submodules/TelegramCore/... +git commit -m "$(cat <<'EOF' +TelegramCore: add + +Prepares for ActionSheetPeerItem to drop Postbox. + + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +Skip this step if Step 2 didn't identify any missing wrappers. + +- [ ] **Step 3: Edit the consumer file** + +In `submodules/ActionSheetPeerItem/Sources/ActionSheetPeerItem.swift`: + +- Apply every mapping from Step 2. +- Remove the line `import Postbox`. + +- [ ] **Step 4: Drop the Bazel dep** + +Edit `submodules/ActionSheetPeerItem/BUILD`. Remove the line `"//submodules/Postbox:Postbox",` from the `deps` array. Leave the rest of the BUILD untouched. + +- [ ] **Step 5: Static checks** + +Run: + +```bash +grep -R "^import Postbox" submodules/ActionSheetPeerItem/Sources # expect: empty +grep "submodules/Postbox" submodules/ActionSheetPeerItem/BUILD # expect: empty +``` + +Both must return no output. If either produces a hit, go back to Step 3 or Step 4. + +- [ ] **Step 6: Full project build** + +Run the full build command from the Background section. Expected: PASS. + +If it fails: +- Read the first error. If it's a name-resolution error in `ActionSheetPeerItem.swift`, fix the mapping and rebuild. +- If it's in a *different* module that depends on `ActionSheetPeerItem`, you changed a public signature unexpectedly; either revert that signature change or, if it's genuinely better, extend the fix to that downstream call site in the same commit. +- If the fix would require editing a module outside the wave-1 list, revert all Task 1 changes and skip to the next fallback module listed in the Background section. + +- [ ] **Step 7: Commit** + +```bash +git add submodules/ActionSheetPeerItem/ +git commit -m "$(cat <<'EOF' +ActionSheetPeerItem: drop direct Postbox dependency + +Route data access through TelegramEngine/TelegramCore; remove the +Postbox import and Bazel dep. Behavior-preserving. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 2: Refactor `ChatInterfaceState` + +**Files:** +- Modify: `submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift` +- Modify: `submodules/ChatInterfaceState/BUILD` + +**Starting inventory** (computed during planning): file references `MessageId` (×2) and `MediaResource` (×3). No `mediaBox`, `transaction`, `combinedView`, or `PostboxView` usage. This is a **type-reference-only** case — expected replacements are `MessageId` → `EngineMessage.Id` and `MediaResource` stays as-is only if a typealias exists, otherwise a typealias `EngineMediaResource = MediaResource` is added in TelegramCore. + +- [ ] **Step 1: Inventory** + +Read `submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift`. Confirm the grep below matches the planning inventory and records exact line numbers and declaration contexts (parameter types, property types, return types, generic arguments). + +```bash +grep -nE "\b(PeerId|MessageId|MessageIndex|MessageTags|MessageAttribute|MessageFlags|Peer|Media|MediaId|MediaResource|PostboxView|CachedPeerData|PreferencesEntry|ChatListIndex|PeerReference|TelegramMediaFile|TelegramMediaImage|Namespaces|TempBox)\b" submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift +``` + +- [ ] **Step 2: Map each reference** + +- `MessageId` → `EngineMessage.Id` (existing typealias, no wrapper needed). +- `MediaResource`: search `submodules/TelegramCore/Sources/TelegramEngine/` for a `public typealias Engine.*Resource.*= MediaResource`. If present, use it. If absent, proceed to Step 2a and add a typealias `public typealias EngineMediaResource = MediaResource` in `submodules/TelegramCore/Sources/TelegramEngine/Resources/` (new file `EngineMediaResource.swift`, or the most natural existing file in that folder). + +- [ ] **Step 2a: (Only if needed) Add engine typealias(es) in TelegramCore** + +For each Postbox type without an engine typealias, add a `public typealias Engine = ` in the appropriate TelegramEngine area file. Do not introduce any new wrapper structs. + +Run full build, expect PASS. Commit: + +```bash +git add submodules/TelegramCore/... +git commit -m "$(cat <<'EOF' +TelegramCore: add engine typealiases for + +Prepares for ChatInterfaceState to drop Postbox. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +- [ ] **Step 3: Edit the consumer file** + +Apply the mappings from Step 2 to every reference. Remove the line `import Postbox`. + +- [ ] **Step 4: Drop the Bazel dep** + +Edit `submodules/ChatInterfaceState/BUILD`. Remove `"//submodules/Postbox:Postbox",` from `deps`. + +- [ ] **Step 5: Static checks** + +```bash +grep -R "^import Postbox" submodules/ChatInterfaceState/Sources # expect: empty +grep "submodules/Postbox" submodules/ChatInterfaceState/BUILD # expect: empty +``` + +- [ ] **Step 6: Full project build** + +Run the full build command. Expected: PASS. Handle failures per the rules in Task 1 Step 6. + +- [ ] **Step 7: Commit** + +```bash +git add submodules/ChatInterfaceState/ +git commit -m "$(cat <<'EOF' +ChatInterfaceState: drop direct Postbox dependency + +Switch remaining Postbox-typed references to engine typealiases; +remove the Postbox import and Bazel dep. Behavior-preserving. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 3: Refactor `ChatListSearchRecentPeersNode` — **ABANDONED** + +**Status:** Abandoned for wave 1. No code changes in this repo from this task. + +**Reason:** The module's public `init` at line 207 takes `postbox: Postbox` as a parameter. Two out-of-wave callers (`submodules/ShareController/Sources/ShareControllerRecentPeersGridItem.swift`, `submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift`) use this init. Refactoring requires either typealiasing the `Postbox` class (banned by spec rule 2) or editing those two out-of-wave modules (banned by wave boundary). Per the abandonment protocol, the module is skipped. + +**Original task body (retained for audit trail, do not implement):** + +**Files:** +- Modify: `submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift` +- Modify: `submodules/ChatListSearchRecentPeersNode/BUILD` + +**Starting inventory** (computed during planning): file uses `postbox.transaction { ... }` (×2), `postbox.combinedView(...)` (×1), and references `TelegramMedia*` types (×3). This is the **first hard module** in the wave — it has real imperative Postbox calls that require engine wrappers, not just typealiases. + +- [ ] **Step 1: Inventory** + +Read the whole file. For each Postbox call, capture: +- The call site (line number, containing function). +- The `PostboxViewKey`(s) passed to `combinedView`. +- What the closure body of each `transaction` does — the *intent*, not just the code. (This determines which engine method to add.) + +Run: + +```bash +grep -nE "\b(postbox\.|mediaBox|transaction\s*\{|combinedView|PostboxView|PostboxViewKey|Namespaces\.|TelegramMedia|PeerId|MessageId)\b" submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift +``` + +- [ ] **Step 2: Map each reference** + +- `TelegramMedia*` — these classes are defined in `TelegramCore` (check `submodules/TelegramCore/Sources/`), not Postbox. After `import Postbox` is removed they remain reachable via `import TelegramCore`, which the file already imports. No action beyond confirming. +- Each `postbox.combinedView` / view subscription → map to an existing `TelegramEngine.data.subscribe(...)` item if one exists for the same `PostboxViewKey`; if not, add an `EngineData.Item` under `submodules/TelegramCore/Sources/TelegramEngine/Data/Data.swift`. +- Each `postbox.transaction { ... }` → a specific new method on the matching engine area (e.g. `TelegramEngine.Peers.recordRecentPeer(id:)` if that's what the closure does). Do **not** add a generic transaction passthrough. + +Write the mapping down before editing. Each new engine method is small and focused. + +- [ ] **Step 2a: Add engine wrappers in TelegramCore** + +For each new `EngineData.Item` or engine method identified in Step 2: + +- Add it to the appropriate file under `submodules/TelegramCore/Sources/TelegramEngine//` (or `…/Data/Data.swift` for data items). +- Keep the body to a minimal pass-through: the new engine method opens a transaction internally and calls the same Postbox code that the consumer was running; the new `EngineData.Item` forwards a `PostboxViewKey` in `keys()` and extracts its `PostboxView` in `extract()`. +- Return engine-typed values where existing engine types are available; otherwise return primitives or `Void`. Do not return bare Postbox types. + +Before editing TelegramCore, grep for existing wrappers covering the same need: + +```bash +grep -rn "\|" submodules/TelegramCore/Sources/TelegramEngine/ +``` + +Record "searched for X, found/not found" in the commit message. + +Run the full build. Expected: PASS. + +Commit: + +```bash +git add submodules/TelegramCore/... +git commit -m "$(cat <<'EOF' +TelegramCore: add + +Prepares for ChatListSearchRecentPeersNode to drop Postbox. +Searched TelegramEngine/ for existing equivalents: not found. + + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +- [ ] **Step 3: Edit the consumer file** + +Replace each `postbox.transaction` and `postbox.combinedView` call with the engine method/subscription added in Step 2a. Swap any Postbox-typed names for engine typealiases per the cheat sheet. Remove `import Postbox`. + +- [ ] **Step 4: Drop the Bazel dep** + +Edit `submodules/ChatListSearchRecentPeersNode/BUILD`. Remove `"//submodules/Postbox:Postbox",` from `deps`. + +- [ ] **Step 5: Static checks** + +```bash +grep -R "^import Postbox" submodules/ChatListSearchRecentPeersNode/Sources # expect: empty +grep "submodules/Postbox" submodules/ChatListSearchRecentPeersNode/BUILD # expect: empty +``` + +- [ ] **Step 6: Full project build** + +Run the full build command. Expected: PASS. Handle failures per the Build-failure handling rules in the Background section. + +- [ ] **Step 7: Commit** + +```bash +git add submodules/ChatListSearchRecentPeersNode/ +git commit -m "$(cat <<'EOF' +ChatListSearchRecentPeersNode: drop direct Postbox dependency + +Route combined-view subscription and transactions through +TelegramEngine; remove the Postbox import and Bazel dep. +Behavior-preserving. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 4: Refactor `ChatSendMessageActionUI` + +**Files:** +- Modify: `submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift` +- Modify: `submodules/ChatSendMessageActionUI/BUILD` + +**Starting inventory** (computed during planning): `mediaBox` (×2), `Peer` type reference (×1), `Media` type reference (×1), `MediaResource` (×1), `Namespaces.` (×1). The mediaBox calls are the substantive work. + +- [ ] **Step 1: Inventory** + +Read the whole file. Capture every `mediaBox` call — what resource is being asked for and what is done with the result (data read, status subscription, fetch start, path access)? Capture each Postbox-typed reference's line/context. + +```bash +grep -nE "\bmediaBox\b|\bNamespaces\.|\b(Peer|Media|MediaResource)\b" submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +``` + +- [ ] **Step 2: Map each reference** + +- Each `mediaBox.` call → `engine.resources.(...)`. Check for an existing method on `TelegramEngine.Resources` first: + ```bash + grep -rn "extension.*Resources\|public func" submodules/TelegramCore/Sources/TelegramEngine/Resources/ + ``` + If an equivalent exists, use it. If not, add one in Step 2a. +- `Peer`, `Media`, `MediaResource` type references → use `EnginePeer`, `EngineMedia`, or `EngineMediaResource` (add typealias if missing, per the cheat sheet). +- `Namespaces.Peer.` — defined in TelegramCore, not Postbox. Confirm via grep; no change needed. + +- [ ] **Step 2a: (Only if needed) Add engine wrappers in TelegramCore** + +Per the rules — minimal pass-through, return engine-typed values. Build, commit `TelegramCore: add ` per the template in Task 3 Step 2a. + +- [ ] **Step 3: Edit the consumer file** + +Apply mappings. Remove `import Postbox`. + +- [ ] **Step 4: Drop the Bazel dep** + +Edit `submodules/ChatSendMessageActionUI/BUILD`. Remove `"//submodules/Postbox:Postbox",` from `deps`. + +- [ ] **Step 5: Static checks** + +```bash +grep -R "^import Postbox" submodules/ChatSendMessageActionUI/Sources # expect: empty +grep "submodules/Postbox" submodules/ChatSendMessageActionUI/BUILD # expect: empty +``` + +- [ ] **Step 6: Full project build** + +Run the full build. Expected: PASS. Handle failures per the Build-failure handling rules in the Background section. + +- [ ] **Step 7: Commit** + +```bash +git add submodules/ChatSendMessageActionUI/ +git commit -m "$(cat <<'EOF' +ChatSendMessageActionUI: drop direct Postbox dependency + +Route MediaBox calls through TelegramEngine.resources and switch +type references to engine typealiases; remove the Postbox import +and Bazel dep. Behavior-preserving. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 5: Refactor `ContactListUI` + +**Files:** +- Modify: `submodules/ContactListUI/Sources/ContactListNode.swift` +- Modify: `submodules/ContactListUI/BUILD` + +**Starting inventory** (computed during planning): `postbox.transaction { ... }` (×4), `Peer` references (×15), `Namespaces.` (×1). Transactions are the substantive work; the 15 `Peer` references are likely in closures reading transaction state and will switch to engine-typed returns once the transactions are replaced. + +- [ ] **Step 1: Inventory** + +Read the whole file. For each `transaction` call, describe what the closure does — this drives what new engine methods to add. Capture every `Peer`-typed declaration. + +```bash +grep -nE "\btransaction\s*\{|account\.postbox|\b(Peer|PeerId|Namespaces)\b" submodules/ContactListUI/Sources/ContactListNode.swift +``` + +- [ ] **Step 2: Map each reference** + +- Each `postbox.transaction { ... }` → a dedicated engine method under `submodules/TelegramCore/Sources/TelegramEngine/{Peers,Contacts,AccountData}/` capturing the closure's intent. Never add a generic transaction passthrough. +- `Peer` type → `EnginePeer` where the value actually flows through the replaced engine method (the engine method should return `EnginePeer` / `[EnginePeer]`). For local variable types that receive the engine-method return, use the engine type. +- `Namespaces.*` — defined in TelegramCore. No change. + +Before adding methods, grep for existing engine functions that may already cover the intent: + +```bash +grep -rn "public func" submodules/TelegramCore/Sources/TelegramEngine/Contacts/ +grep -rn "public func" submodules/TelegramCore/Sources/TelegramEngine/Peers/ | head -60 +``` + +- [ ] **Step 2a: Add engine wrappers in TelegramCore** + +Add each new method. Return engine-typed values. Build; then use the TelegramCore wrapper commit template from the Background section. + +- [ ] **Step 3: Edit the consumer file** + +Replace every `transaction` call with its engine method. Switch `Peer` locals to `EnginePeer`. Remove `import Postbox`. + +- [ ] **Step 4: Drop the Bazel dep** + +Edit `submodules/ContactListUI/BUILD`. Remove `"//submodules/Postbox:Postbox",` from `deps`. + +- [ ] **Step 5: Static checks** + +```bash +grep -R "^import Postbox" submodules/ContactListUI/Sources # expect: empty +grep "submodules/Postbox" submodules/ContactListUI/BUILD # expect: empty +``` + +- [ ] **Step 6: Full project build** + +Run the full build. Expected: PASS. Handle failures per the Build-failure handling rules in the Background section. ContactListUI is imported by other submodules (TelegramUI, SettingsUI, etc.); downstream breakage is most likely here. If a downstream consumer needs a bare `Peer`, either keep the public surface returning engine types (preferred — they're typealiases under the hood) or, if the downstream change is large, revert Task 5 and skip. + +- [ ] **Step 7: Commit** + +```bash +git add submodules/ContactListUI/ +git commit -m "$(cat <<'EOF' +ContactListUI: drop direct Postbox dependency + +Replace direct postbox.transaction calls with dedicated engine +methods; switch peer references to engine-typed equivalents; +remove the Postbox import and Bazel dep. Behavior-preserving. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 6: Refactor `DirectMediaImageCache` — **ABANDONED** + +**Status:** Abandoned for wave 1. No code changes in this repo from this task. + +**Reason:** The module's public `init(account: Account)` at line 241 takes `account: Account` (an umbrella type banned by spec rule 2). Out-of-wave callers include `submodules/CalendarMessageScreen/`, four TelegramUI components (`StoryContainerScreen`, `ShareWithPeersScreen`, `PeerInfoVisualMediaPaneNode` × 2), and `submodules/TelegramUI/Sources/AccountContext.swift`. Refactoring requires either aliasing `Account` (banned) or editing all those out-of-wave callers (banned). Per the abandonment protocol, the module is skipped. + +**Original task body (retained for audit trail, do not implement):** + +**Files:** +- Modify: `submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift` +- Modify: `submodules/DirectMediaImageCache/BUILD` + +**Starting inventory** (computed during planning): `mediaBox` (×11), `PeerReference` (×6), `MediaResource` (×1), `TelegramMedia*` (×13), `Media`/`Message` type references. This module is **mediaBox-heavy** and is the canonical shape for the `engine.resources.*` extension work. + +- [ ] **Step 1: Inventory** + +Read the whole file. For each `mediaBox` call, record the method (`resourceData`, `resourceStatus`, `cachedResourceRepresentation`, `storeCachedResourceRepresentation`, `fetchedResource`, etc.) and whether it reads, writes, or subscribes. + +```bash +grep -nE "\bmediaBox\b|\b(PeerReference|MediaResource|TelegramMedia)" submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift +``` + +- [ ] **Step 2: Map each reference** + +Each distinct `mediaBox.` signature → a method on `TelegramEngine.Resources`. Expected additions (names are suggestions — match existing naming if anything close already exists): + +- `engine.resources.data(_:pathExtension:option:attemptSynchronously:) -> Signal` +- `engine.resources.status(_:approximateSynchronousValue:) -> Signal` +- `engine.resources.cachedRepresentationData(_:representation:complete:) -> Signal<...>` +- `engine.resources.storeCachedRepresentation(_:representation:data:) -> Signal` + +Before adding any of these, grep `submodules/TelegramCore/Sources/TelegramEngine/Resources/` for existing equivalents and only add what's missing. + +`PeerReference`, `TelegramMedia*`, `MediaResource` — check each: `TelegramMedia*` types live in `TelegramCore` (not Postbox). `PeerReference` lives in `TelegramCore`. `MediaResource` is a Postbox protocol; add `EngineMediaResource = MediaResource` typealias if not already present. + +- [ ] **Step 2a: Add engine wrappers in TelegramCore** + +Add all missing methods on `Resources` and any missing typealiases. Each method is a one-line forward to `account.postbox.mediaBox.*`. Build; then commit using the TelegramCore wrapper commit template from the Background section, recording "searched Resources/ for equivalents: found/not found" in the message. + +- [ ] **Step 3: Edit the consumer file** + +Replace every `mediaBox.*` with `engine.resources.*`. This likely requires adding an `engine: TelegramEngine` parameter to a few internal functions in the file (or surfacing it from an existing `AccountContext` already in scope — prefer that). Switch types to engine typealiases. Remove `import Postbox`. + +- [ ] **Step 4: Drop the Bazel dep** + +Edit `submodules/DirectMediaImageCache/BUILD`. Remove `"//submodules/Postbox:Postbox",` from `deps`. + +- [ ] **Step 5: Static checks** + +```bash +grep -R "^import Postbox" submodules/DirectMediaImageCache/Sources # expect: empty +grep "submodules/Postbox" submodules/DirectMediaImageCache/BUILD # expect: empty +``` + +- [ ] **Step 6: Full project build** + +Run the full build. Expected: PASS. Handle failures per the Build-failure handling rules in the Background section. + +- [ ] **Step 7: Commit** + +```bash +git add submodules/DirectMediaImageCache/ +git commit -m "$(cat <<'EOF' +DirectMediaImageCache: drop direct Postbox dependency + +Route MediaBox calls through TelegramEngine.resources; remove the +Postbox import and Bazel dep. Behavior-preserving. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 7: Refactor `DrawingUI` + +**Files:** +- Modify: `submodules/DrawingUI/Sources/DrawingScreen.swift` +- Modify: `submodules/DrawingUI/BUILD` + +**Starting inventory** (computed during planning): `transaction` (×3), `Media` type references (×13), `Namespaces.` (×4). Transactions are the substantive work; `Media`/`Namespaces` are mostly referencing TelegramCore-defined types already. + +- [ ] **Step 1: Inventory** + +Read the whole file. For each `transaction` call, describe what the closure does. Capture every `Media`-typed declaration and every `Namespaces.*` reference. + +```bash +grep -nE "\btransaction\s*\{|account\.postbox|\b(Media|MediaId|Namespaces)\b" submodules/DrawingUI/Sources/DrawingScreen.swift +``` + +- [ ] **Step 2: Map each reference** + +- Each `postbox.transaction` → dedicated engine method. Inspect the closure to find the right home (`Stickers`, `Messages`, `Peers`, …). +- `Media` → `EngineMedia` where the value flows through new engine methods; keep as `Media` (TelegramCore re-defined concrete classes like `TelegramMediaFile` live in TelegramCore and are fine) where the type is already TelegramCore's. +- `Namespaces.*` — TelegramCore. No change. + +- [ ] **Step 2a: Add engine wrappers in TelegramCore** + +Add each new transaction-replacing method. Build; then use the TelegramCore wrapper commit template from the Background section. + +- [ ] **Step 3: Edit the consumer file** + +Apply mappings. Remove `import Postbox`. + +- [ ] **Step 4: Drop the Bazel dep** + +Edit `submodules/DrawingUI/BUILD`. Remove `"//submodules/Postbox:Postbox",` from `deps`. + +- [ ] **Step 5: Static checks** + +```bash +grep -R "^import Postbox" submodules/DrawingUI/Sources # expect: empty +grep "submodules/Postbox" submodules/DrawingUI/BUILD # expect: empty +``` + +- [ ] **Step 6: Full project build** + +Run the full build. Expected: PASS. Handle failures per the Build-failure handling rules in the Background section. + +- [ ] **Step 7: Commit** + +```bash +git add submodules/DrawingUI/ +git commit -m "$(cat <<'EOF' +DrawingUI: drop direct Postbox dependency + +Replace direct postbox.transaction calls with dedicated engine +methods; switch type references to engine equivalents; remove the +Postbox import and Bazel dep. Behavior-preserving. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 8: Refactor `FetchManagerImpl` — **ABANDONED** + +**Status:** Abandoned for wave 1. No code changes in this repo from this task. + +**Reason:** The module's public `init(postbox: Postbox, storeManager: DownloadedMediaStoreManager?)` at line 708 takes `postbox: Postbox`. Out-of-wave caller: `submodules/TelegramUI/Sources/AccountContext.swift:296`. Refactoring requires either aliasing the `Postbox` class (banned by spec rule 2) or editing TelegramUI (banned by wave boundary). Per the abandonment protocol, the module is skipped. + +**Original task body (retained for audit trail, do not implement):** + +**Files:** +- Modify: `submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift` +- Modify: `submodules/FetchManagerImpl/BUILD` + +**Starting inventory** (computed during planning): `mediaBox` (×8), `MediaResource` (×4), `TelegramMedia` (×1). Shape is similar to DirectMediaImageCache — heavy `mediaBox` usage, no transactions. + +- [ ] **Step 1: Inventory** + +Read the whole file. For each `mediaBox` call, record the method and direction (read / subscribe / fetch-start). + +```bash +grep -nE "\bmediaBox\b|\b(MediaResource|TelegramMedia)\b" submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift +``` + +- [ ] **Step 2: Map each reference** + +Reuse every `engine.resources.*` method added in Task 6 — do not re-add them. Grep: + +```bash +grep -rn "extension.*Resources\|public func" submodules/TelegramCore/Sources/TelegramEngine/Resources/ +``` + +If this task needs a `mediaBox` operation that Task 6 did not add (e.g. `cancelInteractiveResourceFetch`, `completeInteractiveResourceFetch`), add it now in the same pattern. + +`MediaResource` → `EngineMediaResource` typealias (already added in an earlier task if needed). `TelegramMedia` — TelegramCore type, no change. + +- [ ] **Step 2a: (Only if needed) Add missing engine methods** + +Minimal pass-through on `TelegramEngine.Resources`. Build, commit. + +- [ ] **Step 3: Edit the consumer file** + +Replace every `mediaBox.*` with `engine.resources.*`. Thread an `engine` argument through internal functions as needed (prefer reading it off an existing `AccountContext` already in scope). Remove `import Postbox`. + +- [ ] **Step 4: Drop the Bazel dep** + +Edit `submodules/FetchManagerImpl/BUILD`. Remove `"//submodules/Postbox:Postbox",` from `deps`. + +- [ ] **Step 5: Static checks** + +```bash +grep -R "^import Postbox" submodules/FetchManagerImpl/Sources # expect: empty +grep "submodules/Postbox" submodules/FetchManagerImpl/BUILD # expect: empty +``` + +- [ ] **Step 6: Full project build** + +Run the full build. Expected: PASS. Handle failures per the Build-failure handling rules in the Background section. + +- [ ] **Step 7: Commit** + +```bash +git add submodules/FetchManagerImpl/ +git commit -m "$(cat <<'EOF' +FetchManagerImpl: drop direct Postbox dependency + +Route MediaBox fetch/status calls through TelegramEngine.resources; +remove the Postbox import and Bazel dep. Behavior-preserving. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 9: Refactor `GalleryData` — **ABANDONED** + +**Status:** Abandoned for wave 1. No code changes in this repo from this task. + +**Reason:** Four public functions take `Media` (Postbox protocol) and/or `Message` (Postbox class) as parameters, called from TelegramUI and ChatListUI (out-of-wave). Refactoring to `EngineMedia` / `EngineMessage` requires `.init(_:)` / `._asMedia()` / `._asMessage()` coercions threaded through many local variables (e.g. `var galleryMedia: Media?` in `chatMessageGalleryControllerData` is reassigned from various `TelegramMedia*` casts and passed to `MessageReference(...)` chains and enum cases), which would cascade into `AvatarGalleryEntry`, `MessageReference`, and other out-of-wave types. The narrow-utility alias path is ruled out because `Media` and especially `Message` are domain types, not utilities. Per the abandonment protocol, the module is skipped. + +**Original task body (retained for audit trail, do not implement):** + +**Files:** +- Modify: `submodules/GalleryData/Sources/GalleryData.swift` +- Modify: `submodules/GalleryData/BUILD` + +**Starting inventory** (computed during planning): `Peer` (×1), `Media` (×9), `Message` (×4), `Namespaces.` (×3), `TelegramMedia*` (×30). No `mediaBox`, no `transaction`, no `combinedView`. This is a **type-reference-only** case at scale. + +- [ ] **Step 1: Inventory** + +Read the whole file. Record every declaration that uses a Postbox-owned type (`Peer`, `Media`, `Message`, `MessageId`, etc.) — note that `TelegramMedia*` and `Namespaces` are TelegramCore, not Postbox, and do **not** need changing. + +```bash +grep -nE "\b(Peer|Media|Message|MessageId|MessageIndex)\b" submodules/GalleryData/Sources/GalleryData.swift | head -60 +``` + +- [ ] **Step 2: Map each reference** + +- `Peer` → `EnginePeer` at the call-site level where the value is newly produced; for existing public signatures that already accept a `Peer` from elsewhere, prefer `EnginePeer` **only if** downstream consumers accept it. Otherwise leave the signature alone and swap only the internal uses. +- `Media`, `Message`, `MessageId` → engine typealiases per the cheat sheet. +- `TelegramMedia*`, `Namespaces.*` — no change. + +- [ ] **Step 2a: (Only if needed) Add engine typealiases** + +Add any missing typealias in `submodules/TelegramCore/Sources/TelegramEngine/…`. Build, commit. + +- [ ] **Step 3: Edit the consumer file** + +Apply mappings. Remove `import Postbox`. + +- [ ] **Step 4: Drop the Bazel dep** + +Edit `submodules/GalleryData/BUILD`. Remove `"//submodules/Postbox:Postbox",` from `deps`. + +- [ ] **Step 5: Static checks** + +```bash +grep -R "^import Postbox" submodules/GalleryData/Sources # expect: empty +grep "submodules/Postbox" submodules/GalleryData/BUILD # expect: empty +``` + +- [ ] **Step 6: Full project build** + +Run the full build. Expected: PASS. Handle failures per the Build-failure handling rules in the Background section. + +- [ ] **Step 7: Commit** + +```bash +git add submodules/GalleryData/ +git commit -m "$(cat <<'EOF' +GalleryData: drop direct Postbox dependency + +Switch Postbox-typed references to engine typealiases; remove the +Postbox import and Bazel dep. Behavior-preserving. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 10: Refactor `ICloudResources` — **ABANDONED** + +**Status:** Abandoned for wave 1. No code changes in this repo from this task. + +**Reason:** The module declares `public class ICloudFileResource: TelegramMediaResource` and thus must implement `func isEqual(to: MediaResource) -> Bool` (protocol requirement inherited from `MediaResource`). That override's parameter type is fixed at `MediaResource`, which can only be named by importing Postbox or adding a typealias for the raw `MediaResource` protocol. The protocol-alias would be borderline per rule 2; user directed to skip. Per the abandonment protocol, the module is skipped. + +**Original task body (retained for audit trail, do not implement):** + +### Original Task 10 + +**Files:** +- Modify: `submodules/ICloudResources/Sources/ICloudResources.swift` +- Modify: `submodules/ICloudResources/BUILD` + +**Starting inventory** (computed during planning): `MediaResource` (×2), `TelegramMedia` (×1). No `mediaBox`, no `transaction`. Small type-reference-only module. The `MediaResource` uses may be a custom `MediaResource`-conforming class defined in this file — confirm during inventory. + +- [ ] **Step 1: Inventory** + +Read the whole file. `MediaResource` is a Postbox protocol; `ICloudResources` likely declares a custom class conforming to it. Capture whether (a) the file declares new `MediaResource`-conforming types, (b) it only references the protocol, or both. + +```bash +grep -nE "\b(MediaResource|TelegramMedia)\b|class.*:.*MediaResource|struct.*:.*MediaResource" submodules/ICloudResources/Sources/ICloudResources.swift +``` + +- [ ] **Step 2: Map each reference** + +- If the file declares a type conforming to `MediaResource`, use the `EngineMediaResource` typealias in the declaration (`class FooResource: EngineMediaResource { ... }`). Because typealiases are transparent, this keeps protocol conformance identical. +- All other `MediaResource` references → `EngineMediaResource`. +- `TelegramMedia*` — TelegramCore, no change. + +Add `EngineMediaResource` typealias in TelegramCore if not already present (Task 2 / Task 6 may have added it; check first). + +- [ ] **Step 2a: (Only if needed) Add `EngineMediaResource` typealias** + +```swift +// submodules/TelegramCore/Sources/TelegramEngine/Resources/EngineMediaResource.swift +import Postbox +public typealias EngineMediaResource = MediaResource +``` + +Build, commit. + +- [ ] **Step 3: Edit the consumer file** + +Apply mappings. Remove `import Postbox`. + +- [ ] **Step 4: Drop the Bazel dep** + +Edit `submodules/ICloudResources/BUILD`. Remove `"//submodules/Postbox:Postbox",` from `deps`. + +- [ ] **Step 5: Static checks** + +```bash +grep -R "^import Postbox" submodules/ICloudResources/Sources # expect: empty +grep "submodules/Postbox" submodules/ICloudResources/BUILD # expect: empty +``` + +- [ ] **Step 6: Full project build** + +Run the full build. Expected: PASS. Handle failures per the Build-failure handling rules in the Background section. + +- [ ] **Step 7: Commit** + +```bash +git add submodules/ICloudResources/ +git commit -m "$(cat <<'EOF' +ICloudResources: drop direct Postbox dependency + +Switch MediaResource references to EngineMediaResource; remove the +Postbox import and Bazel dep. Behavior-preserving. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 11: Wave-1 completion verification + +**Files:** No code changes. + +- [ ] **Step 1: Static check across all 10 modules** + +```bash +for m in ActionSheetPeerItem ChatInterfaceState ChatListSearchRecentPeersNode ChatSendMessageActionUI ContactListUI DirectMediaImageCache DrawingUI FetchManagerImpl GalleryData ICloudResources; do + echo "=== $m ===" + grep -R "^import Postbox" submodules/$m/Sources && echo "FAIL: import in $m" + grep "submodules/Postbox" submodules/$m/BUILD && echo "FAIL: dep in $m" +done +``` + +Expected: no `FAIL` lines printed. If any appear, return to the corresponding task and fix. + +- [ ] **Step 2: Final full build** + +Run the full build one more time from a clean state. Expected: PASS. (If it passed at the end of Task 10 and nothing else has changed, this should be cached and fast.) + +- [ ] **Step 3: Review the commit log** + +```bash +git log --oneline master..HEAD +``` + +Expected: a run of commits matching the pattern `TelegramCore: add …` (optional) and `: drop direct Postbox dependency` (one per module done). If any module was skipped per the fallback rule, verify the fallback ran and a replacement module completed so the total is 10. + +- [ ] **Step 4: No commit** + +Verification only. + +--- + +## What's explicitly NOT in this plan + +- Any edits to `TelegramCore`, `Postbox`, or the 64 modules outside the chosen 10 (except the minimum engine-wrapper / typealias additions to `TelegramCore` that the chosen modules need). +- Any new `Engine*` wrapper *structs* (only typealiases and forwarding methods are in scope this wave). +- Any generic `engine.transaction { postbox in … }` escape hatch. +- Any behavior change, performance tweak, or "while we're here" cleanup. +- Any test work — there are no tests in this project. diff --git a/docs/superpowers/plans/2026-04-17-listview-pin-to-edge.md b/docs/superpowers/plans/2026-04-17-listview-pin-to-edge.md new file mode 100644 index 0000000000..e250e25aef --- /dev/null +++ b/docs/superpowers/plans/2026-04-17-listview-pin-to-edge.md @@ -0,0 +1,223 @@ +# ListView pin-to-edge Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Implement the first-pinned-item-to-bottom-edge behavior in `ListView` by adding a `calculatePinToEdgeTopInset()` helper and wiring it into `snapToBounds` and `updateScroller`, matching the design in [docs/superpowers/specs/2026-04-17-listview-pin-to-edge-design.md](../specs/2026-04-17-listview-pin-to-edge-design.md). + +**Architecture:** Heights-based virtual-top-inset adjustment. A new private helper on `ListViewImpl` computes `max(0, visibleArea - ΣheightsAboveAndIncludingPinned)`. Two call sites add this to `effectiveInsets.top` via the existing `max(…)` chain alongside `stackFromBottomInsetItemFactor`. + +**Tech Stack:** Swift, ASDisplayKit, Bazel build system. + +**Scope:** Single file — `submodules/Display/Source/ListView.swift`. No protocol change (`pinToEdgeWithInset` is already declared on `ListViewItem`). No consumer changes. Because no item overrides `pinToEdgeWithInset` from its default `false`, the existing app surface's behavior is unchanged after this plan lands; the feature will be exercised only by a future consumer in a separate change. + +**No unit tests** exist in this project (per `CLAUDE.md`). Verification is via the full project build. + +--- + +## Task 1: Add `calculatePinToEdgeTopInset` helper and integrate at both call sites + +**Files:** +- Modify: `submodules/Display/Source/ListView.swift` + +The helper, both call-site edits, and the build verification land in one commit because they are tightly coupled: committing the helper without any call site is a no-op, and committing only one of the two call sites would cause `updateScroller` and `snapToBounds` to disagree about `effectiveInsets.top`, producing scroll-position desync whenever pinning is engaged. + +--- + +- [ ] **Step 1: Insert the `calculatePinToEdgeTopInset` helper after `calculateAdditionalTopInverseInset`** + +Use the Edit tool. The helper goes immediately after `calculateAdditionalTopInverseInset`'s closing brace (line 1090) and before `areAllItemsOnScreen` (line 1092). + +old_string: +```swift + private func calculateAdditionalTopInverseInset() -> CGFloat { + var additionalInverseTopInset: CGFloat = 0.0 + if !self.stackFromBottomInsetItemFactor.isZero { + var remainingFactor = self.stackFromBottomInsetItemFactor + for itemNode in self.itemNodes { + if remainingFactor.isLessThanOrEqualTo(0.0) { + break + } + + let itemFactor: CGFloat + if CGFloat(1.0).isLessThanOrEqualTo(remainingFactor) { + itemFactor = 1.0 + } else { + itemFactor = remainingFactor + } + + additionalInverseTopInset += floor(itemNode.apparentBounds.height * itemFactor) + + remainingFactor -= 1.0 + } + } + return additionalInverseTopInset + } + + private func areAllItemsOnScreen() -> Bool { +``` + +new_string: +```swift + private func calculateAdditionalTopInverseInset() -> CGFloat { + var additionalInverseTopInset: CGFloat = 0.0 + if !self.stackFromBottomInsetItemFactor.isZero { + var remainingFactor = self.stackFromBottomInsetItemFactor + for itemNode in self.itemNodes { + if remainingFactor.isLessThanOrEqualTo(0.0) { + break + } + + let itemFactor: CGFloat + if CGFloat(1.0).isLessThanOrEqualTo(remainingFactor) { + itemFactor = 1.0 + } else { + itemFactor = remainingFactor + } + + additionalInverseTopInset += floor(itemNode.apparentBounds.height * itemFactor) + + remainingFactor -= 1.0 + } + } + return additionalInverseTopInset + } + + private func calculatePinToEdgeTopInset() -> CGFloat { + var lowestPinnedIndex: Int = Int.max + for itemNode in self.itemNodes { + guard let index = itemNode.index else { continue } + if index < lowestPinnedIndex && self.items[index].pinToEdgeWithInset { + lowestPinnedIndex = index + } + } + guard lowestPinnedIndex != Int.max else { return 0.0 } + + var totalAboveAndPinned: CGFloat = 0.0 + var sawIndexZero = false + for itemNode in self.itemNodes { + guard let index = itemNode.index else { continue } + if index == 0 { + sawIndexZero = true + } + if index <= lowestPinnedIndex { + totalAboveAndPinned += itemNode.apparentBounds.height + } + } + guard sawIndexZero else { return 0.0 } + + let visibleArea = self.visibleSize.height - self.insets.top - self.insets.bottom + return max(0.0, visibleArea - totalAboveAndPinned) + } + + private func areAllItemsOnScreen() -> Bool { +``` + +- [ ] **Step 2: Integrate at the `snapToBounds` call site** + +Use the Edit tool. The block at lines 1181-1185 in `snapToBounds` gets a new `pinToEdgeTopInset` stanza after the existing `stackFromBottomInsetItemFactor` branch. Include the following line (` ` + `if topItemFound {`) in the old_string to disambiguate from the structurally-identical block in `areAllItemsOnScreen` at line 1110. + +old_string: +```swift + var effectiveInsets = self.insets + if topItemFound && !self.stackFromBottomInsetItemFactor.isZero { + let additionalInverseTopInset = self.calculateAdditionalTopInverseInset() + effectiveInsets.top = max(effectiveInsets.top, self.visibleSize.height - additionalInverseTopInset) + } + + if topItemFound { +``` + +new_string: +```swift + var effectiveInsets = self.insets + if topItemFound && !self.stackFromBottomInsetItemFactor.isZero { + let additionalInverseTopInset = self.calculateAdditionalTopInverseInset() + effectiveInsets.top = max(effectiveInsets.top, self.visibleSize.height - additionalInverseTopInset) + } + let pinToEdgeTopInset = self.calculatePinToEdgeTopInset() + if pinToEdgeTopInset > 0.0 { + effectiveInsets.top = max(effectiveInsets.top, self.insets.top + pinToEdgeTopInset) + } + + if topItemFound { +``` + +- [ ] **Step 3: Integrate at the `updateScroller` call site** + +Use the Edit tool. The block at lines 1612-1616 in `updateScroller` is nested one extra level (12-space indent rather than 8-space), so the string alone is unique and the old_string doesn't need extra context. + +old_string: +```swift + var effectiveInsets = self.insets + if topItemFound && !self.stackFromBottomInsetItemFactor.isZero { + let additionalInverseTopInset = self.calculateAdditionalTopInverseInset() + effectiveInsets.top = max(effectiveInsets.top, self.visibleSize.height - additionalInverseTopInset) + } + + completeHeight = effectiveInsets.top + effectiveInsets.bottom +``` + +new_string: +```swift + var effectiveInsets = self.insets + if topItemFound && !self.stackFromBottomInsetItemFactor.isZero { + let additionalInverseTopInset = self.calculateAdditionalTopInverseInset() + effectiveInsets.top = max(effectiveInsets.top, self.visibleSize.height - additionalInverseTopInset) + } + let pinToEdgeTopInset = self.calculatePinToEdgeTopInset() + if pinToEdgeTopInset > 0.0 { + effectiveInsets.top = max(effectiveInsets.top, self.insets.top + pinToEdgeTopInset) + } + + completeHeight = effectiveInsets.top + effectiveInsets.bottom +``` + +- [ ] **Step 4: Run the full project build** + +Use the Bash tool. The build takes several minutes; run it in the foreground so the agent waits for completion and surfaces failures immediately. The `source ~/.zshrc` prefix picks up `TELEGRAM_CODESIGNING_GIT_PASSWORD` per the build-environment quirk documented in `CLAUDE.md`. + +``` +source ~/.zshrc 2>/dev/null; PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber 1 --configuration debug_sim_arm64 +``` + +Expected: successful build. No warnings or errors touching `ListView.swift`. + +If the build fails: +- Swift syntax error → re-read `ListView.swift` around the edited regions; compare against the plan's old_string/new_string; fix and re-run. +- "`pinToEdgeWithInset` has no member" → the protocol property wasn't found; verify `submodules/Display/Source/ListViewItem.swift:80` still declares `var pinToEdgeWithInset: Bool { get }` and the default implementation at `ListViewItem.swift:102` is intact. If intact but the error persists, check that the `items` array's element type is `ListViewItem` (it is — see `public final var items: [ListViewItem]` in `ListView.swift`). +- Any other failure in unrelated files → not caused by this plan; investigate separately. + +- [ ] **Step 5: Commit** + +```bash +git add submodules/Display/Source/ListView.swift +git commit -m "$(cat <<'EOF' +Display/ListView: pin first pinToEdgeWithInset item to bottom edge + +Adds calculatePinToEdgeTopInset() and wires it into snapToBounds and +updateScroller. When the smallest-index item with pinToEdgeWithInset=true +plus all items above it have a combined apparentBounds height less than +the available scrolling area, the helper returns a positive top-inset +contribution that pushes the pinned item's maxY to visibleSize.height - +insets.bottom. Once items above reach the available area, the +contribution is zero and scrolling is fully ordinary. + +Spec: docs/superpowers/specs/2026-04-17-listview-pin-to-edge-design.md + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +Verify with `git status` that the tree is clean after the commit. + +--- + +## Rationale for task granularity + +This plan has a single task. I considered splitting "add helper" from "apply at two call sites" into two commits: + +- **For splitting:** one commit per "unit of change" is more bisectable. +- **Against splitting:** the helper alone is unused (runtime no-op, and Swift does not warn on unused private methods). Applying at one call site without the other would produce a live bug — `snapToBounds` and `updateScroller` would disagree whenever pinning engages, and `updateScroller` is what sets `scroller.contentSize`/`contentOffset`. Three commits land an internally-consistent state only at the third commit. + +Bundling all edits preserves bisectability at the feature-level boundary (the commit either introduces pin-to-edge support or it doesn't) and keeps the repo free of intermediate broken states. diff --git a/docs/superpowers/plans/2026-04-17-mediaresource-to-enginemediaresource-wave-2.md b/docs/superpowers/plans/2026-04-17-mediaresource-to-enginemediaresource-wave-2.md new file mode 100644 index 0000000000..94b5f43479 --- /dev/null +++ b/docs/superpowers/plans/2026-04-17-mediaresource-to-enginemediaresource-wave-2.md @@ -0,0 +1,880 @@ +# MediaResource → EngineMediaResource Refactor (Wave 2) — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Drive raw `MediaResource` (Postbox protocol) out of the `TelegramEngine` public facade by changing facade-function signatures in-place to take/return `EngineMediaResource`, bridging to the existing `_internal_*` Postbox-facing implementations via wrap/unwrap helpers. In the same commit as each facade change, update every call site. Follow up with a first small batch of consumer type-reference migrations. + +**Architecture:** `TelegramEngine` facade methods live alongside `_internal_*` Postbox-using implementations in `submodules/TelegramCore/Sources/TelegramEngine//`. Today the facade methods already bridge (storing an `Account` and delegating), but their public signatures still expose raw `MediaResource`. The fix: change facade signatures to `EngineMediaResource` (including the `mapResourceToAvatarSizes` closure types) and add the two-line wrap/unwrap bridging. `_internal_*` functions stay on raw `MediaResource` — they are the Postbox-facing layer and must remain so. Consumer call sites swap `MediaResource` → `EngineMediaResource` (usually via `EngineMediaResource(raw)` wrap or `engineResource._asResource()` unwrap at a nearby boundary). + +**Tech Stack:** Swift, Bazel, Postbox (opaque storage), TelegramCore (public facade), SSignalKit. + +**Design constraint (IMPORTANT):** `TelegramCore` is shared with the Telegram-Mac codebase and must **not** import UIKit/Display. Any UIKit-requiring logic (image scaling, `UIImage`, `generateScaledImage`, etc.) stays in consumer-side submodules. Engine API additions must not pull in UIKit. + +**Why not overloads:** An earlier iteration of this plan added opt-in `EngineMediaResource` overloads and kept the raw overloads. That was rejected: duplicate signatures fragment the public API and leave raw-`MediaResource` leaks forever. The correct pattern is to change the single facade function in-place so it takes engine types and bridges inside, forcing callers to migrate in the same commit. + +--- + +## Background the executor needs + +### The full build command + +Run from the repo root (`/Users/ali/build/telegram/telegram-ios`): + +```bash +source ~/.zshrc 2>/dev/null; \ +PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH \ + python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development \ + --gitCodesigningUseCurrent \ + --buildNumber 1 \ + --configuration debug_sim_arm64 +``` + +The build is the only verification (no unit tests per `CLAUDE.md`). Every task ends with a full build that must go green before the next task starts. + +### What `EngineMediaResource` gives you today (bridge primitives) + +Defined in [TelegramEngineResources.swift](../../../submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift): + +```swift +public final class EngineMediaResource: Equatable { + public init(_ resource: MediaResource) + public func _asResource() -> MediaResource + public var id: Id + public struct Id: Equatable, Hashable { + public init(_ id: MediaResourceId) + public init(_ stringRepresentation: String) + } + public final class ResourceData { + public let path: String; public let availableSize: Int64; public let isComplete: Bool + } + public enum FetchStatus: Equatable { /* Remote/Local/Fetching/Paused */ } +} +public extension EngineMediaResource.ResourceData { + convenience init(_ data: MediaResourceData) +} +``` + +### The bridging pattern + +For each facade function whose public signature contains `MediaResource`: + +**Before** (raw-protocol leak): + +```swift +public func uploadedPeerPhoto(resource: MediaResource) -> Signal { + return _internal_uploadedPeerPhoto(postbox: self.account.postbox, network: self.account.network, resource: resource) +} +``` + +**After** (engine-typed facade, internal bridge): + +```swift +public func uploadedPeerPhoto(resource: EngineMediaResource) -> Signal { + return _internal_uploadedPeerPhoto(postbox: self.account.postbox, network: self.account.network, resource: resource._asResource()) +} +``` + +For closures that receive a `MediaResource`: + +**Before:** + +```swift +public func updatePeerPhoto(..., mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> ... { + return _internal_updatePeerPhoto(..., mapResourceToAvatarSizes: mapResourceToAvatarSizes) +} +``` + +**After:** + +```swift +public func updatePeerPhoto(..., mapResourceToAvatarSizes: @escaping (EngineMediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> ... { + return _internal_updatePeerPhoto(..., mapResourceToAvatarSizes: { rawResource, representations in + mapResourceToAvatarSizes(EngineMediaResource(rawResource), representations) + }) +} +``` + +`_internal_*` functions are **not** changed — they stay on raw `MediaResource` as the Postbox-facing layer. + +### Call-site migration pattern + +At each call site, the change is mechanical: + +- `engine.peers.uploadedPeerPhoto(resource: someRawResource)` → `engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(someRawResource))`. +- `engine.peers.updatePeerPhoto(..., mapResourceToAvatarSizes: { resource, representations in ... resource ... })` — the closure's `resource` is now `EngineMediaResource`. Any expression inside the closure that previously treated `resource` as raw protocol (e.g. `postbox.mediaBox.resourceData(resource)`) must use `resource._asResource()`. + +Where the consumer was carrying a `MediaResource?` property / local purely as a pipe into one of these APIs, migrate the property itself to `EngineMediaResource?` so no unwrap/wrap churn is needed. + +### Static-check commands + +```bash +grep -R "^import Postbox" submodules//Sources # expect: empty (only when a module is being fully de-Postboxed) +grep "submodules/Postbox" submodules//BUILD # expect: empty (same condition) +``` + +### Commit convention + +- One commit per engine API family: `TelegramCore: migrate to EngineMediaResource` — bundles facade-signature change **and** all call sites updated in the same commit. The repo must build on every commit. +- Consumer-only type-ref commits: `: migrate MediaResource property to EngineMediaResource` or `: drop direct Postbox dependency`. +- Always use HEREDOC bodies. No `--amend`. + +### What is explicitly out of scope + +- Classes that **conform to `TelegramMediaResource`** (must implement `isEqual(to: MediaResource)`): remain `import Postbox`. Enumerated: + - `submodules/ICloudResources/Sources/ICloudResources.swift` — `ICloudFileResource` + - `submodules/InstantPageUI/Sources/InstantPageExternalMediaResource.swift` — `InstantPageExternalMediaResource` + - `submodules/LocalMediaResources/Sources/LocalMediaResources.swift` — `VideoLibraryMediaResource` + - `submodules/TelegramUniversalVideoContent/Sources/YoutubeEmbedImplementation.swift` — `YoutubeEmbedStoryboardMediaResource` +- TelegramCore-internal `MediaResource` usage (SyncCore, Fetch, `_internal_*` functions, etc.) — Postbox-facing layer. +- Modules already abandoned in wave 1 for non-MediaResource reasons (`FetchManagerImpl` / `ICloudResources` have other umbrella-type blockers). +- The heavy-leak modules in the "Future waves" table at the bottom (`PassportUI`, `TelegramUI`, etc.). +- Importing UIKit/Display into TelegramCore under any circumstance. + +--- + +## Task 0: Baseline verification + +**Files:** No code changes. + +- [ ] **Step 1: Confirm tree state** + +```bash +git status +git log --oneline -5 +``` + +Expected: working tree clean apart from pre-existing untracked (`build-system/tulsi/`, `submodules/TgVoip/`, `third-party/libx264/`) and submodule-content drift on `build-system/bazel-rules/sourcekit-bazel-bsp`. HEAD on `master`. + +- [ ] **Step 2: Baseline build** + +Run the full build command above. Expected: PASS. + +If it fails, stop — a non-green baseline is out of scope. + +- [ ] **Step 3: No commit.** + +--- + +## Task 1: Record the new rules in CLAUDE.md + +**Files:** +- Modify: `CLAUDE.md` + +- [ ] **Step 1: Add the "TelegramCore no UIKit" rule** + +In `CLAUDE.md`, inside the `## Postbox → TelegramEngine refactor (in progress)` section, under `### Rules that apply to every wave`, append a new numbered rule after the existing rule 6: + +```markdown +7. **TelegramCore never imports UIKit/Display.** `TelegramCore` is shared with the Telegram-Mac codebase; its Bazel `deps` and source files must not reference UIKit, Display, or any Apple-UI framework. UIKit-needing helpers (image scaling, rendering, etc.) stay in consumer-side submodules. +``` + +- [ ] **Step 2: Add the MediaResource → EngineMediaResource migration pattern** + +After the `### Engine typealias cheat sheet (existing aliases)` block (which ends with the `MediaResource` / `TelegramMediaResource` note), insert a new section: + +```markdown +### MediaResource → EngineMediaResource consumer migration + +`EngineMediaResource` is a `final class` in `TelegramCore` wrapping a `MediaResource` value. Unlike the typealiases above it is **not** interchangeable with the protocol, but it does provide wrap/unwrap helpers: + +- `EngineMediaResource(rawResource)` — wrap a raw `MediaResource`. +- `engineResource._asResource()` — unwrap to the raw `MediaResource`. +- `EngineMediaResource.ResourceData(rawResourceData)` — wrap `MediaResourceData`. +- `EngineMediaResource.Id(rawMediaResourceId)` — wrap `MediaResourceId`. + +**Pattern for facade functions:** when a `TelegramEngine.` method leaks raw `MediaResource` in its public signature, **change the facade signature in place** to `EngineMediaResource` (and change any closure parameter types the same way). Bridge inside the facade body by calling the existing `_internal_*` function with `engineResource._asResource()` / wrapping raw inputs from inner closures with `EngineMediaResource(rawResource)`. Update all call sites in the same commit. The `_internal_*` function stays on raw `MediaResource` — it is the Postbox-facing layer. + +Do **not** add opt-in `EngineMediaResource` overloads alongside raw-`MediaResource` overloads. Duplicate signatures fragment the public API and leave the leak in place forever. + +For consumer modules, prefer `EngineMediaResource` as the type in properties, locals, generic arguments and function parameters when the usage is a pure type reference. Do **not** try to use `EngineMediaResource` where a class must conform to `TelegramMediaResource` (Postbox protocol) or override `isEqual(to: MediaResource)` — those remain `import Postbox`. +``` + +- [ ] **Step 3: Full build (sanity — docs only)** + +Run the full build. Expected: PASS. + +- [ ] **Step 4: Commit** + +```bash +git add CLAUDE.md +git commit -m "$(cat <<'EOF' +CLAUDE.md: record TelegramCore-no-UIKit rule and EngineMediaResource migration pattern + +Wave-2 preparation. Codifies that TelegramCore is shared with +Telegram-Mac and must stay UIKit-free, and documents the +modify-in-place / bridge-inside pattern for migrating +MediaResource-leaking facade functions to EngineMediaResource. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 2: Migrate `TelegramEngine.Peers` photo APIs to `EngineMediaResource` + +**Files:** +- Modify: `submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift` +- Modify: all call sites (13 + 11 + 7, with heavy overlap — see Step 2 grep). + +**Functions migrated in this task:** +- `uploadedPeerPhoto(resource:)` (line 704) — `MediaResource` → `EngineMediaResource` +- `uploadedPeerVideo(resource:)` (line 708) — `MediaResource` → `EngineMediaResource` +- `updatePeerPhoto(..., mapResourceToAvatarSizes:)` (line 712) — closure parameter `MediaResource` → `EngineMediaResource` + +- [ ] **Step 1: Read the current signatures** + +Read lines 704–720 of `submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift`. Confirm the three functions match the pattern `return _internal_(postbox: self.account.postbox, ..., resource: resource)` or equivalent. + +- [ ] **Step 2: Enumerate call sites** + +```bash +grep -rnE "\\.(uploadedPeerPhoto|uploadedPeerVideo|updatePeerPhoto)\(" submodules/ \ + | grep -v "submodules/TelegramCore" +``` + +Capture every hit — file path, line number, approximate surrounding context (what resource expression is passed in / what the closure body does). The distribution as of planning: + +- `uploadedPeerPhoto`: 11 call sites (spread across TelegramUI, TelegramCallsUI, AuthorizationUI, etc.) +- `uploadedPeerVideo`: 7 +- `updatePeerPhoto`: 13 + +Many call sites chain these (e.g. `updatePeerPhoto(photo: engine.peers.uploadedPeerPhoto(resource: ...))`) so a single file often touches two or three of them in one call. + +- [ ] **Step 3: Change the facade signatures + bridge** + +In `TelegramEnginePeers.swift`, change the three functions to: + +```swift +public func uploadedPeerPhoto(resource: EngineMediaResource) -> Signal { + return _internal_uploadedPeerPhoto(postbox: self.account.postbox, network: self.account.network, resource: resource._asResource()) +} + +public func uploadedPeerVideo(resource: EngineMediaResource) -> Signal { + return _internal_uploadedPeerVideo(postbox: self.account.postbox, network: self.account.network, messageMediaPreuploadManager: self.account.messageMediaPreuploadManager, resource: resource._asResource()) +} + +public func updatePeerPhoto(peerId: PeerId, photo: Signal?, video: Signal? = nil, videoStartTimestamp: Double? = nil, markup: UploadPeerPhotoMarkup? = nil, mapResourceToAvatarSizes: @escaping (EngineMediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updatePeerPhoto(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, accountPeerId: self.account.peerId, peerId: peerId, photo: photo, video: video, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { rawResource, representations in + return mapResourceToAvatarSizes(EngineMediaResource(rawResource), representations) + }) +} +``` + +**Before editing, re-read the existing bodies** — the exact arg names passed into `_internal_updatePeerPhoto` etc. must match what's already there (the skeletons above reproduce what's in the file at planning time, but the executor should preserve every argument the current implementation passes). Only the outer signature and the closure-wrapping change. + +- [ ] **Step 4: Update every call site** (same commit) + +For each hit from Step 2, rewrite the call site per the patterns: + +**Pattern A — passing a raw resource to `uploadedPeerPhoto` / `uploadedPeerVideo`:** + +```swift +// Before: +engine.peers.uploadedPeerPhoto(resource: someRawResource) +// After: +engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(someRawResource)) +``` + +**Pattern B — the `mapResourceToAvatarSizes` closure of `updatePeerPhoto`:** + +```swift +// Before: +mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) +} +// After (if the helper is still raw-MediaResource-facing at this point): +mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource._asResource(), representations: representations) +} +``` + +Task 6 will change `mapResourceToAvatarSizes` itself to accept `EngineMediaResource` and drop the `_asResource()` call. Until Task 6 lands, keep the `_asResource()` here. This keeps the build green between tasks. + +**Pattern C — the consumer was already carrying the resource as a `MediaResource?` local purely as a pipe:** + +If a nearby local/property typed `MediaResource?` only exists to feed `uploadedPeerPhoto(resource:)` or similar, change the local's type to `EngineMediaResource?` at the same time. This avoids wrap/unwrap churn at the call site. + +- [ ] **Step 5: Full build** + +Run the full build. Expected: PASS. + +If it fails, the first error locates the broken call site. Apply Pattern A / B / C at that site and rebuild. If a file imports Postbox only for `MediaResource` and now has no other Postbox identifier, you may optionally remove `import Postbox` in the same commit — but that is not required here; it is a separate goal. + +- [ ] **Step 6: Commit** + +```bash +git add submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift submodules/ +git commit -m "$(cat <<'EOF' +TelegramCore: migrate peer-photo facade to EngineMediaResource + +Change TelegramEngine.Peers.uploadedPeerPhoto / uploadedPeerVideo / +updatePeerPhoto so their public signatures take EngineMediaResource +instead of raw MediaResource (and the mapResourceToAvatarSizes closure +receives EngineMediaResource). The facade bridges to the existing +_internal_* Postbox-facing implementations via _asResource() / +EngineMediaResource(_:). All call sites updated in this commit. + +Part of the MediaResource -> EngineMediaResource migration (wave 2). +No behavior change. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 3: Migrate `TelegramEngine.AccountData.updateAccountPhoto` and `updateFallbackPhoto` + +**Files:** +- Modify: `submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift` +- Modify: all call sites (5 + 4). + +- [ ] **Step 1: Read the current signatures** + +Read lines 55–90 of `submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift`. Confirm both functions match the expected pattern. + +- [ ] **Step 2: Enumerate call sites** + +```bash +grep -rnE "\\.(updateAccountPhoto|updateFallbackPhoto)\(" submodules/ \ + | grep -v "submodules/TelegramCore" +``` + +- [ ] **Step 3: Change the facade signatures + bridge** + +Change both functions in place: + +```swift +public func updateAccountPhoto(resource: EngineMediaResource?, videoResource: EngineMediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mapResourceToAvatarSizes: @escaping (EngineMediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updateAccountPhoto(account: self.account, resource: resource?._asResource(), videoResource: videoResource?._asResource(), videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { rawResource, representations in + return mapResourceToAvatarSizes(EngineMediaResource(rawResource), representations) + }) +} + +public func updateFallbackPhoto(resource: EngineMediaResource?, videoResource: EngineMediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mapResourceToAvatarSizes: @escaping (EngineMediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updateFallbackPhoto(account: self.account, resource: resource?._asResource(), videoResource: videoResource?._asResource(), videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { rawResource, representations in + return mapResourceToAvatarSizes(EngineMediaResource(rawResource), representations) + }) +} +``` + +**Before editing, verify the exact argument names passed to `_internal_updateAccountPhoto` / `_internal_updateFallbackPhoto`** in the current file. Copy those argument spellings verbatim (only the outer signature and inner closure wrapping change). + +- [ ] **Step 4: Update every call site** (same commit) + +Apply Pattern A/B/C from Task 2 to every hit. Wrap `EngineMediaResource(...)` around raw-resource args; add `._asResource()` inside any `mapResourceToAvatarSizes:` closure body where it hands the value onward to a still-raw helper (removed in Task 6). + +- [ ] **Step 5: Full build** + +Run the full build. Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift submodules/ +git commit -m "$(cat <<'EOF' +TelegramCore: migrate account-photo facade to EngineMediaResource + +Change TelegramEngine.AccountData.updateAccountPhoto and +updateFallbackPhoto so their public signatures take EngineMediaResource +(and the mapResourceToAvatarSizes closure receives +EngineMediaResource). Bridges to _internal_* functions via +_asResource()/EngineMediaResource(_:). All call sites updated in this +commit. + +Part of the MediaResource -> EngineMediaResource migration (wave 2). +No behavior change. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 4: Migrate `TelegramEngine.Contacts.updateContactPhoto` + +**Files:** +- Modify: `submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift` +- Modify: all call sites (8). + +- [ ] **Step 1: Read the current signature** + +Read around line 33 of `submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift`. + +- [ ] **Step 2: Enumerate call sites** + +```bash +grep -rn "\.updateContactPhoto(" submodules/ | grep -v "submodules/TelegramCore" +``` + +- [ ] **Step 3: Change the facade signature + bridge** + +```swift +public func updateContactPhoto(peerId: PeerId, resource: EngineMediaResource?, videoResource: EngineMediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mode: SetCustomPeerPhotoMode, mapResourceToAvatarSizes: @escaping (EngineMediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updateContactPhoto(account: self.account, peerId: peerId, resource: resource?._asResource(), videoResource: videoResource?._asResource(), videoStartTimestamp: videoStartTimestamp, markup: markup, mode: mode, mapResourceToAvatarSizes: { rawResource, representations in + return mapResourceToAvatarSizes(EngineMediaResource(rawResource), representations) + }) +} +``` + +Verify the `_internal_updateContactPhoto` call spelling against the existing file before committing. + +- [ ] **Step 4: Update every call site** (same commit) + +Pattern A/B/C as in Task 2. + +- [ ] **Step 5: Full build** + +Run the full build. Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift submodules/ +git commit -m "$(cat <<'EOF' +TelegramCore: migrate updateContactPhoto facade to EngineMediaResource + +Change TelegramEngine.Contacts.updateContactPhoto so its public +signature takes EngineMediaResource parameters and the +mapResourceToAvatarSizes closure receives EngineMediaResource. Bridges +to _internal_updateContactPhoto via _asResource()/EngineMediaResource(_:). +All call sites updated in this commit. + +Part of the MediaResource -> EngineMediaResource migration (wave 2). +No behavior change. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 5: Migrate `TelegramEngine.Auth.uploadedPeerVideo` + +**Files:** +- Modify: `submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift` +- Modify: call sites that route through `TelegramEngine.Auth.uploadedPeerVideo` (separate from `TelegramEngine.Peers.uploadedPeerVideo`). + +- [ ] **Step 1: Read the current signature** + +Read around line 51 of `submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift`. + +- [ ] **Step 2: Enumerate call sites** + +```bash +grep -rn "engine\.auth\.uploadedPeerVideo\|\.auth\.uploadedPeerVideo" submodules/ | grep -v "submodules/TelegramCore" +``` + +The call site count is small (the sign-up flow). If zero, skip Step 4. + +- [ ] **Step 3: Change the facade signature + bridge** + +```swift +public func uploadedPeerVideo(resource: EngineMediaResource) -> Signal { + return _internal_uploadedPeerVideo(postbox: self.account.postbox, network: self.account.network, messageMediaPreuploadManager: self.account.messageMediaPreuploadManager, resource: resource._asResource()) +} +``` + +Preserve the exact argument spellings from the existing function body. + +- [ ] **Step 4: Update call sites** (same commit) + +Pattern A. + +- [ ] **Step 5: Full build** + +Run the full build. Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift submodules/ +git commit -m "$(cat <<'EOF' +TelegramCore: migrate Auth.uploadedPeerVideo facade to EngineMediaResource + +Signature change + call sites. + +Part of the MediaResource -> EngineMediaResource migration (wave 2). +No behavior change. + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 6: Migrate `mapResourceToAvatarSizes` utility and drop `import Postbox` from `MapResourceToAvatarSizes` + +**Files:** +- Modify: `submodules/MapResourceToAvatarSizes/Sources/MapResourceToAvatarSizes.swift` +- Modify: `submodules/MapResourceToAvatarSizes/BUILD` +- Modify: all 27 call sites of the old `mapResourceToAvatarSizes(postbox:resource:representations:)`. + +**Preconditions:** Tasks 2–5 have landed, so every `mapResourceToAvatarSizes:` closure at call sites now receives an `EngineMediaResource` (because the facade closures were retyped). At this point the inner `mapResourceToAvatarSizes(postbox: …, resource: …._asResource(), …)` unwrap becomes avoidable. + +- [ ] **Step 1: Read the current file** + +``` +submodules/MapResourceToAvatarSizes/Sources/MapResourceToAvatarSizes.swift +``` + +Confirm the function body uses `postbox.mediaBox.resourceData(resource)` and requires `UIImage` / `generateScaledImage` / `jpegData(compressionQuality:)`. + +- [ ] **Step 2: Enumerate call sites** + +```bash +grep -rn "mapResourceToAvatarSizes(postbox:" submodules/ | grep -v "submodules/MapResourceToAvatarSizes" +``` + +Expected: 27 call sites, concentrated in `submodules/TelegramUI/...PeerInfoScreenAvatarSetup.swift` (19), `TelegramCallsUI/...VideoChatScreenParticipantContextMenu.swift` (5), and three other TelegramUI files (1 each). + +- [ ] **Step 3: Rewrite the function to use `EngineMediaResource` + `TelegramEngine.Resources.data`** + +Replace the body of `submodules/MapResourceToAvatarSizes/Sources/MapResourceToAvatarSizes.swift` with: + +```swift +import Foundation +import UIKit +import SwiftSignalKit +import TelegramCore +import Display + +public func mapResourceToAvatarSizes(engine: TelegramEngine, resource: EngineMediaResource, representations: [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError> { + return engine.resources.data(id: resource.id) + |> take(1) + |> map { data -> [Int: Data] in + guard data.isComplete, let image = UIImage(contentsOfFile: data.path) else { + return [:] + } + var result: [Int: Data] = [:] + for i in 0 ..< representations.count { + let size: CGSize + if representations[i].dimensions.width == 80 { + size = CGSize(width: 160.0, height: 160.0) + } else { + size = representations[i].dimensions.cgSize + } + if let scaledImage = generateScaledImage(image: image, size: size, scale: 1.0), let scaledData = scaledImage.jpegData(compressionQuality: 0.8) { + result[i] = scaledData + } + } + return result + } +} +``` + +Notes: +- Signature: `(engine: TelegramEngine, resource: EngineMediaResource, representations: [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>`. +- `import Postbox` is gone; replaced usage with `engine.resources.data(id:)` which returns `Signal`. +- `data.complete` → `data.isComplete` (field rename on the engine wrapper). + +- [ ] **Step 4: Drop the Bazel dep** + +Edit `submodules/MapResourceToAvatarSizes/BUILD` and remove `"//submodules/Postbox:Postbox",` from `deps`. Leave the rest untouched. + +- [ ] **Step 5: Update every call site** (same commit) + +At each of the 27 sites, two changes: + +**Pattern D — the call site already lives inside a `mapResourceToAvatarSizes:` closure on a facade function (post-Task-2/3/4, the closure's `resource` parameter is now `EngineMediaResource`):** + +```swift +// Before (from an intermediate state between tasks): +mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: postbox, resource: resource._asResource(), representations: representations) +} +// After: +mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: engine, resource: resource, representations: representations) +} +``` + +The `engine` value is always reachable at the call site — it's either a stored reference used right above the closure or `context.engine` / `accountContext.engine`. Grep shows every current call site has a `postbox = context.account.postbox` (or similar) just above, so `context.engine` / the adjacent engine reference is in scope. + +**Pattern E — direct (non-closure) call with a raw `MediaResource` in scope:** + +Rare in the current code, but if you find one, wrap with `EngineMediaResource(rawResource)` at the call. + +- [ ] **Step 6: Static checks** + +```bash +grep -R "^import Postbox" submodules/MapResourceToAvatarSizes/Sources # expect: empty +grep "submodules/Postbox" submodules/MapResourceToAvatarSizes/BUILD # expect: empty +``` + +- [ ] **Step 7: Full build** + +Run the full build. Expected: PASS. + +Likely failure modes: +- A call site's surrounding scope doesn't have an `engine` in context. Fix: use `.engine` or promote `engine` to a nearby `let`. +- A consumer file passed a non-`EngineMediaResource` into the closure because it wasn't updated by Task 2/3/4. Fix forward (update it now) and record the miss. + +- [ ] **Step 8: Commit** + +```bash +git add submodules/MapResourceToAvatarSizes/ submodules/TelegramUI/ submodules/TelegramCallsUI/ +git commit -m "$(cat <<'EOF' +MapResourceToAvatarSizes: migrate to EngineMediaResource and drop Postbox + +Change the signature of mapResourceToAvatarSizes from +(postbox: Postbox, resource: MediaResource, ...) to +(engine: TelegramEngine, resource: EngineMediaResource, ...), using +engine.resources.data(id:) internally. All 27 call sites updated in +this commit. `import Postbox` and the Bazel dep are removed. +Behavior-preserving. + +Part of the MediaResource -> EngineMediaResource migration (wave 2). + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 7: Migrate `AuthorizationUI` signal type + +**Files:** +- Modify: `submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift` + +**Starting inventory:** exactly one reference — `Signal` at line 1162. AuthorizationUI has six files importing Postbox overall; dropping `import Postbox` from the module as a whole is **not** in scope for this task. + +- [ ] **Step 1: Read line 1162 ± 20** + +Understand: +- What value is put into the signal? Likely some TelegramMediaResource subclass (e.g. `LocalFileMediaResource`). +- Who consumes the signal downstream? After Tasks 2–5, any facade that ultimately receives this signal's value (via `updateAccountPhoto`, `uploadedPeerVideo`, etc.) expects `EngineMediaResource`. + +- [ ] **Step 2: Change the signal type** + +```swift +// Before: +avatarVideo = Signal { subscriber in + // ... produces a TelegramMediaResource ... + subscriber.putNext(someResource) +} +// After: +avatarVideo = Signal { subscriber in + // ... produces a TelegramMediaResource ... + subscriber.putNext(someResource.flatMap { EngineMediaResource($0) }) // or wrap the non-optional path +} +``` + +The exact wrapping site depends on where the raw resource flows in. The grep + read from Step 1 tells you. + +Downstream, any call site that consumed the raw resource and handed it to an engine facade now has an `EngineMediaResource?` which it can pass directly (post-Tasks 2–5). + +- [ ] **Step 3: Full build** + +Run the full build. Expected: PASS. + +If the downstream expected a `TelegramMediaResource?` (e.g. for direct Postbox access that wasn't part of Tasks 2–5), revert this task as `Abandoned — downstream expects raw protocol` with a recorded reason. + +- [ ] **Step 4: Commit** + +```bash +git add submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +git commit -m "$(cat <<'EOF' +AuthorizationUI: migrate avatar-video signal type to EngineMediaResource + +Single type-reference swap. Downstream engine facades already accept +EngineMediaResource after the Phase-1 migrations. Behavior-preserving. + +Part of the MediaResource -> EngineMediaResource migration (wave 2). + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 8: Migrate `SaveToCameraRoll` property type — **ABANDONED** + +**Status:** Abandoned in wave 2. No code changes from this task. + +**Reason:** The planning-time grep that produced the "one reference" inventory only matched `MediaResource`/`TelegramMediaResource` tokens, not the broader set of Postbox usages. Re-inventorying the module at execution time (`grep -nE "\b(postbox|mediaBox|MediaResource)\b|^import Postbox" submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift`) shows three public functions with `postbox: Postbox` in their signatures (`fetchMediaData`, `saveToCameraRoll`, `copyToPasteboard`) plus multiple `postbox.mediaBox.*` calls in their bodies. Per spec rule 2, `Postbox` is an umbrella type that cannot be typealiased, so those public-API signatures cannot be de-Postboxed without editing every caller; and the internal `postbox.mediaBox.*` calls require engine-side wrappers (closer to Task 6's shape) rather than a simple type swap. Scope is a full module-migration wave, not a single type swap — parked for a future wave. + +**Original task body (retained for audit trail, do not implement):** + +**Files:** +- Modify: `submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift` +- Possibly modify: `submodules/SaveToCameraRoll/BUILD` + +**Starting inventory:** one reference — `var resource: MediaResource?` at line 19. + +- [ ] **Step 1: Read + full grep** + +```bash +grep -nE "\b(MediaResource|TelegramMediaResource|postbox|mediaBox|transaction|PostboxView|combinedView)\b|^import Postbox" submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift +``` + +Capture every hit. + +- [ ] **Step 2: Abandon-check** + +If the grep shows Postbox usages other than the single `MediaResource?` property and an `import Postbox` line, abandon this task with a recorded reason. Do not substitute. + +If it shows only the property + import, proceed. + +- [ ] **Step 3: Swap the property type + boundary wrap/unwrap** + +Change `var resource: MediaResource?` to `var resource: EngineMediaResource?`. At each assignment/use: + +- Assignment from a raw resource: `self.resource = EngineMediaResource(rawResource)`; `self.resource = nil` unchanged. +- Read that hands to mediaBox/postbox (if any remains): `self.resource?._asResource()`. + +- [ ] **Step 4: Drop `import Postbox` if now unused** + +If Step 1 showed `import Postbox` as the only remaining Postbox reference: + +- Remove the `import Postbox` line. +- Remove `"//submodules/Postbox:Postbox",` from `submodules/SaveToCameraRoll/BUILD`. + +Static checks: + +```bash +grep -R "^import Postbox" submodules/SaveToCameraRoll/Sources # expect: empty +grep "submodules/Postbox" submodules/SaveToCameraRoll/BUILD # expect: empty +``` + +Else skip this step. + +- [ ] **Step 5: Full build** + +Run the full build. Expected: PASS. + +- [ ] **Step 6: Commit** + +If the import was removed: + +```bash +git add submodules/SaveToCameraRoll/ +git commit -m "$(cat <<'EOF' +SaveToCameraRoll: migrate resource property to EngineMediaResource and drop Postbox + +Swaps the single MediaResource? property for EngineMediaResource?, +wrapping/unwrapping at boundaries. With the only Postbox reference +gone, removes `import Postbox` and the Bazel dep. +Behavior-preserving. + +Part of the MediaResource -> EngineMediaResource migration (wave 2). + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +If the import was kept: + +```bash +git add submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift +git commit -m "$(cat <<'EOF' +SaveToCameraRoll: migrate resource property to EngineMediaResource + +Swaps the single MediaResource? property for EngineMediaResource?, +wrapping/unwrapping at boundaries. import Postbox remains because +other identifiers still need it. Behavior-preserving. + +Part of the MediaResource -> EngineMediaResource migration (wave 2). + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Task 9: Wave-2 completion verification + +**Files:** No code changes. + +- [ ] **Step 1: Commit-log check** + +```bash +git log --oneline master..HEAD # or whatever branch this was executed on +``` + +Expected commits (some may be absent if tasks abandoned): + +- `CLAUDE.md: record TelegramCore-no-UIKit rule and EngineMediaResource migration pattern` +- `TelegramCore: migrate peer-photo facade to EngineMediaResource` +- `TelegramCore: migrate account-photo facade to EngineMediaResource` +- `TelegramCore: migrate updateContactPhoto facade to EngineMediaResource` +- `TelegramCore: migrate Auth.uploadedPeerVideo facade to EngineMediaResource` +- `MapResourceToAvatarSizes: migrate to EngineMediaResource and drop Postbox` +- `AuthorizationUI: migrate avatar-video signal type to EngineMediaResource` +- `SaveToCameraRoll: migrate resource property to EngineMediaResource[...]` + +- [ ] **Step 2: Public-API leak check** + +```bash +grep -nE "^\s*public func .*: MediaResource|public func .*MediaResource, \[" \ + submodules/TelegramCore/Sources/TelegramEngine/ +``` + +Expected: no matches in the facade files touched by Tasks 2–5 (`TelegramEngine/Peers/TelegramEnginePeers.swift`, `TelegramEngine/AccountData/TelegramEngineAccountData.swift`, `TelegramEngine/Contacts/TelegramEngineContacts.swift`, `TelegramEngine/Auth/TelegramEngineAuth.swift`). Other TelegramEngine files may still leak `MediaResource` — those are for future waves. + +- [ ] **Step 3: Final full build from clean state** + +Run the full build. Expected: PASS (cached, fast). + +- [ ] **Step 4: No commit.** Verification only. + +--- + +## Future waves (not in this plan) + +Ranked consumer modules by MediaResource/TelegramMediaResource reference count (from `grep -rE "\\b(MediaResource|TelegramMediaResource)\\b"` over `submodules//Sources/`, excluding TelegramCore/Postbox). Classifications are preliminary and must be re-audited at the start of each future wave. + +| Refs | Module | Future-wave notes | +| --- | --- | --- | +| 2 | ChatPresentationInterfaceState | Public struct field `resource: TelegramMediaResource` — needs caller audit. | +| 2 | ItemListStickerPackItem | Enum case leaks `MediaResource` — needs caller audit. | +| 2 | TelegramCallsUI | Signal locals; mostly type-refs. | +| 3 | LegacyMediaPickerUI | `thumbnailResource: TelegramMediaResource?` internal properties — likely safe. | +| 3 | ReactionSelectionNode | `customEffectResource: MediaResource?` in public func — caller audit. | +| 3 | TelegramAnimatedStickerNode | `public init(postbox: Postbox, resource: MediaResource, …)` + `public convenience init(account: Account, …)` — umbrella-type leaks; needs a paired wave. | +| 4 | GalleryUI | `private func setupStatus(resource: MediaResource)` — internal, 4 files. | +| 5 | StickerResources | Multiple public funcs take `postbox: Postbox, resource: MediaResource` / `mediaBox: MediaBox`. | +| 6 | PhotoResources | Similar to StickerResources; also `securePhoto(account: Account, resource: TelegramMediaResource, …)`. | +| 7 | MediaPlayer | `mediaBox: MediaBox, resource: MediaResource` in public init — umbrella leaks. | +| 7 | WebSearchUI | `thumbnailResource: TelegramMediaResource?` in multiple structs/inits. | +| 8 | AccountContext | Protocol surface — audit carefully. | +| 8 | SoftwareVideo | Public init takes `mediaBox: MediaBox` + `resource: MediaResource`. | +| 12 | LocalMediaResources | Contains `VideoLibraryMediaResource: TelegramMediaResource` — blocked for conformance. | +| 14 | LegacyDataImport | Legacy path; audit scope. | +| 25 | PassportUI | Large surface; break into multiple tasks. | +| 36 | TelegramUI | Umbrella module; never as one wave. | + +**Blocked-by-conformance modules, explicitly out of all waves:** + +- `submodules/ICloudResources/Sources/ICloudResources.swift` — `ICloudFileResource` +- `submodules/InstantPageUI/Sources/InstantPageExternalMediaResource.swift` — `InstantPageExternalMediaResource` +- `submodules/LocalMediaResources/Sources/LocalMediaResources.swift` — `VideoLibraryMediaResource` +- `submodules/TelegramUniversalVideoContent/Sources/YoutubeEmbedImplementation.swift` — `YoutubeEmbedStoryboardMediaResource` + +These classes must conform to `TelegramMediaResource` to satisfy the PostboxCoding serialization contract. They remain `import Postbox`. + +--- + +## What's explicitly NOT in this plan + +- Adding opt-in `EngineMediaResource` overloads alongside raw-`MediaResource` overloads. The facade is changed in place. +- Touching any class conforming to `TelegramMediaResource`. +- Editing `TelegramUI`, `PassportUI`, `LegacyDataImport`, or the other heavy-ref modules in the Future-waves table beyond what the Phase-1 call-site migrations require. +- Importing UIKit/Display into TelegramCore under any circumstance. +- Modifying `_internal_*` functions in TelegramCore — they stay on raw `MediaResource`. +- Any behavior change, performance tweak, or "while we're here" cleanup. diff --git a/docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-3.md b/docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-3.md new file mode 100644 index 0000000000..a1d8b9fbcf --- /dev/null +++ b/docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-3.md @@ -0,0 +1,968 @@ +# Postbox → TelegramEngine Wave 3 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add three thin forwarding methods on `TelegramEngine.Resources` for fetch/status/data, then migrate `SaveToCameraRoll` to use them, drop `import Postbox` from that module, and update all 23 call sites. + +**Architecture:** Two atomic commits on branch `refactor/postbox-to-engine-wave-3`. C1 adds the facades in isolation. C2 changes `SaveToCameraRoll`'s public API (drops the `postbox:` parameter, switches `FetchMediaDataState.data` payload from `MediaResourceData` to `EngineMediaResource.ResourceData`), rewrites the module's internals via `context.engine.resources.*`, removes `import Postbox`, and updates every caller in the same commit so the tree remains buildable. + +**Tech Stack:** Swift / Bazel. No unit tests exist in this repo — verification is a full project build. + +**Spec:** [docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-3-design.md](docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-3-design.md) + +**Build command (use for every "full build" step):** + +```bash +source ~/.zshrc 2>/dev/null; PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber 1 --configuration debug_sim_arm64 +``` + +The prefix `source ~/.zshrc 2>/dev/null;` is required because `TELEGRAM_CODESIGNING_GIT_PASSWORD` lives in `~/.zshrc` and the bash tool does not source shell config by default. + +--- + +## Task 1: Add `TelegramEngine.Resources.fetch/status/data` facades (C1) + +**Files:** +- Modify: [submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift:415-417](submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift#L415) + +- [ ] **Step 1: Insert the three facade methods** + +Open `submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift`. Find the existing `applicationIcons()` method (currently the last method in the `Resources` class). Insert the three new methods immediately after it, still inside the `final class Resources` brace (before the closing `}`): + +```swift + public func applicationIcons() -> Signal { + return _internal_applicationIcons(account: account) + } + + public func fetch( + reference: MediaResourceReference, + userLocation: MediaResourceUserLocation, + userContentType: MediaResourceUserContentType + ) -> Signal { + return fetchedMediaResource( + mediaBox: self.account.postbox.mediaBox, + userLocation: userLocation, + userContentType: userContentType, + reference: reference + ) + } + + public func status( + resource: EngineMediaResource + ) -> Signal { + return self.account.postbox.mediaBox.resourceStatus(resource._asResource()) + |> map { EngineMediaResource.FetchStatus($0) } + } + + public func data( + resource: EngineMediaResource, + pathExtension: String?, + waitUntilFetchStatus: Bool + ) -> Signal { + return self.account.postbox.mediaBox.resourceData( + resource._asResource(), + pathExtension: pathExtension, + option: .complete(waitUntilFetchStatus: waitUntilFetchStatus) + ) + |> map { EngineMediaResource.ResourceData($0) } + } + } +} +``` + +- [ ] **Step 2: Full build — verify C1 compiles cleanly** + +Run the build command from the header. Expected: build succeeds with no errors. If a `signature mismatch` or `cannot find 'fetchedMediaResource'` error appears, double-check that `FetchedMediaResource.swift` and `MediaBox.swift` already export the referenced symbols (they do as of this plan's writing — no import changes are needed in `TelegramEngineResources.swift`, which already imports `Postbox`, `SwiftSignalKit`, and `TelegramApi`). + +- [ ] **Step 3: Commit C1** + +```bash +git add submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift +git commit -m "$(cat <<'EOF' +TelegramEngine.Resources: add fetch/status/data facades + +Thin forwarders over MediaBox for the narrow surface SaveToCameraRoll +needs. Takes EngineMediaResource and returns EngineMediaResource-typed +results where applicable. Wave-3 groundwork. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 2: Pre-flight — re-inventory call sites and verify ShareController postbox + +No code changes in this task. Its purpose is to catch drift from the spec's inventory before editing code, per CLAUDE.md's "inventory at execution time" guidance. + +**Files:** (read-only) +- Spec inventory: [docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-3-design.md](docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-3-design.md) +- Definition to verify: `submodules/ShareController/Sources/ShareController.swift` around line 2403 and `ShareControllerAppAccountContext` + +- [ ] **Step 1: Re-grep the current call-site set** + +Run: + +```bash +grep -rnE "(fetchMediaData|saveToCameraRoll|copyToPasteboard)\(" submodules --include="*.swift" \ + | grep -v "SaveToCameraRoll/Sources/SaveToCameraRoll.swift" \ + | grep -v "private func saveToCameraRoll" \ + | grep -v "self\?\.saveToCameraRoll\|strongSelf\.saveToCameraRoll" +``` + +Expected output has exactly 23 lines across 14 files, matching the spec's inventory table: + +| Module | File | Expected count | +|---|---|---| +| InstantPageUI | `Sources/InstantPageControllerNode.swift` | 2 | +| LegacyMediaPickerUI | `Sources/LegacyAttachmentMenu.swift` | 2 | +| LegacyMediaPickerUI | `Sources/LegacyAvatarPicker.swift` | 2 | +| BrowserUI | `Sources/BrowserInstantPageContent.swift` | 2 | +| GalleryUI | `Sources/Items/ChatImageGalleryItem.swift` | 2 | +| GalleryUI | `Sources/Items/UniversalVideoGalleryItem.swift` | 3 | +| TelegramUI (MediaEditorScreen) | `Components/MediaEditorScreen/Sources/MediaEditorScreen.swift` | 1 | +| TelegramUI (MediaEditorScreen) | `Components/MediaEditorScreen/Sources/EditStories.swift` | 1 | +| TelegramUI (ChatQrCodeScreen) | `Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift` | 1 | +| TelegramUI (StoryContainer) | `Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift` | 1 | +| TelegramUI (PeerInfoStoryGrid) | `Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift` | 1 | +| TelegramUI | `Sources/ChatInterfaceStateContextMenus.swift` | 1 | +| TelegramUI | `Sources/SaveMediaToFiles.swift` | 1 | +| ShareController | `Sources/ShareController.swift` | 3 | + +If the count or file list has drifted meaningfully from this table, **stop**, report the drift, and request a spec revision before continuing. Additions of one or two call sites can be folded in; larger drift should pause the wave. + +- [ ] **Step 2: Verify `ShareController:2406` postbox equivalence** + +Read `submodules/ShareController/Sources/ShareController.swift` lines 2395–2420. The private helper `saveToCameraRoll(messages:completion:)` contains `let postbox = self.currentContext.stateManager.postbox` and passes it to `SaveToCameraRoll.saveToCameraRoll`. After the migration, `SaveToCameraRoll` will use `context.account.postbox.mediaBox` internally. + +The enclosing function gates on `self.currentContext as? ShareControllerAppAccountContext`. In that code path, `accountContext.context.account` is the `Account` that `ShareControllerAppAccountContext` was built from, and `self.currentContext.stateManager` is that same account's state manager. Therefore `accountContext.context.account.postbox === self.currentContext.stateManager.postbox`. + +Confirm this by reading the definition of `ShareControllerAppAccountContext` in `submodules/AccountContext/Sources/ShareController.swift` (or the file where it's defined — grep for `ShareControllerAppAccountContext` to locate). If the `stateManager` there is derived from the same `account` whose `postbox` is reachable via `context.account.postbox`, treat the two as equivalent and proceed. If they can diverge (e.g., share-extension account switching creates a separate state manager), **stop** and abandon the ShareController:2406 edit with a recorded reason before continuing — the rest of the wave still applies. + +- [ ] **Step 3: Record verification outcome** + +Write a one-line note in the executor's task log noting either "ShareController:2406 postbox equivalence confirmed" or "ShareController:2406 abandoned — reason: ...". No commit. + +--- + +## Task 3: Migrate `SaveToCameraRoll` module + +This task changes the module's public API and internals. Build will fail after this task because all callers are still passing `postbox:` — that's expected and will be fixed in Task 4, which must land in the same commit as this task. + +**Files:** +- Modify: [submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift](submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift) (entire file rewritten as shown below) + +- [ ] **Step 1: Rewrite `SaveToCameraRoll.swift`** + +Replace the file's contents with: + +```swift +import Foundation +import UIKit +import SwiftSignalKit +import TelegramCore +import Photos +import Display +import MobileCoreServices +import DeviceAccess +import AccountContext +import LegacyComponents + +public enum FetchMediaDataState { + case progress(Float) + case data(EngineMediaResource.ResourceData) +} + +public func fetchMediaData(context: AccountContext, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, mediaReference: AnyMediaReference, forceVideo: Bool = false) -> Signal<(FetchMediaDataState, Bool), NoError> { + var resource: TelegramMediaResource? + var isImage = true + var fileExtension: String? + var userContentType: MediaResourceUserContentType = .other + if let image = mediaReference.media as? TelegramMediaImage { + userContentType = .image + if let video = image.videoRepresentations.last, forceVideo { + resource = video.resource + isImage = false + } else if let representation = largestImageRepresentation(image.representations) { + resource = representation.resource + } + } else if let file = mediaReference.media as? TelegramMediaFile { + userContentType = MediaResourceUserContentType(file: file) + resource = file.resource + if file.isVideo || file.mimeType.hasPrefix("video/") { + isImage = false + } + let maybeExtension = ((file.fileName ?? "") as NSString).pathExtension + if !maybeExtension.isEmpty { + fileExtension = maybeExtension + } + } else if let webpage = mediaReference.media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + if let file = content.file { + resource = file.resource + if file.isVideo { + isImage = false + } + } else if let image = content.image { + if let representation = largestImageRepresentation(image.representations) { + resource = representation.resource + } + } + } + if let customUserContentType { + userContentType = customUserContentType + } + + if let resource = resource { + let engineResource = EngineMediaResource(resource) + let fetchedData: Signal = Signal { subscriber in + let fetched = context.engine.resources.fetch( + reference: mediaReference.resourceReference(resource), + userLocation: userLocation, + userContentType: userContentType + ).start() + let status = context.engine.resources.status(resource: engineResource).start(next: { status in + switch status { + case .Local: + subscriber.putNext(.progress(1.0)) + case .Remote: + subscriber.putNext(.progress(0.0)) + case let .Fetching(_, progress): + subscriber.putNext(.progress(progress)) + case let .Paused(progress): + subscriber.putNext(.progress(progress)) + } + }) + let data = context.engine.resources.data( + resource: engineResource, + pathExtension: fileExtension, + waitUntilFetchStatus: true + ).start(next: { next in + subscriber.putNext(.data(next)) + }, completed: { + subscriber.putCompletion() + }) + return ActionDisposable { + fetched.dispose() + status.dispose() + data.dispose() + } + } + return fetchedData + |> map { data in + return (data, isImage) + } + } else { + return .complete() + } +} + +public func saveToCameraRoll(context: AccountContext, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, mediaReference: AnyMediaReference, video: AnyMediaReference? = nil) -> Signal { + let mediaData: Signal<(FetchMediaDataState, Bool), NoError> = fetchMediaData(context: context, userLocation: userLocation, customUserContentType: customUserContentType, mediaReference: mediaReference) + let videoData: Signal + if let video { + videoData = fetchMediaData(context: context, userLocation: userLocation, customUserContentType: customUserContentType, mediaReference: video) + |> map { state, _ in + return state + } + |> map(Optional.init) + } else { + videoData = .single(nil) + } + + return combineLatest( + queue: Queue.mainQueue(), + mediaData, + videoData + ) + |> mapToSignal { stateAndIsImage, videoStateAndIsImage -> Signal in + let isImage = stateAndIsImage.1 + var mainData: EngineMediaResource.ResourceData? + var videoData: EngineMediaResource.ResourceData? + var waitForVideo = false + if let videoState = videoStateAndIsImage { + switch videoState { + case let .progress(value): + return .single(value * 0.95) + case let .data(data): + videoData = data + } + switch stateAndIsImage.0 { + case let .progress(value): + return .single(0.95 + 0.05 * value) + case let .data(data): + mainData = data + } + waitForVideo = true + } else { + switch stateAndIsImage.0 { + case let .progress(value): + return .single(value) + case let .data(data): + mainData = data + } + } + if let mainData, mainData.isComplete, videoData != nil || !waitForVideo { + return Signal { subscriber in + DeviceAccess.authorizeAccess(to: .mediaLibrary(.save), presentationData: context.sharedContext.currentPresentationData.with { $0 }, present: { c, a in + context.sharedContext.presentGlobalController(c, a) + }, openSettings: context.sharedContext.applicationBindings.openSettings, { authorized in + if !authorized { + subscriber.putCompletion() + return + } + + let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4" + if isImage, let videoData, let imageData = try? Data(contentsOf: URL(fileURLWithPath: mainData.path)) { + let id = UUID().uuidString + + let jpegWithID = addAssetIdentifierToJPEG(imageData, assetIdentifier: id)! + let outputVideoURL = URL(fileURLWithPath: NSTemporaryDirectory() + "\(id).mov") + + try? FileManager.default.copyItem(atPath: videoData.path, toPath: tempVideoPath) + + addAssetIdentifierToVideo(inputURL: URL(fileURLWithPath: tempVideoPath), outputURL: outputVideoURL, assetIdentifier: id) { success in + guard success else { return } + + PHPhotoLibrary.shared().performChanges({ + let request = PHAssetCreationRequest.forAsset() + + request.addResource(with: .photo, data: jpegWithID, options: nil) + request.addResource(with: .pairedVideo, fileURL: outputVideoURL, options: nil) + }, completionHandler: { _, error in + let _ = try? FileManager.default.removeItem(atPath: tempVideoPath) + subscriber.putNext(1.0) + subscriber.putCompletion() + }) + } + } else { + PHPhotoLibrary.shared().performChanges({ + if isImage { + if let imageData = try? Data(contentsOf: URL(fileURLWithPath: mainData.path)) { + PHAssetCreationRequest.forAsset().addResource(with: .photo, data: imageData, options: nil) + } + } else { + if let _ = try? FileManager.default.copyItem(atPath: mainData.path, toPath: tempVideoPath) { + PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: tempVideoPath)) + } + } + }, completionHandler: { _, error in + if let error { + print("\(error)") + } + let _ = try? FileManager.default.removeItem(atPath: tempVideoPath) + subscriber.putNext(1.0) + subscriber.putCompletion() + }) + } + }) + + return ActionDisposable { + } + } + } else { + return .complete() + } + } +} + +public func copyToPasteboard(context: AccountContext, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference) -> Signal { + return fetchMediaData(context: context, userLocation: userLocation, mediaReference: mediaReference) + |> mapToSignal { state, isImage -> Signal in + if case let .data(data) = state, data.isComplete { + return Signal { subscriber in + let pasteboard = UIPasteboard.general + + if mediaReference.media is TelegramMediaImage { + if let fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: .mappedIfSafe) { + pasteboard.setData(fileData, forPasteboardType: kUTTypeJPEG as String) + } + } + subscriber.putNext(Void()) + subscriber.putCompletion() + + return EmptyDisposable + } + } else { + return .complete() + } + } + |> mapToSignal { _ -> Signal in return .complete() } +} + +private func addAssetIdentifierToJPEG(_ imageData: Data, assetIdentifier: String) -> Data? { + guard let source = CGImageSourceCreateWithData(imageData as CFData, nil), let uti = CGImageSourceGetType(source), let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else { + return nil + } + + let mutableData = NSMutableData() + guard let destination = CGImageDestinationCreateWithData(mutableData, uti, 1, nil) else { + return nil + } + + var metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any] ?? [:] + + var maker = metadata[kCGImagePropertyMakerAppleDictionary as String] as? [String: Any] ?? [:] + maker["17"] = assetIdentifier + metadata[kCGImagePropertyMakerAppleDictionary as String] = maker + + CGImageDestinationAddImage(destination, cgImage, metadata as CFDictionary) + CGImageDestinationFinalize(destination) + + return mutableData as Data +} + +private func addAssetIdentifierToVideo(inputURL: URL, outputURL: URL, assetIdentifier: String, completion: @escaping (Bool) -> Void) { + let asset = AVAsset(url: inputURL) + + guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough) else { + completion(false) + return + } + + let identifierItem = AVMutableMetadataItem() + identifierItem.keySpace = .quickTimeMetadata + identifierItem.key = AVMetadataKey.quickTimeMetadataKeyContentIdentifier as NSString + identifierItem.value = assetIdentifier as NSString + + let stillImageTimeItem = AVMutableMetadataItem() + let keyStillImageTime = "com.apple.quicktime.still-image-time" + let keySpaceQuickTimeMetadata = "mdta" + stillImageTimeItem.key = keyStillImageTime as (NSCopying & NSObjectProtocol)? + stillImageTimeItem.keySpace = AVMetadataKeySpace(rawValue: keySpaceQuickTimeMetadata) + stillImageTimeItem.value = 0 as (NSCopying & NSObjectProtocol)? + stillImageTimeItem.dataType = "com.apple.metadata.datatype.int8" + + exportSession.outputURL = outputURL + exportSession.outputFileType = .mov + exportSession.metadata = [identifierItem, stillImageTimeItem] + exportSession.shouldOptimizeForNetworkUse = true + + exportSession.exportAsynchronously { + completion(exportSession.status == .completed) + } +} +``` + +The key differences from the original file: + +1. `import Postbox` — removed. +2. `FetchMediaDataState.data(MediaResourceData)` → `FetchMediaDataState.data(EngineMediaResource.ResourceData)`. +3. Three public functions drop their `postbox: Postbox` parameter. +4. `var resource: MediaResource?` → `var resource: TelegramMediaResource?`. +5. Inside `fetchMediaData`: build an `EngineMediaResource(resource)` once, and call `context.engine.resources.fetch / status / data` instead of `fetchedMediaResource(...)` / `postbox.mediaBox.resourceStatus(...)` / `postbox.mediaBox.resourceData(...)`. +6. `var mainData: MediaResourceData?` / `var videoData: MediaResourceData?` → `var ...: EngineMediaResource.ResourceData?`. +7. `mainData.complete` → `mainData.isComplete`. `data.complete` (in `copyToPasteboard`) → `data.isComplete`. Field `data.path` is unchanged. + +- [ ] **Step 2: Do not build yet — proceed to Task 4** + +Builds will fail until every caller in Task 4 is migrated. Do not run the build command here. No commit yet either — Task 3 and Task 4 share a single atomic commit in Task 5. + +--- + +## Task 4: Update all 23 call sites + +Every call site does one or both of two edits: + +- **Edit A (all 23 sites):** drop `postbox: someExpression,` from the argument list. +- **Edit B (the 7 sites that destructure `fetchMediaData`):** rename `.complete` → `.isComplete` on the destructured data value; `.path` stays the same. + +Each sub-step below is one file. No builds between files. No commit. Task 5 builds everything together. + +**Sub-task 4.1 — InstantPageUI** + +- [ ] **File:** [submodules/InstantPageUI/Sources/InstantPageControllerNode.swift](submodules/InstantPageUI/Sources/InstantPageControllerNode.swift) + +At line 1027, replace: + +```swift + let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() +``` + +with: + +```swift + let _ = copyToPasteboard(context: strongSelf.context, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() +``` + +At line 1032, replace: + +```swift + let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() +``` + +with: + +```swift + let _ = saveToCameraRoll(context: strongSelf.context, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() +``` + +**Sub-task 4.2 — LegacyMediaPickerUI / LegacyAttachmentMenu.swift** (destructures) + +- [ ] **File:** [submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift](submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift) + +At line 173, replace: + +```swift + let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media) +``` + +with: + +```swift + let _ = (fetchMediaData(context: context, userLocation: .other, mediaReference: media) +``` + +In the `.start` block that follows (around line 175), replace `data.complete` with `data.isComplete` (only the `.complete` boolean access — do not touch `data.path`). + +At line 490, replace: + +```swift + let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: editCurrentMedia) +``` + +with: + +```swift + let _ = (fetchMediaData(context: context, userLocation: .other, mediaReference: editCurrentMedia) +``` + +In the destructuring block that follows (around line 492), replace `data.complete` with `data.isComplete`. + +**Sub-task 4.3 — LegacyMediaPickerUI / LegacyAvatarPicker.swift** (destructures) + +- [ ] **File:** [submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift](submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift) + +At line 58, replace: + +```swift + let imageSignal = fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media, forceVideo: false) +``` + +with: + +```swift + let imageSignal = fetchMediaData(context: context, userLocation: .other, mediaReference: media, forceVideo: false) +``` + +In the `|> map` block immediately after (line ~60), replace `data.complete` with `data.isComplete`. + +At line 67, replace: + +```swift + let videoSignal = isVideo ? fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media, forceVideo: true) +``` + +with: + +```swift + let videoSignal = isVideo ? fetchMediaData(context: context, userLocation: .other, mediaReference: media, forceVideo: true) +``` + +In the `|> map` block immediately after (line ~69), replace `data.complete` with `data.isComplete`. + +**Sub-task 4.4 — BrowserUI / BrowserInstantPageContent.swift** + +- [ ] **File:** [submodules/BrowserUI/Sources/BrowserInstantPageContent.swift](submodules/BrowserUI/Sources/BrowserInstantPageContent.swift) + +At line 1175, replace: + +```swift + let _ = copyToPasteboard(context: self.context, postbox: self.context.account.postbox, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() +``` + +with: + +```swift + let _ = copyToPasteboard(context: self.context, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() +``` + +At line 1180, replace: + +```swift + let _ = saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() +``` + +with: + +```swift + let _ = saveToCameraRoll(context: self.context, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() +``` + +**Sub-task 4.5 — GalleryUI / ChatImageGalleryItem.swift** (one destructures) + +- [ ] **File:** [submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift](submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift) + +At line 732, replace: + +```swift + let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media) +``` + +with: + +```swift + let _ = (fetchMediaData(context: context, userLocation: .other, mediaReference: media) +``` + +In the `.start` block that follows (around line 734), replace `data.complete` with `data.isComplete`. + +At line 758, replace: + +```swift + let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: media) +``` + +with: + +```swift + let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, userLocation: .peer(message.id.peerId), mediaReference: media) +``` + +**Sub-task 4.6 — GalleryUI / UniversalVideoGalleryItem.swift** + +- [ ] **File:** [submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift](submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift) + +At line 3764, replace: + +```swift + let saveSignal = SaveToCameraRoll.saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: saveFileReference) +``` + +with: + +```swift + let saveSignal = SaveToCameraRoll.saveToCameraRoll(context: self.context, userLocation: .peer(message.id.peerId), mediaReference: saveFileReference) +``` + +At line 3810, replace: + +```swift + let _ = (SaveToCameraRoll.saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file)) +``` + +with: + +```swift + let _ = (SaveToCameraRoll.saveToCameraRoll(context: self.context, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file)) +``` + +At line 3867, replace: + +```swift + let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: image), video: videoReference) +``` + +with: + +```swift + let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: image), video: videoReference) +``` + +**Sub-task 4.7 — TelegramUI / MediaEditorScreen / MediaEditorScreen.swift** (destructures) + +- [ ] **File:** [submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift](submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift) + +At line 5136, in the multi-line call starting with `let _ = (fetchMediaData(`, delete the line ` postbox: self.context.account.postbox,`. The remaining call should read: + +```swift + let _ = (fetchMediaData( + context: self.context, + userLocation: .other, + mediaReference: file + ) |> deliverOnMainQueue).start(next: { [weak self] state, _ in +``` + +Inside this closure, the destructuring is `if case let .data(data) = state { let path = data.path ... }` — `data.path` stays unchanged, and this site does not access `data.complete` (verified against the current file). No Edit B rename needed here. + +**Sub-task 4.8 — TelegramUI / MediaEditorScreen / EditStories.swift** (destructures) + +- [ ] **File:** [submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift](submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift) + +At line 37, replace: + +```swift + return fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .peer(peerReference.id), customUserContentType: .story, mediaReference: .story(peer: peerReference, id: storyItem.id, media: media)) +``` + +with: + +```swift + return fetchMediaData(context: context, userLocation: .peer(peerReference.id), customUserContentType: .story, mediaReference: .story(peer: peerReference, id: storyItem.id, media: media)) +``` + +At line 39 (inside the `mapToSignal`), replace: + +```swift + guard case let .data(data) = value, data.complete else { +``` + +with: + +```swift + guard case let .data(data) = value, data.isComplete else { +``` + +(`data.path` accesses below this line remain unchanged.) + +**Sub-task 4.9 — TelegramUI / ChatQrCodeScreen / ChatQrCodeScreen.swift** (destructures) + +- [ ] **File:** [submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift](submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift) + +At line 2505, replace: + +```swift + let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: userLocation, mediaReference: AnyMediaReference.standalone(media: media)) +``` + +with: + +```swift + let _ = (fetchMediaData(context: context, userLocation: userLocation, mediaReference: AnyMediaReference.standalone(media: media)) +``` + +At line 2507, replace: + +```swift + guard case let .data(data) = value, data.complete else { +``` + +with: + +```swift + guard case let .data(data) = value, data.isComplete else { +``` + +**Sub-task 4.10 — TelegramUI / StoryContainerScreen / StoryItemSetContainerComponent.swift** + +- [ ] **File:** [submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift](submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift) + +At line 5980, replace: + +```swift + let disposable = (saveToCameraRoll(context: component.context, postbox: component.context.account.postbox, userLocation: .peer(peerReference.id), customUserContentType: .story, mediaReference: .story(peer: peerReference, id: component.slice.item.storyItem.id, media: component.slice.item.storyItem.media._asMedia())) +``` + +with: + +```swift + let disposable = (saveToCameraRoll(context: component.context, userLocation: .peer(peerReference.id), customUserContentType: .story, mediaReference: .story(peer: peerReference, id: component.slice.item.storyItem.id, media: component.slice.item.storyItem.media._asMedia())) +``` + +**Sub-task 4.11 — TelegramUI / PeerInfoStoryGridScreen / PeerInfoStoryGridScreen.swift** + +- [ ] **File:** [submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift](submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift) + +At line 268, replace: + +```swift + signals.append(saveToCameraRoll(context: component.context, postbox: component.context.account.postbox, userLocation: .other, mediaReference: .story(peer: peerReference, id: item.id, media: item.media._asMedia())) +``` + +with: + +```swift + signals.append(saveToCameraRoll(context: component.context, userLocation: .other, mediaReference: .story(peer: peerReference, id: item.id, media: item.media._asMedia())) +``` + +**Sub-task 4.12 — TelegramUI / Sources / ChatInterfaceStateContextMenus.swift** + +- [ ] **File:** [submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift](submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift) + +At line 1419, replace: + +```swift + let _ = (saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: mediaReference) +``` + +with: + +```swift + let _ = (saveToCameraRoll(context: context, userLocation: .peer(message.id.peerId), mediaReference: mediaReference) +``` + +**Sub-task 4.13 — TelegramUI / Sources / SaveMediaToFiles.swift** (destructures) + +- [ ] **File:** [submodules/TelegramUI/Sources/SaveMediaToFiles.swift](submodules/TelegramUI/Sources/SaveMediaToFiles.swift) + +At line 27, replace: + +```swift + var signal = fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: fileReference.abstract) +``` + +with: + +```swift + var signal = fetchMediaData(context: context, userLocation: .other, mediaReference: fileReference.abstract) +``` + +At line 63, replace: + +```swift + if data.complete { +``` + +with: + +```swift + if data.isComplete { +``` + +(`data.path` accesses in the block below remain unchanged.) + +**Sub-task 4.14 — ShareController / ShareController.swift** + +- [ ] **File:** [submodules/ShareController/Sources/ShareController.swift](submodules/ShareController/Sources/ShareController.swift) + +At line 2406, after verifying Task 2's postbox-equivalence, replace: + +```swift + return SaveToCameraRoll.saveToCameraRoll(context: context, postbox: postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: media)) +``` + +with: + +```swift + return SaveToCameraRoll.saveToCameraRoll(context: context, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: media)) +``` + +Also delete the now-unused local binding above (line 2403): + +```swift + let postbox = self.currentContext.stateManager.postbox +``` + +(This line is used only by the `saveToCameraRoll` call on line 2406. If the build later flags it as unused instead of an error, leave it; but preferred is to remove the dead binding.) + +At line 2432, replace: + +```swift + self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: .standalone(media: media)) |> map(Optional.init), dismissImmediately: true, completion: {}) +``` + +with: + +```swift + self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, userLocation: .other, mediaReference: .standalone(media: media)) |> map(Optional.init), dismissImmediately: true, completion: {}) +``` + +At line 2441, replace: + +```swift + self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: mediaReference) |> map(Optional.init), dismissImmediately: completion == nil, completion: completion ?? {}) +``` + +with: + +```swift + self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, userLocation: .other, mediaReference: mediaReference) |> map(Optional.init), dismissImmediately: completion == nil, completion: completion ?? {}) +``` + +(The abandonment branch: if Task 2's verification found `stateManager.postbox` and `account.postbox` are non-equivalent, skip the `line 2406` edit, leave `let postbox = self.currentContext.stateManager.postbox` in place, and revert Task 3's change to the `saveToCameraRoll` public signature only for this one callsite — which is impossible without duplicate signatures, so in that case abandon the entire wave and record the reason in a new commit to the plan.) + +--- + +## Task 5: Full build and commit C2 + +- [ ] **Step 1: Run the full project build** + +Run the build command from the header. Expected: build succeeds with no errors across all modules. + +If there are failures, they fall into a few predictable categories and are fixed in place — do not split into another commit: + +- **"cannot convert value of type 'Postbox' to expected argument type"** — a call site was missed. Grep again for `postbox: ` usages in the migrated files and fix. +- **"value of type 'EngineMediaResource.ResourceData' has no member 'complete'"** — an Edit B site was missed. Rename to `isComplete`. +- **"use of unresolved identifier 'fetchedMediaResource'" or similar inside `SaveToCameraRoll.swift`** — indicates `import Postbox` was dropped but a bare Postbox top-level function is still referenced. Replace the call with the engine facade introduced in Task 1. +- **Warnings about unused local `let postbox = ...`** — delete the binding. + +Re-run the build after each fix until it succeeds. + +- [ ] **Step 2: Stage all touched files** + +```bash +git add \ + submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift \ + submodules/InstantPageUI/Sources/InstantPageControllerNode.swift \ + submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift \ + submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift \ + submodules/BrowserUI/Sources/BrowserInstantPageContent.swift \ + submodules/GalleryUI/Sources/Items/ChatImageGalleryItem.swift \ + submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift \ + submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift \ + submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift \ + submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift \ + submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift \ + submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift \ + submodules/TelegramUI/Sources/SaveMediaToFiles.swift \ + submodules/ShareController/Sources/ShareController.swift +``` + +- [ ] **Step 3: Verify the diff is clean** + +Run: + +```bash +git diff --staged --stat +``` + +Expected: exactly 15 files changed, with SaveToCameraRoll.swift having the largest diff (the full-file rewrite) and each call-site file showing small line-count changes. + +- [ ] **Step 4: Commit C2** + +```bash +git commit -m "$(cat <<'EOF' +SaveToCameraRoll: drop import Postbox via engine.resources facades + +Migrates SaveToCameraRoll's three public functions to take context +only (no more postbox:), switches the FetchMediaDataState.data payload +from MediaResourceData to EngineMediaResource.ResourceData, rewrites +internals via TelegramEngine.Resources.fetch/status/data, and drops +import Postbox from the module. All 23 call sites across 14 files +updated in the same commit to keep the tree buildable. + +Wave-3 of the Postbox -> TelegramEngine refactor. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 5: Verify branch log** + +Run: + +```bash +git log --oneline refactor/postbox-to-engine-wave-3 | head -5 +``` + +Expected: the top two commits on the branch are `SaveToCameraRoll: drop import Postbox ...` (C2) and `TelegramEngine.Resources: add fetch/status/data facades` (C1), above the previous spec commits. + +- [ ] **Step 6: Update CLAUDE.md tally** + +Open `CLAUDE.md`, find the "Modules currently free of `import Postbox`" section, and add `SaveToCameraRoll (wave 3)` to the bullet list. Also add a "Wave 3 outcome (2026-04-18)" subsection documenting: three facades added on `TelegramEngine.Resources`, `SaveToCameraRoll` fully de-Postboxed, 23 call sites migrated. If any call site was abandoned in Task 2, record the reason here. + +Commit: + +```bash +git add CLAUDE.md +git commit -m "$(cat <<'EOF' +CLAUDE.md: record wave-3 outcome + +Adds SaveToCameraRoll to the Postbox-free module tally and documents +the three new TelegramEngine.Resources facades added in wave 3. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Success criteria + +- `submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift` contains no `import Postbox`. +- `grep -rnE "(fetchMediaData|saveToCameraRoll|copyToPasteboard)\\(" submodules --include="*.swift" | grep "postbox:"` returns zero matches outside of the private `collectExternalShareResource`/`collectExternalShareItems` helpers in `ShareController.swift` (which take their own `postbox:` parameters unrelated to SaveToCameraRoll). +- Full build succeeds in `debug_sim_arm64` configuration. +- Three branch commits above the spec commits: C1 (facades), C2 (SaveToCameraRoll + callers), C3 (CLAUDE.md tally). diff --git a/docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-4.md b/docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-4.md new file mode 100644 index 0000000000..21e88108d6 --- /dev/null +++ b/docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-4.md @@ -0,0 +1,500 @@ +# Postbox → TelegramEngine Wave 4 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate `TelegramEngine.Stickers.uploadSticker`'s public surface — `peer: Peer → EnginePeer`, `resource: MediaResource → EngineMediaResource`, `thumbnail: MediaResource? → EngineMediaResource?`, and `UploadStickerStatus.complete(CloudDocumentMediaResource, String) → .complete(EngineMediaResource, String)` — with one atomic commit touching the facade, the internal enum, and the two call sites. + +**Architecture:** Two commits on branch `refactor/postbox-to-engine-wave-4`. C1 is the atomic four-file code change. C2 is the CLAUDE.md tally update. `_internal_uploadSticker` keeps its raw `Peer`/`MediaResource` signature; the facade does all the wrapping/unwrapping. One spec-allowed one-line exception: `_internal_uploadSticker` constructs `EngineMediaResource(uploadedResource)` at the `.complete(...)` result-construction site to keep `UploadStickerStatus` as a single enum instead of splitting into raw+engine variants. + +**Tech Stack:** Swift / Bazel. No unit tests in this repo — verification is a full project build. + +**Spec:** [docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-4-design.md](docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-4-design.md) + +**Build command** (use for every "full build" step): + +```bash +source ~/.zshrc 2>/dev/null; PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber 1 --configuration debug_sim_arm64 +``` + +The `source ~/.zshrc` prefix is required because `TELEGRAM_CODESIGNING_GIT_PASSWORD` lives in `~/.zshrc` and the bash tool does not source shell config by default. For a background build from the controller session, prefer `run_in_background: true` and monitor by tailing the task output file (subagent-spawned background builds orphan when the subagent shell terminates). + +--- + +## Task 1: Pre-flight re-verification + +No code changes. Purpose: re-confirm the facade call-site count and the MediaEditorScreen line numbers haven't drifted. + +**Files:** (read-only) + +- [ ] **Step 1: Re-grep facade call sites** + +```bash +grep -rnE "\.uploadSticker\(" submodules --include="*.swift" \ + | grep -v "/TelegramEngine/Stickers/" \ + | grep -v "self\.uploadSticker\|strongSelf\.uploadSticker\|self\?\.uploadSticker" +``` + +Expected output: exactly 2 lines + +- `submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift:91` +- `submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift:8099` + +If the count or line numbers have drifted meaningfully, stop and revise the plan before editing. + +- [ ] **Step 2: Re-read MediaEditorScreen block** + +```bash +sed -n '8080,8190p' submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +``` + +Visually confirm: +- Line ~8097 has `.complete(resource, mimeType)` inside an `if let resource = resource as? CloudDocumentMediaResource { … }` branch. +- Line ~8099 has `context.engine.stickers.uploadSticker(peer: peer._asPeer(), resource: resource, thumbnail: file.previewRepresentations.first?.resource, …)`. +- Line ~8105 has `case let .complete(resource, _):` destructuring the inner `.mapToSignal` status. +- Line ~8106 has `stickerFile(resource: resource, thumbnailResource: file.previewRepresentations.first?.resource, …)`. +- Line ~8119 has `ImportSticker(resource: .standalone(resource: resource), …)` inside `case let .createStickerPack(title):`. +- Line ~8138 has a second `ImportSticker(resource: .standalone(resource: resource), …)` inside `case let .addToStickerPack(pack, title):`. +- Line ~8178 has a second `case let .complete(resource, _):` in the outer `.startStandalone(next: …)` handler. +- Line ~8180 has `stickerFile(resource: resource, thumbnailResource: file.previewRepresentations.first?.resource, size: resource.size ?? 0, …)`. + +- [ ] **Step 3: Confirm `stickerFile` signature** + +```bash +grep -nE "^private func stickerFile\(" submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +``` + +Expected: `private func stickerFile(resource: TelegramMediaResource, thumbnailResource: TelegramMediaResource?, size: Int64, dimensions: PixelDimensions, duration: Double?, isVideo: Bool) -> TelegramMediaFile` at line ~9196. This confirms `stickerFile` takes `TelegramMediaResource` (requires `resource._asResource()` at every call). + +- [ ] **Step 4: Confirm ImportStickerPackController's `peer` type** + +```bash +sed -n '82,95p' submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift +``` + +Expected pattern: +```swift +let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) +|> deliverOnMainQueue).start(next: { [weak self] peer in +``` + +`postbox.loadedPeerWithId` returns `Signal`. The local `peer` is therefore a raw `Peer`, not an `EnginePeer`. The call-site edit will need `EnginePeer(peer)` to wrap. + +If any of these expectations fails to match the current source, stop and revise the plan. + +--- + +## Task 2: Migrate `UploadStickerStatus` enum and internal wrap + +No build; the project won't compile until Tasks 3–5 also land. Do not commit. + +**File:** `submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift` + +- [ ] **Step 1: Update enum payload (line 7–10)** + +Replace: + +```swift +public enum UploadStickerStatus { + case progress(Float) + case complete(CloudDocumentMediaResource, String) +} +``` + +with: + +```swift +public enum UploadStickerStatus { + case progress(Float) + case complete(EngineMediaResource, String) +} +``` + +- [ ] **Step 2: Update the `.complete(...)` construction in `_internal_uploadSticker` (line ~97)** + +Replace the line reading: + +```swift + return .single(.complete(uploadedResource, file.mimeType)) +``` + +with: + +```swift + return .single(.complete(EngineMediaResource(uploadedResource), file.mimeType)) +``` + +Nothing else in `_internal_uploadSticker` changes. In particular its parameter list (`peer: Peer, resource: MediaResource, thumbnail: MediaResource? = nil, …`) stays exactly as is. + +--- + +## Task 3: Migrate the public facade signature + +No build; no commit. + +**File:** `submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift` + +- [ ] **Step 1: Update the `uploadSticker` facade (line 85–87)** + +Replace: + +```swift + public func uploadSticker(peer: Peer, resource: MediaResource, thumbnail: MediaResource?, alt: String, dimensions: PixelDimensions, duration: Double?, mimeType: String) -> Signal { + return _internal_uploadSticker(account: self.account, peer: peer, resource: resource, thumbnail: thumbnail, alt: alt, dimensions: dimensions, duration: duration, mimeType: mimeType) + } +``` + +with: + +```swift + public func uploadSticker(peer: EnginePeer, resource: EngineMediaResource, thumbnail: EngineMediaResource?, alt: String, dimensions: PixelDimensions, duration: Double?, mimeType: String) -> Signal { + return _internal_uploadSticker(account: self.account, peer: peer._asPeer(), resource: resource._asResource(), thumbnail: thumbnail?._asResource(), alt: alt, dimensions: dimensions, duration: duration, mimeType: mimeType) + } +``` + +No other method in `TelegramEngineStickers.swift` changes. + +--- + +## Task 4: Migrate `ImportStickerPackController.swift:91` + +No build; no commit. + +**File:** `submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift` + +- [ ] **Step 1: Update the facade call (line ~91)** + +Replace: + +```swift + signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource._asResource(), thumbnail: nil, alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), duration: nil, mimeType: sticker.mimeType) +``` + +with: + +```swift + signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: EnginePeer(peer), resource: resource, thumbnail: nil, alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), duration: nil, mimeType: sticker.mimeType) +``` + +Two changes: `peer` (raw `Peer`) → `EnginePeer(peer)`, and `resource._asResource()` → `resource` (the local `resource` is an `EngineMediaResource`). + +- [ ] **Step 2: Update the destructure re-wrap (line ~99)** + +Replace: + +```swift + case let .complete(resource, mimeType): + if ["application/x-tgsticker", "video/webm"].contains(mimeType) { + return (sticker.uuid, .verified, EngineMediaResource(resource)) + } else { +``` + +with: + +```swift + case let .complete(resource, mimeType): + if ["application/x-tgsticker", "video/webm"].contains(mimeType) { + return (sticker.uuid, .verified, resource) + } else { +``` + +One change: `EngineMediaResource(resource)` → `resource`. The destructured `resource` is now already an `EngineMediaResource`. + +Nothing else in this file changes. + +--- + +## Task 5: Migrate `MediaEditorScreen.swift` sticker-upload block + +No build; no commit. This task touches multiple lines inside a single nested block (~8084–8190). The `UploadStickerStatus` payload migration cascades: wherever the code constructs or destructures `.complete(...)`, types change. + +**File:** `submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift` + +- [ ] **Step 1: Wrap at the direct construction site (line ~8097)** + +Replace the line reading: + +```swift + return .single((.progress(1.0), nil)) |> then(.single((.complete(resource, mimeType), nil))) +``` + +with: + +```swift + return .single((.progress(1.0), nil)) |> then(.single((.complete(EngineMediaResource(resource), mimeType), nil))) +``` + +Context: this is inside `if let resource = resource as? CloudDocumentMediaResource { … }`, so `resource` here is `CloudDocumentMediaResource`; the outer tuple's `UploadStickerStatus.complete` now takes `EngineMediaResource`. + +- [ ] **Step 2: Migrate the facade call (line ~8099)** + +Replace: + +```swift + return context.engine.stickers.uploadSticker(peer: peer._asPeer(), resource: resource, thumbnail: file.previewRepresentations.first?.resource, alt: "", dimensions: dimensions, duration: duration, mimeType: mimeType) +``` + +with: + +```swift + return context.engine.stickers.uploadSticker(peer: peer, resource: EngineMediaResource(resource), thumbnail: file.previewRepresentations.first.flatMap { EngineMediaResource($0.resource) }, alt: "", dimensions: dimensions, duration: duration, mimeType: mimeType) +``` + +Three changes: `peer._asPeer()` → `peer` (local is `EnginePeer`); `resource` → `EngineMediaResource(resource)` (local is raw `MediaResource` from the outer enum destructure); `file.previewRepresentations.first?.resource` → `file.previewRepresentations.first.flatMap { EngineMediaResource($0.resource) }`. + +- [ ] **Step 3: Unwrap at inner-handler `stickerFile` call (line ~8106)** + +Replace: + +```swift + case let .complete(resource, _): + let file = stickerFile(resource: resource, thumbnailResource: file.previewRepresentations.first?.resource, size: file.size ?? 0, dimensions: dimensions, duration: file.duration, isVideo: isVideo) +``` + +with: + +```swift + case let .complete(resource, _): + let file = stickerFile(resource: resource._asResource(), thumbnailResource: file.previewRepresentations.first?.resource, size: file.size ?? 0, dimensions: dimensions, duration: file.duration, isVideo: isVideo) +``` + +The destructured `resource` is now an `EngineMediaResource`. `stickerFile` (see line 9196) takes `TelegramMediaResource`, so unwrap with `._asResource()`. `file.previewRepresentations.first?.resource` is already a `TelegramMediaResource?` — no change there. + +- [ ] **Step 4: Unwrap at `.createStickerPack` sticker construction (line ~8119)** + +Replace: + +```swift + case let .createStickerPack(title): + let sticker = ImportSticker( + resource: .standalone(resource: resource), + emojis: emojis, + dimensions: dimensions, + duration: duration, + mimeType: mimeType, + keywords: "" + ) +``` + +with: + +```swift + case let .createStickerPack(title): + let sticker = ImportSticker( + resource: .standalone(resource: resource._asResource()), + emojis: emojis, + dimensions: dimensions, + duration: duration, + mimeType: mimeType, + keywords: "" + ) +``` + +`MediaResourceReference.standalone(resource:)` takes `MediaResource`; `resource` here is the `EngineMediaResource` destructured at line ~8105. Unwrap with `._asResource()`. + +- [ ] **Step 5: Unwrap at `.addToStickerPack` sticker construction (line ~8138)** + +Replace: + +```swift + case let .addToStickerPack(pack, title): + let sticker = ImportSticker( + resource: .standalone(resource: resource), + emojis: emojis, + dimensions: dimensions, + duration: duration, + mimeType: mimeType, + keywords: "" + ) +``` + +with: + +```swift + case let .addToStickerPack(pack, title): + let sticker = ImportSticker( + resource: .standalone(resource: resource._asResource()), + emojis: emojis, + dimensions: dimensions, + duration: duration, + mimeType: mimeType, + keywords: "" + ) +``` + +Same unwrap as Step 4. + +- [ ] **Step 6: Unwrap at outer-handler `stickerFile` call (line ~8178–8180)** + +Replace: + +```swift + case let .complete(resource, _): + let navigationController = self.effectiveNavigationController as? NavigationController + + let result: MediaEditorScreenImpl.Result + switch action { + case .update: + result = MediaEditorScreenImpl.Result(media: .sticker(file: file, emoji: emojis)) + case .upload, .send: + let file = stickerFile(resource: resource, thumbnailResource: file.previewRepresentations.first?.resource, size: resource.size ?? 0, dimensions: dimensions, duration: self.preferredStickerDuration(), isVideo: isVideo) +``` + +with: + +```swift + case let .complete(resource, _): + let rawResource = resource._asResource() + let navigationController = self.effectiveNavigationController as? NavigationController + + let result: MediaEditorScreenImpl.Result + switch action { + case .update: + result = MediaEditorScreenImpl.Result(media: .sticker(file: file, emoji: emojis)) + case .upload, .send: + let file = stickerFile(resource: rawResource, thumbnailResource: file.previewRepresentations.first?.resource, size: rawResource.size ?? 0, dimensions: dimensions, duration: self.preferredStickerDuration(), isVideo: isVideo) +``` + +Two changes: introduce `let rawResource = resource._asResource()` at the top of the `case let .complete(resource, _):` block, and use `rawResource` at both the `resource:` argument and the `size: rawResource.size ?? 0` read. (`EngineMediaResource` does not expose `.size`; only the raw `MediaResource` does.) + +- [ ] **Step 7: Scan for any missed downstream use** + +Run inside the repo: + +```bash +sed -n '8080,8200p' submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift | grep -nE "\bresource\b" +``` + +Skim the output. Every reference to the destructured `resource` inside the nested block (lines ~8084–8190) should either be the new `EngineMediaResource`-typed local or a wrapped/unwrapped form. If you spot a use that would still expect `CloudDocumentMediaResource`-specific members or raw `MediaResource` without the unwrap, stop and report. + +--- + +## Task 6: Full build and commit C1 + +- [ ] **Step 1: Run the full project build** + +Run the build command from the header. Expected: clean success. + +Typical failure modes and fixes (do them inline — do not split into another commit): + +- **"cannot convert value of type 'Peer' to expected argument type 'EnginePeer'"** — a call site was missed or the wrap is misplaced. +- **"value of type 'EngineMediaResource' has no member 'size'"** — Task 5 Step 6 wasn't applied (or similar `.size`/`.id.stringRepresentation`/`.isEqual` access on `EngineMediaResource`). +- **"cannot convert value of type 'EngineMediaResource' to expected argument type 'TelegramMediaResource'"** — an `._asResource()` is missing at a `stickerFile(...)` or `.standalone(resource:)` call. +- **"reference to enum case 'UploadStickerStatus.complete' requires that 'CloudDocumentMediaResource' conform to 'something'"** — a `.complete(...)` construction site wasn't migrated to pass `EngineMediaResource`. + +Re-run the build after each fix. + +- [ ] **Step 2: Stage the 4 files** + +```bash +git add \ + submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift \ + submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift \ + submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +``` + +- [ ] **Step 3: Verify diff scope** + +```bash +git diff --staged --stat +``` + +Expected: exactly 4 files staged, with MediaEditorScreen having the largest diff (~8 line changes), ImportStickers ~2, TelegramEngineStickers ~2, ImportStickerPackController ~2. + +- [ ] **Step 4: Commit** + +```bash +git commit -m "$(cat <<'EOF' +TelegramEngine.Stickers.uploadSticker: migrate to EnginePeer + EngineMediaResource + +Public facade and UploadStickerStatus.complete payload now use +EnginePeer and EngineMediaResource instead of raw Peer / MediaResource +/ CloudDocumentMediaResource. _internal_uploadSticker stays on raw +Postbox types with one inline EngineMediaResource(uploadedResource) +construction at the .complete result site. + +Both call sites (ImportStickerPackController, MediaEditorScreen) +updated atomically in the same commit. + +Wave-4 of the Postbox -> TelegramEngine refactor. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 5: Verify branch state** + +```bash +git log --oneline master..HEAD +``` + +Expected (newest at top): + +- ` TelegramEngine.Stickers.uploadSticker: migrate to EnginePeer + EngineMediaResource` +- `b6392bce7c docs(spec): wave-4 enumerate MediaEditorScreen downstream edits` +- `59a01b0d4d docs(spec): wave-4 TelegramEngine.Stickers.uploadSticker facade migration` + +--- + +## Task 7: Update CLAUDE.md tally and commit C2 + +- [ ] **Step 1: Add Wave 4 outcome subsection** + +Open `CLAUDE.md`. Find the "Wave 3 outcome (2026-04-18)" section (currently around line 96 onward). Insert a new subsection **after** Wave 3's outcome block and **before** "### Modules currently free of `import Postbox` (running tally)": + +```markdown +### Wave 4 outcome (2026-04-18) + +1 `TelegramEngine` facade migrated in place to `EnginePeer` + `EngineMediaResource` (signatures changed; `_internal_uploadSticker` keeps raw `Peer`/`MediaResource`): + +- `TelegramEngine.Stickers.uploadSticker(peer: Peer → EnginePeer, resource: MediaResource → EngineMediaResource, thumbnail: MediaResource? → EngineMediaResource?, …)` + +1 public enum payload migrated: `UploadStickerStatus.complete(CloudDocumentMediaResource, String)` → `.complete(EngineMediaResource, String)`. The internal `_internal_uploadSticker` constructs `EngineMediaResource(uploadedResource)` at the result site — a narrow, spec-allowed one-line deviation from "internal Postbox-facing stays raw", taken to keep `UploadStickerStatus` as a single public enum. + +2 call sites migrated atomically with the facade: +- `submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift:91` +- `submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift:8099` (plus ~5 cascading sites in the same enclosing block for the new `UploadStickerStatus.complete` payload) + +No module becomes Postbox-free in this wave (both caller files import Postbox for unrelated reasons). + +Plan: `docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-4.md` +``` + +- [ ] **Step 2: Remove the `uploadSticker` entry from "Known future-wave candidates"** + +Still in `CLAUDE.md`, find the "Known future-wave candidates" list and delete this bullet (currently around line 143): + +```markdown +- `TelegramEngine.Stickers.uploadSticker(peer: Peer, resource: MediaResource, thumbnail: MediaResource?, …)` — same MediaResource migration as wave 2, plus `peer: Peer` which would naturally migrate to `EnginePeer` at the same time. Self-contained to a small number of call sites. +``` + +Do not touch the other bullets in that list. + +- [ ] **Step 3: Commit** + +```bash +git add CLAUDE.md +git commit -m "$(cat <<'EOF' +CLAUDE.md: record wave-4 outcome + +Documents the uploadSticker facade migration + UploadStickerStatus +payload change; removes uploadSticker from the future-wave candidates +list. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Success criteria + +- `TelegramEngine.Stickers.uploadSticker`'s public signature references neither `Peer` nor `MediaResource` nor `CloudDocumentMediaResource`. +- `UploadStickerStatus.complete`'s payload is `(EngineMediaResource, String)`. +- `_internal_uploadSticker`'s signature is unchanged (still raw `Peer` / `MediaResource`). +- Full build succeeds in `debug_sim_arm64`. +- The two call sites (`ImportStickerPackController`, `MediaEditorScreen`) and the cascading sites within MediaEditorScreen's nested block compile against the new types. +- `CLAUDE.md` has a "Wave 4 outcome (2026-04-18)" subsection; the `uploadSticker` bullet is gone from "Known future-wave candidates". +- Branch `refactor/postbox-to-engine-wave-4` contains 4 commits above `master`: 2 docs (spec + spec fix), 1 code (C1), 1 tally (C2). diff --git a/docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-5.md b/docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-5.md new file mode 100644 index 0000000000..9b77640185 --- /dev/null +++ b/docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-5.md @@ -0,0 +1,381 @@ +# Postbox → TelegramEngine Wave 5 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate `uploadSecureIdFile`'s public surface to `(context:, engine: TelegramEngine, resource: EngineMediaResource)`, refactor `SecureIdVerificationDocumentsContext` to take `engine: TelegramEngine` in place of raw `Postbox` + `Network`, and drop `import Postbox` from that caller module. Land as one atomic code commit + one CLAUDE.md tally commit on branch `refactor/postbox-to-engine-wave-5`. + +**Architecture:** Three files land together in C1 because the facade signature change, the caller class's stored-property change, and the one instantiation site are mutually breaking. The facade body inside TelegramCore continues to access raw Postbox types via `engine.account.postbox` / `engine.account.network` — CLAUDE.md's "internal Postbox-facing stays raw" rule applies to the body, while the public signature is the clean surface. C2 updates the CLAUDE.md tally and removes the wave-5-named bullet from "Known future-wave candidates". + +**Tech Stack:** Swift / Bazel. No unit tests by repo policy — verification is a full project build. + +**Spec:** [docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-5-design.md](docs/superpowers/specs/2026-04-18-postbox-to-telegramengine-wave-5-design.md) + +**Build command** (use for every "full build" step): + +```bash +source ~/.zshrc 2>/dev/null; PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber 1 --configuration debug_sim_arm64 +``` + +The `source ~/.zshrc` prefix is required because `TELEGRAM_CODESIGNING_GIT_PASSWORD` lives in `~/.zshrc` and the bash tool does not source shell config by default. For a long-running build, prefer `run_in_background: true` from the controller session (subagent-spawned background builds orphan when the subagent's shell terminates). + +--- + +## Task 1: Pre-flight re-verification + +No code changes. Confirms the inventory hasn't drifted. + +- [ ] **Step 1: Re-grep `uploadSecureIdFile` call sites** + +```bash +grep -rnE "uploadSecureIdFile\(" submodules --include="*.swift" | grep -v "/SecureId/" +``` + +Expected: exactly 1 match — `submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift:43`. If the count or file has drifted, stop and revise the plan. + +- [ ] **Step 2: Re-grep `SecureIdVerificationDocumentsContext(...)` instantiation sites** + +```bash +grep -rnE "SecureIdVerificationDocumentsContext\(" submodules --include="*.swift" | grep -v "final class SecureIdVerificationDocumentsContext" +``` + +Expected: exactly 1 match — `submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift:2172`. If drift, stop. + +- [ ] **Step 3: Confirm `AccountContext.engine` protocol requirement** + +```bash +grep -nE "var engine: TelegramEngine" submodules/AccountContext/Sources/AccountContext.swift +``` + +Expected: one line matching `var engine: TelegramEngine { get }` (the protocol requirement). This confirms `self.context.engine` will be available at the instantiation site in Task 4. + +- [ ] **Step 4: Confirm `info.resource` type** + +```bash +grep -nE "let resource:" submodules/PassportUI/Sources/SecureIdVerificationDocument.swift +``` + +Expected: two matches, both showing `resource: TelegramMediaResource`. Confirms `EngineMediaResource(info.resource)` will compile (the `EngineMediaResource(_:)` init takes `MediaResource`, which `TelegramMediaResource` inherits). + +--- + +## Task 2: Migrate `uploadSecureIdFile`'s public facade and body + +No build; no commit. Tasks 2–4 share one atomic commit in Task 5. + +**File:** `submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift` + +- [ ] **Step 1: Replace the function signature and body** + +Find the `uploadSecureIdFile` function (currently starts at line 90). Replace the entire function (from `public func uploadSecureIdFile` through its closing `}`) with this version: + +```swift +public func uploadSecureIdFile(context: SecureIdAccessContext, engine: TelegramEngine, resource: EngineMediaResource) -> Signal { + return engine.account.postbox.mediaBox.resourceData(resource._asResource()) + |> mapError { _ -> UploadSecureIdFileError in + } + |> mapToSignal { next -> Signal in + if !next.complete { + return .complete() + } + + guard let data = try? Data(contentsOf: URL(fileURLWithPath: next.path)) else { + return .fail(.generic) + } + + guard let encryptedData = encryptedSecureIdFile(context: context, data: data) else { + return .fail(.generic) + } + + return multipartUpload(network: engine.account.network, postbox: engine.account.postbox, source: .data(encryptedData.data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + |> mapError { _ -> UploadSecureIdFileError in + return .generic + } + |> mapToSignal { result -> Signal in + switch result { + case let .progress(value): + return .single(.progress(value)) + case let .inputFile(.inputFile(fileData)): + return .single(.result(UploadedSecureIdFile(id: fileData.id, parts: fileData.parts, md5Checksum: fileData.md5Checksum, fileHash: encryptedData.hash, encryptedSecret: encryptedData.encryptedSecret), encryptedData.data)) + default: + return .fail(.generic) + } + } + } +} +``` + +Changes from the original: + +- Signature: `(context: SecureIdAccessContext, postbox: Postbox, network: Network, resource: MediaResource)` → `(context: SecureIdAccessContext, engine: TelegramEngine, resource: EngineMediaResource)`. +- Line 1 of body: `postbox.mediaBox.resourceData(resource)` → `engine.account.postbox.mediaBox.resourceData(resource._asResource())`. +- Inside the `mapToSignal`: `multipartUpload(network: network, postbox: postbox, ...)` → `multipartUpload(network: engine.account.network, postbox: engine.account.postbox, ...)`. + +No other file in `TelegramCore/Sources/TelegramEngine/SecureId/` is touched. No imports change inside `UploadSecureIdFile.swift` — it continues to `import Foundation`, `import Postbox`, `import MtProtoKit`, `import SwiftSignalKit`, which remain correct (the body still uses raw Postbox types via `engine.account.postbox`). + +--- + +## Task 3: Migrate `SecureIdVerificationDocumentsContext` + +No build; no commit. + +**File:** `submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift` + +- [ ] **Step 1: Drop `import Postbox`** + +Replace the import block at the top (lines 1–4): + +```swift +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit +``` + +with: + +```swift +import Foundation +import TelegramCore +import SwiftSignalKit +``` + +Only `Postbox` is removed. The three remaining imports stay. + +- [ ] **Step 2: Replace stored properties** + +Find the `final class SecureIdVerificationDocumentsContext` block (starting around line 18). Replace lines 20–21: + +```swift + private let postbox: Postbox + private let network: Network +``` + +with: + +```swift + private let engine: TelegramEngine +``` + +- [ ] **Step 3: Update the constructor** + +Replace the `init` (lines 26–31): + +```swift + init(postbox: Postbox, network: Network, context: SecureIdAccessContext, update: @escaping (Int64, SecureIdVerificationLocalDocumentState) -> Void) { + self.postbox = postbox + self.network = network + self.context = context + self.update = update + } +``` + +with: + +```swift + init(engine: TelegramEngine, context: SecureIdAccessContext, update: @escaping (Int64, SecureIdVerificationLocalDocumentState) -> Void) { + self.engine = engine + self.context = context + self.update = update + } +``` + +- [ ] **Step 4: Update the `uploadSecureIdFile` call inside `stateUpdated`** + +Find line 43, which currently reads: + +```swift + disposable.set((uploadSecureIdFile(context: self.context, postbox: self.postbox, network: self.network, resource: info.resource) +``` + +Replace with: + +```swift + disposable.set((uploadSecureIdFile(context: self.context, engine: self.engine, resource: EngineMediaResource(info.resource)) +``` + +Two changes: +- `postbox: self.postbox, network: self.network` → `engine: self.engine`. +- `resource: info.resource` → `resource: EngineMediaResource(info.resource)`. + +No other line in this file changes. The `DocumentContext` inner class is untouched. The `stateUpdated` method's outer structure is untouched. + +--- + +## Task 4: Update the instantiation site + +No build; no commit. + +**File:** `submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift` + +- [ ] **Step 1: Update line 2172** + +Find line 2172, which currently reads: + +```swift + self.uploadContext = SecureIdVerificationDocumentsContext(postbox: self.context.account.postbox, network: self.context.account.network, context: self.secureIdContext, update: { id, state in +``` + +Replace with: + +```swift + self.uploadContext = SecureIdVerificationDocumentsContext(engine: self.context.engine, context: self.secureIdContext, update: { id, state in +``` + +Two removed arguments (`postbox:`, `network:`) collapse into one new argument (`engine:`). The closure body inside `update: { id, state in ... }` is unchanged. + +No other line in this file changes. The file continues to `import Postbox` for unrelated reasons — this is expected, do not remove. + +--- + +## Task 5: Full build and commit C1 + +- [ ] **Step 1: Run the full project build** + +Run the build command from the header. Expected: clean success. + +Typical failure modes and fixes (do them inline — do not split): + +- **"cannot convert value of type 'Postbox' to expected argument type 'TelegramEngine'"** — a call site was missed. Re-grep both `uploadSecureIdFile(` and `SecureIdVerificationDocumentsContext(` across the repo. +- **"cannot convert value of type 'MediaResource' to expected argument type 'EngineMediaResource'"** — Task 3 Step 4's `EngineMediaResource(info.resource)` wrap was missed. +- **"use of unresolved identifier 'Network'"** or **"use of unresolved identifier 'Postbox'"** inside `SecureIdVerificationDocumentsContext.swift`** — Tasks 3 Steps 2–3 or 4 weren't fully applied. +- **"missing argument for parameter 'engine'"** — the Task 4 call site wasn't updated. + +Re-run the build after each fix. + +- [ ] **Step 2: Stage the three files** + +```bash +git add \ + submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift \ + submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift \ + submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift +``` + +- [ ] **Step 3: Verify diff scope** + +```bash +git diff --staged --stat +``` + +Expected: exactly 3 files staged. Approximate line changes: +- `UploadSecureIdFile.swift`: ~3 lines (signature + 2 body sites). +- `SecureIdVerificationDocumentsContext.swift`: ~8 lines (1 import removed, stored props, constructor, call site). +- `SecureIdDocumentFormControllerNode.swift`: 1 line. + +- [ ] **Step 4: Commit C1** + +```bash +git commit -m "$(cat <<'EOF' +SecureId: migrate uploadSecureIdFile + context to TelegramEngine + +uploadSecureIdFile's public signature now takes engine: TelegramEngine +and resource: EngineMediaResource instead of raw postbox: Postbox + +network: Network + MediaResource. The function body accesses raw +Postbox types via engine.account.postbox / engine.account.network +(internal Postbox-facing layer stays raw per CLAUDE.md). + +SecureIdVerificationDocumentsContext refactored in lockstep: stores +engine: TelegramEngine instead of raw postbox + network, drops +import Postbox. The one instantiation site in +SecureIdDocumentFormControllerNode updates to pass engine: +self.context.engine. + +Wave-5 of the Postbox -> TelegramEngine refactor; completes the last +explicitly-named future-wave candidate. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 5: Verify branch state** + +```bash +git log --oneline master..HEAD +``` + +Expected (newest at top): +- ` SecureId: migrate uploadSecureIdFile + context to TelegramEngine` +- `b7a1a5dfb0 docs(spec): wave-5 uploadSecureIdFile facade + SecureId context migration` + +--- + +## Task 6: Update CLAUDE.md tally and commit C2 + +- [ ] **Step 1: Add `SecureIdVerificationDocumentsContext` to the Postbox-free tally** + +Open `CLAUDE.md`. Find the "Modules currently free of `import Postbox` (running tally)" section. Add `- SecureIdVerificationDocumentsContext (wave 5)` as the last bullet in the list, immediately after `- SaveToCameraRoll (wave 3)`: + +```markdown +- `MapResourceToAvatarSizes` (wave 2) +- `SaveToCameraRoll` (wave 3) +- `SecureIdVerificationDocumentsContext` (wave 5) +``` + +- [ ] **Step 2: Add a "Wave 5 outcome" subsection** + +Still in `CLAUDE.md`, find the "Wave 4 outcome (2026-04-18)" block. Insert a new "Wave 5 outcome" subsection **after** Wave 4 and **before** "Modules currently free of `import Postbox`": + +```markdown +### Wave 5 outcome (2026-04-18) + +Completes the last explicitly-named future-wave candidate from the wave-2 final review. + +`uploadSecureIdFile(context: SecureIdAccessContext, postbox: Postbox, network: Network, resource: MediaResource)` migrated in place to `(context:, engine: TelegramEngine, resource: EngineMediaResource)`. Function body accesses raw Postbox types via `engine.account.postbox` / `engine.account.network` (internal Postbox-facing layer stays raw per the standing rule). + +1 consumer submodule fully de-Postboxed: `SecureIdVerificationDocumentsContext` (PassportUI/Sources). Signature changed from `(postbox: Postbox, network: Network, context: SecureIdAccessContext, update: ...)` to `(engine: TelegramEngine, context: SecureIdAccessContext, update: ...)`; stored props collapsed into a single `engine: TelegramEngine` field. One instantiation site updated in the same commit. + +After this wave, the "Known future-wave candidates" list contains only the 4 permanently-blocked classes conforming to `TelegramMediaResource`. + +Plan: `docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-5.md` +``` + +- [ ] **Step 3: Remove the `uploadSecureIdFile` bullet from "Known future-wave candidates"** + +Still in `CLAUDE.md`, find the "Known future-wave candidates" list. Delete this bullet entirely: + +```markdown +- `submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift: public func uploadSecureIdFile(…, postbox: Postbox, …, resource: MediaResource)` — rule-2-sensitive (umbrella-type leak). Needs a paired wave with its caller(s). +``` + +Do not touch the remaining bullet about permanently-blocked classes. + +- [ ] **Step 4: Commit C2** + +```bash +git add CLAUDE.md +git commit -m "$(cat <<'EOF' +CLAUDE.md: record wave-5 outcome + +Adds SecureIdVerificationDocumentsContext to the Postbox-free module +tally, documents the uploadSecureIdFile facade migration, and removes +the uploadSecureIdFile bullet from "Known future-wave candidates". +After this wave, the candidate list contains only the 4 permanently- +blocked TelegramMediaResource-conforming classes. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 5: Verify final branch state** + +```bash +git log --oneline master..HEAD +``` + +Expected: +- ` CLAUDE.md: record wave-5 outcome` +- ` SecureId: migrate uploadSecureIdFile + context to TelegramEngine` +- `b7a1a5dfb0 docs(spec): wave-5 uploadSecureIdFile facade + SecureId context migration` + +--- + +## Success criteria + +- `uploadSecureIdFile`'s public signature references neither `Postbox`, `Network`, nor `MediaResource`. +- `SecureIdVerificationDocumentsContext.swift` does not contain `import Postbox`. +- Full build succeeds in `debug_sim_arm64`. +- `grep -l "import Postbox" submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift` returns no match. +- `CLAUDE.md`'s "Known future-wave candidates" list no longer mentions `uploadSecureIdFile`; the Postbox-free running tally includes `SecureIdVerificationDocumentsContext (wave 5)`. +- Branch `refactor/postbox-to-engine-wave-5` contains 3 commits above `master`: 1 doc (spec) + 1 code (C1) + 1 tally (C2). diff --git a/docs/superpowers/plans/2026-04-19-postbox-to-telegramengine-wave-6.md b/docs/superpowers/plans/2026-04-19-postbox-to-telegramengine-wave-6.md new file mode 100644 index 0000000000..2693f19732 --- /dev/null +++ b/docs/superpowers/plans/2026-04-19-postbox-to-telegramengine-wave-6.md @@ -0,0 +1,374 @@ +# Postbox → TelegramEngine Wave 6 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Speculatively drop `import Postbox` from every consumer file where a plain `^import Postbox$` line appears, run a full project build, restore the import on files that fail to compile, iterate up to 3 times, commit surviving drops as one atomic commit. Then land a CLAUDE.md update with the outcome and permanent methodology guidance. + +**Architecture:** Two commits on branch `refactor/postbox-to-engine-wave-6`. C1 is the atomic batch deletion whose diff is N single-line removals (build-verified). C2 is a docs update that (a) records the outcome and (b) codifies the sweep methodology under "Wave-selection guidance" so future sweeps can be triggered directly. The project build is the safety net — anything that compiles after restoration is definitionally safe. + +**Tech Stack:** Swift / Bazel. No unit tests — verification is a full project build. + +**Spec:** [docs/superpowers/specs/2026-04-19-postbox-to-telegramengine-wave-6-design.md](docs/superpowers/specs/2026-04-19-postbox-to-telegramengine-wave-6-design.md) + +**Build command** (use for every "full build" step): + +```bash +source ~/.zshrc 2>/dev/null; PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber 1 --configuration debug_sim_arm64 +``` + +For background execution (recommended given build length), use `run_in_background: true` from the controller session. Do not let a subagent spawn the build — when the subagent returns the process orphans. The controller owns every build invocation in this wave. + +--- + +## Task 1: Generate and record the candidate list + +Read-only setup. No code changes yet. + +- [ ] **Step 1: Generate the candidate list** + +```bash +grep -rl "^import Postbox$" submodules --include="*.swift" \ + | grep -vE "/(TelegramCore|Postbox|TelegramApi)/" \ + | sort > /tmp/wave-6-candidates.txt +wc -l /tmp/wave-6-candidates.txt +``` + +Expected: a count somewhere between 100 and 400. Record the exact number — call it `N_candidates`. If the count is outside that range, stop and investigate: either the grep is too narrow (missing `@_exported` etc. ought to be rare) or too broad (accidentally matching TelegramCore). + +- [ ] **Step 2: Snapshot baseline** + +The snapshot is implicit: every candidate file is at branch HEAD, so `git checkout -- ` always restores the pre-sweep content. Verify the working tree is clean: + +```bash +git status --short | grep -v '^??' | grep -v sourcekit-bazel-bsp +``` + +Expected: empty output. (The `sourcekit-bazel-bsp` submodule shows as modified across the whole repo; that's pre-existing and orthogonal.) If there are any other unstaged changes, commit or stash them before proceeding. + +- [ ] **Step 3: Confirm branch and HEAD** + +```bash +git branch --show-current +git log --oneline -3 +``` + +Expected: +- current branch: `refactor/postbox-to-engine-wave-6` +- top commit: the wave-6 spec commit. + +--- + +## Task 2: Speculative drop pass + +Mutates all candidate files. No commit yet. + +- [ ] **Step 1: Drop `import Postbox` from every candidate** + +```bash +while IFS= read -r f; do + /usr/bin/sed -i '' '/^import Postbox$/d' "$f" +done < /tmp/wave-6-candidates.txt +``` + +macOS `sed` requires the `''` after `-i` (BSD flavor). + +- [ ] **Step 2: Verify every candidate had exactly one line removed** + +```bash +git diff --stat | wc -l +``` + +Expected: `N_candidates + 1` (one line per file in `--stat` output, plus the summary line). + +```bash +git diff --stat | awk '{print $3}' | grep -v deletion | head -5 +``` + +Expected: each shown entry is `1` (one insertion, zero counted since all are single-line deletes). If any file shows more than 1 line changed, something went wrong — investigate. + +- [ ] **Step 3: Confirm no `@_exported` lines were accidentally touched** + +```bash +grep -r "@_exported import Postbox" submodules --include="*.swift" | head -5 +``` + +If this returns results, those lines must still be intact — verify. The regex used in Step 1 only matches bare `^import Postbox$`, so `@_exported import Postbox` is untouched. This step is a sanity check. + +--- + +## Task 3: Iteration 1 — first build, parse errors, restore failing files + +- [ ] **Step 1: Run the full project build (iteration 1)** + +Run the build command from the header. Expected: many errors — this is by design. Capture stderr to the build output file. + +Watch the tail of the output file for either `INFO: Build completed successfully` (rare: means zero imports were needed) or a cascade of compile errors (expected). + +- [ ] **Step 2: Extract failing files from the build output** + +```bash +BUILD_OUT=/private/tmp/claude-501/-Users-ali-build-telegram-telegram-ios/5d9b3268-5c9f-45fc-bd4e-87cac5361498/tasks/.output +grep -E "^submodules/.*\.swift:[0-9]+:[0-9]+: error:" "$BUILD_OUT" \ + | awk -F: '{print $1}' \ + | sort -u > /tmp/wave-6-failing.txt +wc -l /tmp/wave-6-failing.txt +``` + +The task-id comes from the background Bash tool's output file. Substitute the actual `/private/tmp/claude-501/.../.output` path. + +Sanity-check the content: + +```bash +head -3 /tmp/wave-6-failing.txt +``` + +Every line should be a path under `submodules/` that appears in `/tmp/wave-6-candidates.txt`. If any line is from `TelegramCore`, `Postbox`, or `TelegramApi`, the sweep has cascaded beyond the candidate set — halt and investigate. + +- [ ] **Step 3: Validate error types** + +```bash +grep -E "^submodules/.*\.swift:[0-9]+:[0-9]+: error:" "$BUILD_OUT" \ + | head -10 +``` + +Expected error patterns: +- `cannot find type 'X' in scope` +- `use of unresolved identifier 'X'` +- `cannot find 'X' in scope` +- `reference to invalid associated type 'X' of type 'Y'` (occasional) + +If you see `no such module 'Postbox'` or errors unrelated to missing Postbox symbols (e.g., codesign failures, Bazel graph errors), halt and investigate — those are not the sweep's signal. + +- [ ] **Step 4: Restore `import Postbox` on failing files** + +```bash +while IFS= read -r f; do + git checkout -- "$f" +done < /tmp/wave-6-failing.txt +``` + +- [ ] **Step 5: Verify restoration** + +```bash +git diff --stat | wc -l +``` + +Expected: `N_candidates - N_failing + 1` lines in `--stat` output (one per still-modified file plus summary). The count should be lower than Task 2 Step 2's count by exactly `N_failing`. + +--- + +## Task 4: Iteration 2 — rebuild, parse new errors, restore + +- [ ] **Step 1: Run the full project build (iteration 2)** + +Run the build command again. Expected: ideally clean success. If errors persist, it's because restoring some files in iteration 1 removed a symbol that another file (still in the candidate set with import dropped) needed transitively via that symbol's module-level re-export. + +Watch for `INFO: Build completed successfully`. If found, proceed to Task 6 (skipping Task 5). If errors persist, continue with Step 2. + +- [ ] **Step 2: Extract failing files** + +```bash +BUILD_OUT=/private/tmp/claude-501/-Users-ali-build-telegram-telegram-ios/5d9b3268-5c9f-45fc-bd4e-87cac5361498/tasks/.output +grep -E "^submodules/.*\.swift:[0-9]+:[0-9]+: error:" "$BUILD_OUT" \ + | awk -F: '{print $1}' \ + | sort -u > /tmp/wave-6-failing-2.txt +wc -l /tmp/wave-6-failing-2.txt +``` + +- [ ] **Step 3: Restore** + +```bash +while IFS= read -r f; do + git checkout -- "$f" +done < /tmp/wave-6-failing-2.txt +``` + +- [ ] **Step 4: Decision point** + +If `wc -l /tmp/wave-6-failing-2.txt` is 0, the iteration-2 rebuild actually succeeded — proceed to Task 6. If it's greater than 0, proceed to Task 5 for iteration 3. + +--- + +## Task 5: Iteration 3 — final rebuild + +- [ ] **Step 1: Run the full project build (iteration 3)** + +Run the build command again. If this iteration does not complete successfully, the sweep has failed the stability test. + +- [ ] **Step 2: Clean-success check** + +Expected: `INFO: Build completed successfully`. + +If successful, proceed to Task 6. + +If a third iteration of errors appears, **abandon the wave**: + +```bash +git checkout -- . +git status --short +``` + +Working tree should now be clean (modulo the pre-existing sourcekit-bazel-bsp submodule marker). Do not commit. Skip Task 6. Jump straight to an updated Task 7 that records the failed attempt in CLAUDE.md instead of a success outcome, and document what kind of errors surfaced so a future attempt can plan around them. + +--- + +## Task 6: Commit C1 — build-verified batch drop + +- [ ] **Step 1: Compute the final count** + +```bash +git diff --stat | tail -1 +``` + +Expected: something like ` N files changed, 0 insertions(+), N deletions(-)` where N is the number of files that survived the sweep. Record this count as `N_dropped`. + +- [ ] **Step 2: Spot-check a few diffs** + +```bash +git diff | grep -E "^-import Postbox$" | wc -l +``` + +Expected: `N_dropped` (every surviving diff is a single-line `-import Postbox` removal). + +```bash +git diff | grep -E "^\+" | grep -v "^+++" | head +``` + +Expected: no output. (The sweep only removes lines; it never adds any.) + +- [ ] **Step 3: Stage all changes** + +```bash +git add -u +``` + +`-u` stages only files that are already tracked and modified. No need to enumerate each file — the sweep touched many and they're all known to git. + +- [ ] **Step 4: Commit** + +```bash +N_DROPPED=$(git diff --staged --stat | tail -1 | awk '{print $1}') +git commit -m "$(cat < +EOF +)" +``` + +- [ ] **Step 5: Verify branch state** + +```bash +git log --oneline master..HEAD +``` + +Expected: +- ` Drop unused import Postbox from N consumer files` +- `816e7699ec docs(spec): wave-6 unused import Postbox batch sweep` + +--- + +## Task 7: CLAUDE.md — record outcome and add permanent sweep methodology + +- [ ] **Step 1: Add Wave 6 outcome subsection** + +Open `CLAUDE.md`. Find the "Wave 5 outcome (2026-04-19)" block. Insert a new "Wave 6 outcome (2026-04-19)" subsection immediately after Wave 5 and before "Modules currently free of `import Postbox`": + +```markdown +### Wave 6 outcome (2026-04-19) + +First unused-import sweep. Ran the speculative-drop + build-verify methodology (see "Unused-import sweeps" under Wave-selection guidance): dropped `import Postbox` from every consumer file where a plain `^import Postbox$` appeared (out of ~N_CANDIDATES candidates), rebuilt, restored the import on failures, iterated. N_DROPPED drops survived. + +No behavior change; zero facade migrations in this wave. Running tally updated for any modules whose last `import Postbox`-bearing file was swept (see the per-module list below). + +Plan: `docs/superpowers/plans/2026-04-19-postbox-to-telegramengine-wave-6.md` +``` + +Replace `N_CANDIDATES` and `N_DROPPED` with the actual numbers from Task 1 Step 1 and Task 6 Step 1. If the wave was abandoned (see Task 5 Step 2), replace the outcome text with a failed-attempt description instead: what iteration the sweep stalled at and what error category. + +- [ ] **Step 2: Add permanent "Unused-import sweeps" subsection under Wave-selection guidance** + +Still in `CLAUDE.md`, find the "Wave-selection guidance" block. Insert the following new subsection at the end of that block (immediately before "### Wave 1 outcome"): + +```markdown +**Unused-import sweeps are a valid wave shape.** After a round of facade migrations, consumer files accumulate `import Postbox` lines whose last semantic use was removed. Periodically sweep these: + +1. `grep -rl "^import Postbox$" submodules --include="*.swift" | grep -vE "/(TelegramCore|Postbox|TelegramApi)/"` generates the candidate list. +2. `sed -i '' '/^import Postbox$/d' ` (BSD sed) speculatively drops the import from every candidate. +3. Run the full project build. Swift compile errors (`::: error: cannot find type 'X'`) identify files that need the import restored via `git checkout -- `. +4. Rebuild. Iterate up to 3 times. Only restore files from the candidate set — if errors surface in `TelegramCore`, `Postbox`, or `TelegramApi`, halt and investigate (cascading breakage). +5. Commit the surviving drops as one atomic commit. + +Re-run this after every 2–3 facade-migration waves. First run: wave 6. +``` + +- [ ] **Step 3: Update "Modules currently free of `import Postbox`" tally** + +For each module in `submodules/` that has **no** remaining `import Postbox` after this wave, add a bullet under "Modules currently free of `import Postbox` (running tally)". Determine this list with: + +```bash +for d in submodules/*/; do + mod=$(basename "$d") + if [ -d "$d/Sources" ]; then + count=$(grep -rlE "^(@_exported )?import Postbox" "$d/Sources" --include="*.swift" 2>/dev/null | wc -l) + if [ "$count" -eq 0 ]; then + # Check this module isn't already in CLAUDE.md's tally + if ! grep -qF "\`$mod\`" CLAUDE.md; then + echo "$mod" + fi + fi + fi +done +``` + +Each printed module becomes a new bullet like `- \`\` (wave 6)` in the list. + +If the output is empty, no new module-level additions — individual file drops across multiple mixed modules aren't tally-eligible. That's fine, the Wave-6 outcome subsection still records the raw count. + +- [ ] **Step 4: Commit** + +```bash +git add CLAUDE.md +git commit -m "$(cat <<'EOF' +CLAUDE.md: record wave-6 outcome and unused-import-sweep methodology + +Adds the wave-6 outcome subsection with the candidate/drop counts, +documents the speculative-drop + build-verify methodology as +permanent guidance under wave-selection so future waves can re-run +the sweep directly, and updates the Postbox-free running tally for +any modules whose last import Postbox file was swept in this wave. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 5: Verify final branch state** + +```bash +git log --oneline master..HEAD +``` + +Expected (newest first): +- ` CLAUDE.md: record wave-6 outcome and unused-import-sweep methodology` +- ` Drop unused import Postbox from N consumer files` +- `816e7699ec docs(spec): wave-6 unused import Postbox batch sweep` + +--- + +## Success criteria + +- At least one `import Postbox` line has been removed from at least one consumer file, build-verified. +- Full build succeeds in `debug_sim_arm64`. +- `CLAUDE.md` has a "Wave 6 outcome (2026-04-19)" subsection with actual numeric results. +- `CLAUDE.md`'s "Wave-selection guidance" section has a new permanent "Unused-import sweeps" bullet list that describes the methodology for future re-runs. +- `CLAUDE.md`'s "Modules currently free of `import Postbox`" running tally includes any newly-fully-clean modules (if any). +- Branch `refactor/postbox-to-engine-wave-6` contains 3 commits above `master`: 1 doc (spec) + 1 code (C1 batch drop) + 1 tally (C2). diff --git a/docs/superpowers/plans/2026-04-20-decrypt-match-python-port.md b/docs/superpowers/plans/2026-04-20-decrypt-match-python-port.md new file mode 100644 index 0000000000..702cbe807a --- /dev/null +++ b/docs/superpowers/plans/2026-04-20-decrypt-match-python-port.md @@ -0,0 +1,539 @@ +# Pure-Python port of fastlane match `decrypt.rb` — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the Ruby-based fastlane match decryption (`build-system/decrypt.rb` shelled from `BuildConfiguration.py:110`) with a self-contained Python 3 implementation using only the standard library. + +**Architecture:** Rewrite `build-system/Make/DecryptMatch.py` from scratch as a pure-Python AES-256 implementation. Covers V1 (CBC via `EVP_BytesToKey` with MD5→SHA256 fallback) and V2 (GCM with PBKDF2-derived key/iv/AAD + auth tag). `BuildConfiguration.py` calls the existing `decrypt_match_data(source, destination, password)` entry point directly instead of shelling out to Ruby. `decrypt.rb` is deleted. + +**Tech Stack:** Python 3 stdlib only — `hashlib` (MD5 / SHA256 / PBKDF2-HMAC), `base64`. + +--- + +## File structure + +- **Rewrite (not edit):** `build-system/Make/DecryptMatch.py` — new file replacing the broken placeholder. Single module containing: AES-256 primitives, `EVP_BytesToKey`, CBC decrypt, GCM decrypt (with GHASH + CTR), `MatchDataEncryption` dispatcher, `decrypt_match_data` public entry, `__main__` CLI. +- **Modify:** `build-system/Make/BuildConfiguration.py:103-118` — swap `os.system('ruby …')` for a direct Python call. +- **Delete:** `build-system/decrypt.rb`. + +--- + +## Task 1: Rewrite `build-system/Make/DecryptMatch.py` + +**Files:** +- Modify (rewrite): `build-system/Make/DecryptMatch.py` + +- [ ] **Step 1.1: Replace the file contents entirely** + +Overwrite `build-system/Make/DecryptMatch.py` with the following. This is the full file — no other changes to this module in later tasks. + +```python +import base64 +import hashlib + + +# FIPS-197 AES S-box and inverse S-box. +_SBOX = bytes.fromhex( + "637c777bf26b6fc53001672bfed7ab76" + "ca82c97dfa5947f0add4a2af9ca472c0" + "b7fd9326363ff7cc34a5e5f171d83115" + "04c723c31896059a071280e2eb27b275" + "09832c1a1b6e5aa0523bd6b329e32f84" + "53d100ed20fcb15b6acbbe394a4c58cf" + "d0efaafb434d338545f9027f503c9fa8" + "51a3408f929d38f5bcb6da2110fff3d2" + "cd0c13ec5f974417c4a77e3d645d1973" + "60814fdc222a908846eeb814de5e0bdb" + "e0323a0a4906245cc2d3ac629195e479" + "e7c8376d8dd54ea96c56f4ea657aae08" + "ba78252e1ca6b4c6e8dd741f4bbd8b8a" + "703eb5664803f60e613557b986c11d9e" + "e1f8981169d98e949b1e87e9ce5528df" + "8ca1890dbfe6426841992d0fb054bb16" +) + +_INV_SBOX = bytes.fromhex( + "52096ad53036a538bf40a39e81f3d7fb" + "7ce339829b2fff87348e4344c4dee9cb" + "547b9432a6c2233dee4c950b42fac34e" + "082ea16628d924b2765ba2496d8bd125" + "72f8f66486689816d4a45ccc5d65b692" + "6c704850fdedb9da5e154657a78d9d84" + "90d8ab008cbcd30af7e45805b8b34506" + "d02c1e8fca3f0f02c1afbd0301138a6b" + "3a9111414f67dcea97f2cfcef0b4e673" + "96ac7422e7ad3585e2f937e81c75df6e" + "47f11a711d29c5896fb7620eaa18be1b" + "fc563e4bc6d279209adbc0fe78cd5af4" + "1fdda8338807c731b11210592780ec5f" + "60517fa919b54a0d2de57a9f93c99cef" + "a0e03b4dae2af5b0c8ebbb3c83539961" + "172b047eba77d626e169146355210c7d" +) + +_RCON = bytes.fromhex("01020408102040801b36") + + +def _xtime(a): + return (((a << 1) ^ 0x1b) & 0xff) if (a & 0x80) else (a << 1) + + +def _gf_mul(a, b): + r = 0 + for _ in range(8): + if b & 1: + r ^= a + b >>= 1 + a = _xtime(a) + return r + + +def _key_expansion_256(key): + # AES-256: Nk=8, Nr=14, total 4 * (Nr + 1) = 60 words = 240 bytes. + assert len(key) == 32 + w = bytearray(240) + w[:32] = key + i = 32 + while i < 240: + t = bytearray(w[i - 4:i]) + if i % 32 == 0: + t = bytearray([t[1], t[2], t[3], t[0]]) + for j in range(4): + t[j] = _SBOX[t[j]] + t[0] ^= _RCON[i // 32 - 1] + elif i % 32 == 16: + for j in range(4): + t[j] = _SBOX[t[j]] + for j in range(4): + w[i + j] = w[i - 32 + j] ^ t[j] + i += 4 + return [bytes(w[r * 16:(r + 1) * 16]) for r in range(15)] + + +def _add_round_key(state, rk): + return bytes(s ^ k for s, k in zip(state, rk)) + + +def _sub_bytes(state): + return bytes(_SBOX[b] for b in state) + + +def _inv_sub_bytes(state): + return bytes(_INV_SBOX[b] for b in state) + + +# Column-major state: state[r + 4 * c], r = 0..3 (row), c = 0..3 (column). +def _shift_rows(state): + s = bytearray(state) + s[1], s[5], s[9], s[13] = s[5], s[9], s[13], s[1] + s[2], s[6], s[10], s[14] = s[10], s[14], s[2], s[6] + s[3], s[7], s[11], s[15] = s[15], s[3], s[7], s[11] + return bytes(s) + + +def _inv_shift_rows(state): + s = bytearray(state) + s[1], s[5], s[9], s[13] = s[13], s[1], s[5], s[9] + s[2], s[6], s[10], s[14] = s[10], s[14], s[2], s[6] + s[3], s[7], s[11], s[15] = s[7], s[11], s[15], s[3] + return bytes(s) + + +def _mix_columns(state): + s = bytearray(16) + for c in range(4): + a0, a1, a2, a3 = state[4 * c], state[4 * c + 1], state[4 * c + 2], state[4 * c + 3] + s[4 * c] = _xtime(a0) ^ (_xtime(a1) ^ a1) ^ a2 ^ a3 + s[4 * c + 1] = a0 ^ _xtime(a1) ^ (_xtime(a2) ^ a2) ^ a3 + s[4 * c + 2] = a0 ^ a1 ^ _xtime(a2) ^ (_xtime(a3) ^ a3) + s[4 * c + 3] = (_xtime(a0) ^ a0) ^ a1 ^ a2 ^ _xtime(a3) + return bytes(s) + + +def _inv_mix_columns(state): + s = bytearray(16) + for c in range(4): + a0, a1, a2, a3 = state[4 * c], state[4 * c + 1], state[4 * c + 2], state[4 * c + 3] + s[4 * c] = _gf_mul(a0, 0x0e) ^ _gf_mul(a1, 0x0b) ^ _gf_mul(a2, 0x0d) ^ _gf_mul(a3, 0x09) + s[4 * c + 1] = _gf_mul(a0, 0x09) ^ _gf_mul(a1, 0x0e) ^ _gf_mul(a2, 0x0b) ^ _gf_mul(a3, 0x0d) + s[4 * c + 2] = _gf_mul(a0, 0x0d) ^ _gf_mul(a1, 0x09) ^ _gf_mul(a2, 0x0e) ^ _gf_mul(a3, 0x0b) + s[4 * c + 3] = _gf_mul(a0, 0x0b) ^ _gf_mul(a1, 0x0d) ^ _gf_mul(a2, 0x09) ^ _gf_mul(a3, 0x0e) + return bytes(s) + + +def _aes_encrypt_block(block, round_keys): + state = _add_round_key(block, round_keys[0]) + for r in range(1, 14): + state = _sub_bytes(state) + state = _shift_rows(state) + state = _mix_columns(state) + state = _add_round_key(state, round_keys[r]) + state = _sub_bytes(state) + state = _shift_rows(state) + state = _add_round_key(state, round_keys[14]) + return state + + +def _aes_decrypt_block(block, round_keys): + state = _add_round_key(block, round_keys[14]) + for r in range(13, 0, -1): + state = _inv_shift_rows(state) + state = _inv_sub_bytes(state) + state = _add_round_key(state, round_keys[r]) + state = _inv_mix_columns(state) + state = _inv_shift_rows(state) + state = _inv_sub_bytes(state) + state = _add_round_key(state, round_keys[0]) + return state + + +def _evp_bytes_to_key(password, salt, hash_name, key_len=32, iv_len=16): + # OpenSSL EVP_BytesToKey with count=1, matching Ruby's + # Cipher#pkcs5_keyivgen(password, salt, 1, hash). + if isinstance(password, str): + password = password.encode('utf-8') + required = key_len + iv_len + material = b"" + prev = b"" + while len(material) < required: + h = hashlib.new(hash_name) + h.update(prev + password + salt) + prev = h.digest() + material += prev + return material[:key_len], material[key_len:key_len + iv_len] + + +def _aes_cbc_decrypt(ciphertext, key, iv): + if len(ciphertext) == 0 or len(ciphertext) % 16 != 0: + raise ValueError("V1 ciphertext length must be a non-zero multiple of 16") + round_keys = _key_expansion_256(key) + out = bytearray() + prev = iv + for i in range(0, len(ciphertext), 16): + block = ciphertext[i:i + 16] + decrypted = _aes_decrypt_block(block, round_keys) + out.extend(bytes(d ^ p for d, p in zip(decrypted, prev))) + prev = block + pad = out[-1] + if pad < 1 or pad > 16 or not all(b == pad for b in out[-pad:]): + raise ValueError("V1 PKCS#7 padding check failed") + return bytes(out[:-pad]) + + +def _ghash(h_bytes, data): + # GHASH over GF(2^128) with reduction polynomial x^128 + x^7 + x^2 + x + 1, + # using GCM's bit-reversed convention (top-bit-first when encoded as bytes). + h = int.from_bytes(h_bytes, 'big') + y = 0 + reduction = 0xe1 << 120 + for i in range(0, len(data), 16): + block = data[i:i + 16].ljust(16, b"\x00") + y ^= int.from_bytes(block, 'big') + z = 0 + v = y + for bit in range(127, -1, -1): + if (h >> bit) & 1: + z ^= v + if v & 1: + v = (v >> 1) ^ reduction + else: + v >>= 1 + y = z + return y.to_bytes(16, 'big') + + +def _aes_gcm_decrypt(ciphertext, key, iv, aad, auth_tag): + if len(iv) != 12: + raise ValueError("V2 requires a 96-bit IV") + round_keys = _key_expansion_256(key) + H = _aes_encrypt_block(b"\x00" * 16, round_keys) + j0 = iv + b"\x00\x00\x00\x01" + + plaintext = bytearray() + j0_int = int.from_bytes(j0, 'big') + mask32 = (1 << 32) - 1 + counter_high = j0_int & ~mask32 + counter_low = j0_int & mask32 + n_blocks = (len(ciphertext) + 15) // 16 + for i in range(n_blocks): + counter_low = (counter_low + 1) & mask32 + ctr_bytes = (counter_high | counter_low).to_bytes(16, 'big') + keystream = _aes_encrypt_block(ctr_bytes, round_keys) + block = ciphertext[i * 16:(i + 1) * 16] + plaintext.extend(bytes(c ^ k for c, k in zip(block, keystream[:len(block)]))) + + aad_pad = b"\x00" * ((16 - len(aad) % 16) % 16) + ct_pad = b"\x00" * ((16 - len(ciphertext) % 16) % 16) + length_block = (len(aad) * 8).to_bytes(8, 'big') + (len(ciphertext) * 8).to_bytes(8, 'big') + s = _ghash(H, aad + aad_pad + ciphertext + ct_pad + length_block) + e_j0 = _aes_encrypt_block(j0, round_keys) + computed_tag = bytes(a ^ b for a, b in zip(s, e_j0)) + if computed_tag != auth_tag: + raise ValueError("V2 GCM auth tag mismatch") + return bytes(plaintext) + + +_V1_PREFIX = b"Salted__" +_V2_PREFIX = b"match_encrypted_v2__" + + +def _decrypt_stored(stored_data, password): + if stored_data.startswith(_V2_PREFIX): + salt = stored_data[20:28] + auth_tag = stored_data[28:44] + ciphertext = stored_data[44:] + material = hashlib.pbkdf2_hmac( + 'sha256', + password.encode('utf-8'), + salt, + 10_000, + dklen=32 + 12 + 24, + ) + key = material[0:32] + iv = material[32:44] + aad = material[44:68] + return _aes_gcm_decrypt(ciphertext, key, iv, aad, auth_tag) + if stored_data.startswith(_V1_PREFIX): + salt = stored_data[8:16] + ciphertext = stored_data[16:] + try: + key, iv = _evp_bytes_to_key(password, salt, 'md5', 32, 16) + return _aes_cbc_decrypt(ciphertext, key, iv) + except Exception: + key, iv = _evp_bytes_to_key(password, salt, 'sha256', 32, 16) + return _aes_cbc_decrypt(ciphertext, key, iv) + raise ValueError("Unrecognized fastlane match payload (missing V1 'Salted__' or V2 'match_encrypted_v2__' prefix)") + + +def decrypt_match_data(source_path: str, destination_path: str, password: str): + with open(source_path, 'rb') as f: + raw = f.read() + stored_data = base64.b64decode(raw) + decrypted = _decrypt_stored(stored_data, password) + with open(destination_path, 'wb') as f: + f.write(decrypted) + + +if __name__ == '__main__': + import sys + if len(sys.argv) != 4: + print('Usage: DecryptMatch.py ') + sys.exit(1) + decrypt_match_data(source_path=sys.argv[2], destination_path=sys.argv[3], password=sys.argv[1]) +``` + +--- + +## Task 2: Smoke-test the AES-256 block primitive (FIPS-197 Appendix C.3) + +**Files:** +- No changes. One-liner shell command to validate the just-written primitive. + +- [ ] **Step 2.1: Run the FIPS-197 C.3 known-answer test** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +python3 -c " +import sys +sys.path.insert(0, 'build-system/Make') +from DecryptMatch import _key_expansion_256, _aes_encrypt_block, _aes_decrypt_block +key = bytes.fromhex('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f') +pt = bytes.fromhex('00112233445566778899aabbccddeeff') +expected = bytes.fromhex('8ea2b7ca516745bfeafc49904b496089') +rks = _key_expansion_256(key) +assert _aes_encrypt_block(pt, rks) == expected, 'encrypt failed' +assert _aes_decrypt_block(expected, rks) == pt, 'decrypt failed' +print('AES-256 FIPS-197 C.3 OK') +" +``` + +Expected output: `AES-256 FIPS-197 C.3 OK`. If this fails, the AES primitive is broken — re-read Task 1's code and fix before proceeding. + +--- + +## Task 3: Validate V2 decryption on real encrypted files + +**Files:** +- No changes. Decrypt real samples with the new Python and verify each output is a cryptographically-valid Apple-signed artifact. + +**Success criteria:** the decrypted `.mobileprovision` files verify under `openssl smime -verify` and parse as valid plists. A CMS signature covers every byte of the payload, so successful verification is equivalent to bit-exact decryption — any wrong byte anywhere would break the signature. This is a stronger check than diffing against another implementation, and it matches what `BuildConfiguration.copy_profiles_from_directory` does on every profile in the real build, so passing here means the port is production-ready. + +The encrypted repo is at `~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/encrypted/profiles/development/`. Repo password: `sluchainost` (per the hard-coded value in the file Task 1 replaced). + +> NOTE: Do not attempt a byte-for-byte comparison against `ruby build-system/decrypt.rb`. Ruby's OpenSSL binding on macOS LibreSSL 3.3.6 fails on `cipher.auth_data=` with `couldn't set additional authenticated data`, so the legacy script cannot decrypt V2 at all on current macOS. (This is likely why the build accumulated a broken aspirational Python port in the first place.) Signature verification of the Python output is the authoritative check. + +- [ ] **Step 3.1: Decrypt one sample file** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +SAMPLE=~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/encrypted/profiles/development/Development_org.telegram.TelegramInternal.BroadcastUpload.mobileprovision +python3 build-system/Make/DecryptMatch.py sluchainost "$SAMPLE" /tmp/match-py.bin +shasum -a 256 /tmp/match-py.bin +``` + +Expected: `match-py.bin` is non-empty; a sha256 is printed. + +- [ ] **Step 3.2: Verify the output is a valid Apple-signed provisioning profile** + +```bash +openssl smime -inform der -verify -noverify -in /tmp/match-py.bin | plutil -lint - +``` + +Expected: `openssl smime` prints `Verification successful` (or similar; exit code 0 is what matters), and `plutil` reports `OK`. Either failure means the decryption is corrupt — STOP and report BLOCKED with the exact openssl/plutil output. + +- [ ] **Step 3.3: Spot-check remaining V2 files decrypt without error** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +ENCRYPTED=~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/encrypted/profiles/development +for f in "$ENCRYPTED"/*.mobileprovision; do + python3 build-system/Make/DecryptMatch.py sluchainost "$f" /tmp/match-check.bin \ + && openssl smime -inform der -verify -noverify -in /tmp/match-check.bin > /dev/null 2>&1 \ + && echo "OK $(basename "$f")" \ + || echo "FAIL $(basename "$f")" +done +``` + +Expected: every line starts with `OK`. Any `FAIL` line means that file's decryption is corrupt — STOP and report BLOCKED. + +--- + +## Task 4: Commit the rewrite + +**Files:** +- Commit `build-system/Make/DecryptMatch.py` only. + +- [ ] **Step 4.1: Stage and commit** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +git add build-system/Make/DecryptMatch.py +git commit -m "$(cat <<'EOF' +DecryptMatch: pure-Python AES-256 port of decrypt.rb + +Implements fastlane match V1 (AES-256-CBC via EVP_BytesToKey with +MD5 default and SHA256 fallback) and V2 (AES-256-GCM with PBKDF2- +derived key/IV/AAD + auth tag) using only Python stdlib. Validated +by decrypting every V2 .mobileprovision in the repo and confirming +each output verifies under openssl smime + plutil -lint as a valid +Apple-signed artifact. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +Expected: commit created cleanly. + +--- + +## Task 5: Switch `BuildConfiguration.py` to the Python implementation and remove `decrypt.rb` + +**Files:** +- Modify: `build-system/Make/BuildConfiguration.py:103-118` +- Delete: `build-system/decrypt.rb` + +- [ ] **Step 5.1: Swap the call site** + +Replace lines 103-118 of `build-system/Make/BuildConfiguration.py`: + +```python +def decrypt_codesigning_directory_recursively(source_base_path, destination_base_path, password): + for file_name in os.listdir(source_base_path): + source_path = source_base_path + '/' + file_name + destination_path = destination_base_path + '/' + file_name + allowed_file_extensions = ['.mobileprovision', '.cer', '.p12'] + if os.path.isfile(source_path) and any(source_path.endswith(ext) for ext in allowed_file_extensions): + #print('Decrypting {} to {} with {}'.format(source_path, destination_path, password)) + os.system('ruby build-system/decrypt.rb "{password}" "{source_path}" "{destination_path}"'.format( + password=password, + source_path=source_path, + destination_path=destination_path + )) + #decrypt_match_data(source_path, destination_path, password) + elif os.path.isdir(source_path): + os.makedirs(destination_path, exist_ok=True) + decrypt_codesigning_directory_recursively(source_path, destination_path, password) +``` + +with: + +```python +def decrypt_codesigning_directory_recursively(source_base_path, destination_base_path, password): + for file_name in os.listdir(source_base_path): + source_path = source_base_path + '/' + file_name + destination_path = destination_base_path + '/' + file_name + allowed_file_extensions = ['.mobileprovision', '.cer', '.p12'] + if os.path.isfile(source_path) and any(source_path.endswith(ext) for ext in allowed_file_extensions): + decrypt_match_data(source_path, destination_path, password) + elif os.path.isdir(source_path): + os.makedirs(destination_path, exist_ok=True) + decrypt_codesigning_directory_recursively(source_path, destination_path, password) +``` + +- [ ] **Step 5.2: Delete the Ruby script** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +git rm build-system/decrypt.rb +``` + +- [ ] **Step 5.3: Commit** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +git add build-system/Make/BuildConfiguration.py +git commit -m "$(cat <<'EOF' +BuildConfiguration: use Python DecryptMatch, drop Ruby decrypt.rb + +Swap the os.system('ruby build-system/decrypt.rb ...') shell-out for +a direct decrypt_match_data() call, and delete the now-unused Ruby +script. The iOS build no longer depends on a Ruby interpreter. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +Expected: commit created cleanly; `git status` shows a clean tree. + +--- + +## Task 6: End-to-end verification with `generateProject` + +**Files:** +- No changes. + +- [ ] **Step 6.1: Wipe the previously-decrypted directory so the build re-decrypts fresh** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +rm -rf ~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/decrypted +``` + +Expected: directory removed. If it did not exist, that's also fine. + +- [ ] **Step 6.2: Run the user-supplied `generateProject` command** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +source ~/.zshrc 2>/dev/null +python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/build/telegram/telegram-bazel-cache \ + generateProject \ + --configurationPath ~/build/telegram/telegram-internal-tools/PrivateData/build-configurations/enterprise-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent +``` + +Expected: the command runs through project generation. The decryption step is silent on success (per `BuildConfiguration.py:decrypt_codesigning_directory_recursively`). Any decryption failure would surface downstream in `copy_profiles_from_directory` when `openssl smime -verify` chokes on a corrupted `.mobileprovision`, so a clean run proves the port is working end-to-end. + +If the command fails with a decryption-related error, revert the two commits (`git revert HEAD~1..HEAD`) and debug; otherwise the migration is complete. + +- [ ] **Step 6.3: Spot-check the generated decrypted directory** + +```bash +ls ~/build/telegram/telegram-ios/build-input/configuration-repository-workdir/decrypted/profiles/development/ +``` + +Expected: a populated list of `.mobileprovision` files, matching the list in the encrypted sibling directory. diff --git a/docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-10.md b/docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-10.md new file mode 100644 index 0000000000..e13ee5a2d8 --- /dev/null +++ b/docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-10.md @@ -0,0 +1,194 @@ +# Postbox → TelegramEngine Wave 10 Implementation Plan + +> **For agentic workers:** This plan was executed in a single session; steps below are a post-hoc record of the work landed, not a to-do list. + +**Goal:** Finish the `StorageUsageScreen` consumer-module de-Postbox work started in wave 8 and continued in wave 9 by eliminating the last `import Postbox` in the module: `StorageFileListPanelComponent.swift`'s `Icon.media(Media, TelegramMediaImageRepresentation)` enum case. + +**Architecture:** Replace the heterogeneous-protocol `Icon.media(Media, ...)` case with two concrete-type cases `.mediaFile(TelegramMediaFile, ...)` and `.mediaImage(TelegramMediaImage, ...)`. The split is lossless because the two construction sites already knew the concrete subtype (`imageIconValue = .media(file, representation)` vs `.media(image, representation)`), and the one consumer binding site immediately downcasted via `as? TelegramMediaFile` / `as? TelegramMediaImage` to pick which `setSignal(...)` to call. Auto-split the switch body over the two new cases; no downcast needed. Also replaces a placeholder `PeerId(namespace:..., id:...)` construction in the `measureItem` layout-measurement instance with `component.context.account.peerId` (a real, already-available `EnginePeer.Id`). + +**Tech Stack:** Swift / Bazel. No unit tests. + +**Build command:** + +```bash +source ~/.zshrc 2>/dev/null; PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber 1 --configuration debug_sim_arm64 --continueOnError +``` + +--- + +## Scope + +**In scope:** +- `StorageFileListPanelComponent.swift`'s `Icon` enum: replace `case media(Media, TelegramMediaImageRepresentation)` with two concrete-type cases. +- Equatable rewrite: switch-over-tuple `(lhs, rhs)` pattern with id-based equality per concrete type (`lFile.fileId == rFile.fileId`, `lImage.imageId == rImage.imageId`). +- Binding rewrite at the `if case let .media(media, representation)` site (former line 448): lift `representation` via a compound `case let .mediaFile(_, representation), let .mediaImage(_, representation):` pattern, then inner switch-over-`component.icon` selects `setSignal` flavor. +- Construction rewrite at two `imageIconValue = .media(...)` sites: use the concrete case name (`.mediaFile`, `.mediaImage`). +- Placeholder `PeerId(namespace:..., id:...)` at former line 1062 (in the `measureItem` layout-measurement instance): replace with `component.context.account.peerId`. +- Remove `import Postbox` from `StorageFileListPanelComponent.swift`. + +**Out of scope:** +- None. This is the last file in the `StorageUsageScreen` module that imports Postbox; after this wave, the module is fully Postbox-free. + +--- + +## Tasks + +### Task 1: Split `Icon.media` into two concrete cases + +**Files:** +- Modify: `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift` (former 103–135). + +Before: + +```swift +enum Icon: Equatable { + case fileExtension(String) + case media(Media, TelegramMediaImageRepresentation) + case audio + + static func ==(lhs: Icon, rhs: Icon) -> Bool { + switch lhs { + case let .fileExtension(value): + if case .fileExtension(value) = rhs { return true } else { return false } + case let .media(media, representation): + if case let .media(rhsMedia, rhsRepresentation) = rhs { + if media.id != rhsMedia.id { return false } + if representation != rhsRepresentation { return false } + return true + } else { return false } + case .audio: + if case .audio = rhs { return true } else { return false } + } + } +} +``` + +After: + +```swift +enum Icon: Equatable { + case fileExtension(String) + case mediaFile(TelegramMediaFile, TelegramMediaImageRepresentation) + case mediaImage(TelegramMediaImage, TelegramMediaImageRepresentation) + case audio + + static func ==(lhs: Icon, rhs: Icon) -> Bool { + switch (lhs, rhs) { + case let (.fileExtension(l), .fileExtension(r)): + return l == r + case let (.mediaFile(lFile, lRepresentation), .mediaFile(rFile, rRepresentation)): + return lFile.fileId == rFile.fileId && lRepresentation == rRepresentation + case let (.mediaImage(lImage, lRepresentation), .mediaImage(rImage, rRepresentation)): + return lImage.imageId == rImage.imageId && lRepresentation == rRepresentation + case (.audio, .audio): + return true + default: + return false + } + } +} +``` + +### Task 2: Rewrite the binding site + +**Files:** +- Modify: `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift` (former 448–500). + +Before, the block started with `if case let .media(media, representation) = component.icon { ... }` then inside did `if let file = media as? TelegramMediaFile { ... } else if let image = media as? TelegramMediaImage { ... }`. + +After, use a compound case-binding pattern at the entry (both cases have the same `representation` type, so the pattern works) and an inner switch for the `setSignal` branch: + +```swift +let mediaRepresentation: TelegramMediaImageRepresentation? +switch component.icon { +case let .mediaFile(_, representation), let .mediaImage(_, representation): + mediaRepresentation = representation +default: + mediaRepresentation = nil +} + +if let representation = mediaRepresentation { + // ... setup iconImageNode as before ... + if resetImage { + switch component.icon { + case let .mediaFile(file, _): + iconImageNode.setSignal(chatWebpageSnippetFile( + account: component.context.account, + userLocation: .peer(component.messageId.peerId), + mediaReference: FileMediaReference.standalone(media: file).abstract, + representation: representation, + automaticFetch: false + )) + case let .mediaImage(image, _): + iconImageNode.setSignal(mediaGridMessagePhoto( + account: component.context.account, + userLocation: .peer(component.messageId.peerId), + photoReference: ImageMediaReference.standalone(media: image), + automaticFetch: false + )) + default: + break + } + } + // ... frame + asyncLayout + apply as before ... +} +``` + +### Task 3: Update the two construction sites + +**Files:** +- Modify: `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift` (former 985 and 992). + +`imageIconValue = .media(file, representation)` → `.mediaFile(file, representation)` (for `TelegramMediaFile` branch). +`imageIconValue = .media(image, representation)` → `.mediaImage(image, representation)` (for `TelegramMediaImage` branch). + +### Task 4: Replace the placeholder `PeerId(...)` construction + +**Files:** +- Modify: `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift` (former 1062). + +The `measureItem` layout-measurement instance uses a fully-zero placeholder peer id: + +```swift +messageId: EngineMessage.Id(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(0)), namespace: 0, id: 0), +``` + +Naming `PeerId`, `PeerId.Namespace`, `PeerId.Id` all require `import Postbox` (these are raw Postbox types, not TelegramCore typealiases). Replace with `component.context.account.peerId`, a real `EnginePeer.Id` already in scope: + +```swift +messageId: EngineMessage.Id(peerId: component.context.account.peerId, namespace: 0, id: 0), +``` + +Semantically equivalent for the measurement use case — `messageId` is used downstream only for `.peerId` extraction in the image-fetch userLocation and for Equatable comparison; the measurement instance is standalone and not compared. The `id: 0, namespace: 0` part stays; those are plain `Int32`, nothing Postbox-specific. + +Caught by second-pass build failure `cannot find 'PeerId' in scope` after dropping `import Postbox`. + +### Task 5: Drop `import Postbox` + +**Files:** +- Modify: `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift` (former line 14). + +Remove the `import Postbox` line. + +### Task 6: Full project build + +Expected green after Tasks 4 and 5. The first build attempt surfaced the `PeerId` issue; Task 4's fix addressed it. + +### Task 7: Commit + +Single wave-10 atomic commit. CLAUDE.md gets a wave-10 outcome section; the "Modules currently free of `import Postbox`" tally gains `StorageUsageScreen` (the module as a whole). Both files that previously imported Postbox in this module (`StorageUsageScreen.swift` from wave 9 and `StorageFileListPanelComponent.swift` from wave 10) are now Postbox-free. + +--- + +## Outcome (2026-04-20) + +Single atomic commit. Build verified green (27 actions, cached). + +**`StorageUsageScreen` consumer module is now fully Postbox-free** — last file (`StorageFileListPanelComponent.swift`) landed in this wave; the other file (`StorageUsageScreen.swift`) landed in wave 9. + +Net: 1 file changed, +22 / -29 lines (−7 simplification — the new switch-over-tuple Equatable is both terser and more idiomatic than the old three-way nested `switch` + `if case` pattern). + +**Lessons:** + +- **Heterogeneous-protocol enum cases are an easy de-Postbox win** when the protocol values already get downcast to a fixed small set of concrete subtypes. The compiler-enforced exhaustiveness of the split improves call-site safety (no silent `else` branch that forgot a subtype). +- **Placeholder `PeerId(...)` constructions in layout-measurement code are traps.** Common pattern in this codebase: a "dummy" component instance is constructed purely to run `.update(...)` and harvest the returned size. The dummy values (`messageId`, `peerId`) are not used for anything but type-filling, yet naming the types forces `import Postbox`. When de-Postboxing, look for `PeerId(namespace:...`/`MessageId(peerId:...` constructions with all-zero arguments and replace with any convenient real value already in scope (`context.account.peerId` works for peer-id placeholders). diff --git a/docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-7.md b/docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-7.md new file mode 100644 index 0000000000..b5bf1f0f57 --- /dev/null +++ b/docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-7.md @@ -0,0 +1,95 @@ +# Postbox → TelegramEngine Wave 7 Implementation Plan + +> **For agentic workers:** This plan was executed in a single session; steps below are a post-hoc record of the work landed, not a to-do list. + +**Goal:** Close out the remaining raw-Postbox leaks in `TelegramEngine.*` public facades surfaced by the wave-6 post-sweep scouting pass (2026-04-20). Six facade-signature migrations + one dead-facade deletion + consumer call-site bridging, landed as a single wave commit. + +**Architecture:** Wave-2 shape scaled to seven facades at once: each facade signature changes in place from raw Postbox domain types (`Message`, `Peer`) to engine equivalents (`EngineMessage`, `EnginePeer`), with `_internal_*` implementations left raw per the standing "internal Postbox-facing stays raw" rule. Consumer call sites bridge at the facade boundary via `EngineMessage.init` / `._asMessage()` wrap/unwrap helpers or drop now-redundant wrapping. + +**Tech Stack:** Swift / Bazel. No unit tests by repo policy — verification is a full project build. + +**Build command:** + +```bash +source ~/.zshrc 2>/dev/null; PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber 1 --configuration debug_sim_arm64 --continueOnError +``` + +--- + +## Scope — candidate list + +All seven items from the wave-6 post-sweep scouting pass: + +1. `TelegramEngine.Messages.downloadMessage(messageId: MessageId) -> Signal` → `(messageId: EngineMessage.Id) -> Signal`. Callers: 1 (`ChatListSearchListPaneNode`). +2. `TelegramEngine.Messages.topPeerActiveLiveLocationMessages(peerId: PeerId) -> Signal<(Peer?, [Message]), NoError>` → `(peerId: EnginePeer.Id) -> Signal<(EnginePeer?, [EngineMessage]), NoError>`. Callers: 2 (`LocationViewControllerNode`, `LiveLocationSummaryManager`). +3. `TelegramEngine.Messages.getSynchronizeAutosaveItemOperations()` — dead facade (sole caller `StoreDownloadedMedia.swift:298` uses `_internal_*` directly). Deleted. +4. `TelegramEngine.Peers.updatedRemotePeer(peer: PeerReference) -> Signal` → `Signal`. `PeerReference` param kept (no `EnginePeer.Reference` alias today). Callers: 1 (`ChannelAdminsController`, `ignoreValues` so no caller change needed). +5. `TelegramEngine.Resources.renderStorageUsageStatsMessages(…existingMessages: [EngineMessage.Id: Message]) -> Signal<[EngineMessage.Id: Message], NoError>` → `[EngineMessage.Id: EngineMessage]` on both sides. Callers: 1 (`StorageUsageScreen`). +6–8. `TelegramEngine.Resources.clearStorage(...)` overloads (three) — `[Message]` params → `[EngineMessage]`. Real external callers: 2 (`StorageUsageScreen`, two overloads). The third overload `clearStorage(messages:)` has no callers; migrated for overload-set consistency. + +--- + +## Tasks + +### Task 1: Migrate three `TelegramEngine.Messages` facades + +**Files:** +- Modify: `submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift` (3 facades) +- Modify: `submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift` (drop redundant `.flatMap(EngineMessage.init)`) +- Modify: `submodules/LocationUI/Sources/LocationViewControllerNode.swift` (drop redundant `.map(EngineMessage.init)`) +- Modify: `submodules/LiveLocationManager/Sources/LiveLocationSummaryManager.swift` (drop redundant `EnginePeer(...)` / `EngineMessage(...)` wrappers) + +**Changes:** + +`downloadMessage` — wrap return `Message?` → `EngineMessage?` via `|> map { $0.flatMap(EngineMessage.init) }`. `_internal_downloadMessage` still takes `messageId: MessageId`, which is typealiased to `EngineMessage.Id`, so the param change is purely a rename at the public surface. + +`topPeerActiveLiveLocationMessages` — wrap tuple return via `|> map { peer, messages -> (EnginePeer?, [EngineMessage]) in (peer.flatMap(EnginePeer.init), messages.map(EngineMessage.init)) }`. + +`getSynchronizeAutosaveItemOperations` — deleted. The sole caller `StoreDownloadedMedia.swift:298` was already calling `_internal_getSynchronizeAutosaveItemOperations` directly (inside its own transaction block), so no caller update needed. + +### Task 2: Migrate `TelegramEngine.Peers.updatedRemotePeer` + +**Files:** +- Modify: `submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift` + +Append `|> map(EnginePeer.init)` to wrap the `Peer` result. `PeerReference` param stays. Single call site in `ChannelAdminsController.swift` uses `ignoreValues`, so no caller-side change. + +### Task 3: Migrate four `TelegramEngine.Resources` facades + +**Files:** +- Modify: `submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift` (4 facades) +- Modify: `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift` (3 call sites) + +`renderStorageUsageStatsMessages` — unwrap `[EngineMessage.Id: EngineMessage]` input via `.mapValues { $0._asMessage() }`, wrap raw result via `.mapValues(EngineMessage.init)`. Caller bridges the other direction at its single call site (`.mapValues(EngineMessage.init)` on the input `existingMessages`, `.mapValues { $0._asMessage() }` on the mapped result). + +`clearStorage(peerId:categories:includeMessages:excludeMessages:)` / `clearStorage(peerIds:includeMessages:excludeMessages:)` / `clearStorage(messages:)` — unwrap `[EngineMessage]` params via `.map { $0._asMessage() }` before forwarding to `_internal_clearStorage`. Callers bridge `[Message]` locals with `.map(EngineMessage.init)` at the facade call site. + +Call-site changes in `StorageUsageScreen` are intentionally minimal: the file's `AggregatedData` type keeps `[MessageId: Message]` / `[Message]` internally, with bridging applied only at the four facade-call points. A full-consumer-module migration to `EngineMessage` is out of scope for this wave (would require changing ~30 sites plus the item types in `StorageFileListPanelComponent`; a future "StorageUsageScreen full de-Postbox" wave could land that). + +### Task 4: Full project build + +Run the build command above with `--continueOnError`. Expected: clean build (no errors or warnings introduced). One full build covers all facades since they're in TelegramCore and rebuilding TelegramCore re-verifies every consumer. + +### Task 5: Commit + +Single wave-7 atomic commit covering the 8 modified files and the CLAUDE.md outcome update. + +--- + +## Outcome (2026-04-20) + +All seven candidates landed. Single atomic commit. Build verified green (`bazel-bin/Telegram/Telegram.ipa` produced; 5854 total actions, 1009 executed). + +- 3 `TelegramEngine.Messages` facades migrated (1 rewrite, 1 rewrite, 1 deletion) +- 1 `TelegramEngine.Peers` facade migrated +- 4 `TelegramEngine.Resources` facades migrated (1 dict, 3 overloads) +- 5 consumer files updated: `ChatListSearchListPaneNode`, `LocationViewControllerNode`, `LiveLocationSummaryManager`, `StorageUsageScreen`, CLAUDE.md + +No modules became Postbox-free in this wave (all five touched consumers still import Postbox for unrelated reasons — `StorageUsageScreen` especially, which still has 43 raw `Message` / `MessageId` references outside the facade boundary). + +**Lesson recorded:** when a facade's consumer file uses the raw Postbox type extensively outside the facade boundary (e.g. `StorageUsageScreen` with its `[MessageId: Message]` dict stored in a helper class and threaded through ~30 sites), bridging at the facade call site is the correct scope. Full-consumer-module migration is its own separate wave, not a side-effect of facade migration. + +**Next-wave candidates.** The sum of the scouting pass's 8 candidates has been closed. No new `TelegramEngine.*` public facades with raw `Postbox`/`Account`/`MediaBox`/`Peer`/`Message`/`MediaResource` leaks remain. Future-wave focus shifts to: + +1. Full-consumer-module migrations (e.g. `StorageUsageScreen` — drop `AggregatedData`'s raw-Postbox storage types, drop `import Postbox`). +2. Another speculative unused-import sweep pass like wave 6, to catch imports that became unused after waves 4–7. diff --git a/docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-8.md b/docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-8.md new file mode 100644 index 0000000000..85e3edd146 --- /dev/null +++ b/docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-8.md @@ -0,0 +1,103 @@ +# Postbox → TelegramEngine Wave 8 Implementation Plan + +> **For agentic workers:** This plan was executed in a single session; steps below are a post-hoc record of the work landed, not a to-do list. + +**Goal:** `StorageUsageScreen` consumer-module migration — drop all raw `Message` domain types from the screen's internal storage and public peer-panel item types, and eliminate the wave-7 facade-boundary bridging. Scope is narrower than a full de-Postbox of the module: direct `postbox.combinedView` / `postbox.transaction` sites for `AccountSpecificCacheStorageSettings` observation are left for a future wave. + +**Architecture:** Two files modified. `StorageFileListPanelComponent.Item.message` and `StorageUsageScreen`'s `AggregatedData` + `RenderResult` + `SelectionState` internal types are migrated from raw `Message`/`[Message]`/`[MessageId: Message]` to `EngineMessage`/`[EngineMessage]`/`[EngineMessage.Id: EngineMessage]`. The two external APIs that still take raw `Message` (`OpenChatMessageParams.message`, `chatMediaListPreviewControllerData(message:)`) are called with `engineMessage._asMessage()` at the call site. + +**Tech Stack:** Swift / Bazel. No unit tests by repo policy — verification is a full project build. + +**Build command:** + +```bash +source ~/.zshrc 2>/dev/null; PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber 1 --configuration debug_sim_arm64 --continueOnError +``` + +--- + +## Scope + +**In scope:** + +- `StorageFileListPanelComponent.Item.message: Message` → `EngineMessage` (the item type co-located with the panel component). +- `StorageUsageScreen.Component.SelectionState.togglePeer(id:availableMessages: [EngineMessage.Id: Message])` → `[EngineMessage.Id: EngineMessage]`. +- `StorageUsageScreen.Component.AggregatedData.messages: [MessageId: Message]` → `[EngineMessage.Id: EngineMessage]`. +- `AggregatedData.clearIncludeMessages: [Message]` / `.clearExcludeMessages: [Message]` → `[EngineMessage]` (plus the corresponding local vars in `AggregatedData.updateSelected...`). +- `AggregatedData.init(..., messages: [MessageId: Message])` → `[EngineMessage.Id: EngineMessage]`. +- `StorageUsageScreen.Component.RenderResult.messages: [MessageId: Message]` → `[EngineMessage.Id: EngineMessage]`. +- `openMessage(message: Message)` → `openMessage(message: EngineMessage)`. +- Drop the now-redundant wave-7 facade-boundary bridging (`.mapValues(EngineMessage.init)` on `existingMessages`, `.mapValues { $0._asMessage() }` on the facade's engineMessages output, `.map(EngineMessage.init)` on the two `clearStorage` call sites, `._asMessage()` on `item.message` inside the `AggregatedData.updateSelected...` loop, and `EngineMessage(message)` inside the `result.imageItems.append(...)` site). + +**Out of scope — left for a future wave:** + +- Direct postbox usage for `AccountSpecificCacheStorageSettings` observation: `StorageUsageScreen.swift:1047-1062` and `3131-3185`. Blocks `import Postbox` removal. Requires engine equivalents for `PostboxViewKey.preferences` / `PreferencesView` observation and for `transaction.getPeer` / `transaction.getPeerCachedData` — likely an `EngineData`-subscription based rewrite plus peer-category classification via already-existing engine APIs. +- `StorageFileListPanelComponent`'s `Icon.media(Media, TelegramMediaImageRepresentation)` enum case. Holds either `TelegramMediaFile` or `TelegramMediaImage` (always one of these two TelegramCore types per `imageIconValue = .media(file, ...)` and `.media(image, ...)` construction sites). Could be split into `.mediaFile(TelegramMediaFile, ...)` / `.mediaImage(TelegramMediaImage, ...)` to eliminate the raw `Media` protocol dependency; out of scope as it's unrelated to Message-type migration. + +--- + +## Tasks + +### Task 1: Migrate `StorageFileListPanelComponent.Item.message` + +**Files:** +- Modify: `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift` — `Item.message` type and `init(message:)` param. + +No other changes inside the file. Internal usage (`item.message.id`, `item.message.timestamp`, `item.message.media`) already works on `EngineMessage` — `EngineMessage.media` returns `[Media]` (raw), so the `as? TelegramMediaFile` / `as? TelegramMediaImage` downcasts inside the `for media in item.message.media` loop still compile. + +### Task 2: Migrate `StorageUsageScreen` internal storage types + +**Files:** +- Modify: `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift` + +Change: +- `SelectionState.togglePeer(availableMessages: [EngineMessage.Id: Message])` → `[EngineMessage.Id: EngineMessage]`. Body only uses `messageId.peerId` so no body change required. +- `AggregatedData.messages` / `.clearIncludeMessages` / `.clearExcludeMessages` type declarations and the init param. +- The selected-messages accumulation loop inside `AggregatedData` (the block running from the photo/video/file/music category branches): drop `item.message._asMessage()` in the two photo/video branches (`imageItems` holds EngineMessage items, so `._asMessage()` was the EngineMessage→Message unwrap to fit the old `[Message]` local); `item.message` in the file/music branches now passes through since Item.message is EngineMessage. +- `RenderResult.messages` type. + +### Task 3: Drop wave-7 facade-boundary bridging + +At `StorageUsageScreen.swift:2397` the `renderStorageUsageStatsMessages` call previously wrapped input via `(self.aggregatedData?.messages ?? [:]).mapValues(EngineMessage.init)` and unwrapped output via `.mapValues { $0._asMessage() }`. With `AggregatedData.messages` and `RenderResult.messages` now EngineMessage-typed, both bridges vanish: the call just passes `self.aggregatedData?.messages ?? [:]` directly and assigns the result to `result.messages` unchanged. + +At the two `clearStorage` call sites in `StorageUsageScreen.swift` (inside `clearSelected(...)`): `aggregatedData.clearIncludeMessages.map(EngineMessage.init)` → `aggregatedData.clearIncludeMessages` (same for `excludeMessages`), plus the local `includeMessages: [Message]` / `excludeMessages: [Message]` vars become `[EngineMessage]`. + +At the `RenderResult`-building loop (post-`renderStorageUsageStatsMessages`), `StorageMediaGridPanelComponent.Item(message: EngineMessage(message), ...)` → `message: message` since `message` is already `EngineMessage`. + +### Task 4: Migrate `openMessage` + external-API unwraps + +`openMessage(message: Message)` → `openMessage(message: EngineMessage)`. Two external APIs receive raw `Message`: pass `message._asMessage()` to `OpenChatMessageParams(message:)` inside `openMessage`, and to `chatMediaListPreviewControllerData(message:)` inside `messageGaleryContextAction`. Also drop the one-line `let foundGalleryMessage: Message? = message` + `guard let galleryMessage = foundGalleryMessage` dance inside `openMessage` — it's a no-op wrap preserved from an older version. + +### Task 5: Full project build + +Expected clean (cached — 30 seconds on an incremental build; ~60s from a cold start since wave 7). + +### Task 6: Commit + +Single wave-8 atomic commit. + +--- + +## Outcome (2026-04-20) + +Single atomic commit landing the migration. Build verified green (59s incremental, 27 actions). Net -5 lines (simplification, as expected — most changes are type swaps and a handful of redundant wraps/unwraps removed). + +Two files modified: + +| File | Δ | +|---|---| +| `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift` | +33 / -44 | +| `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift` | +3 / -3 | + +**Module did NOT become Postbox-free.** Both files retain `import Postbox` for the out-of-scope sites listed above. Drop-candidacy inventory in `StorageUsageScreen.swift`: + +- 1047–1062: preferences-view observation of `AccountSpecificCacheStorageSettings` via `postbox.combinedView` + `PreferencesView`. +- 3131–3185: second preferences-view observation + `postbox.transaction { transaction in ... transaction.getPeer / transaction.getPeerCachedData as? CachedGroupData / CachedChannelData ... }` for classifying peer-storage-timeout exceptions. + +And in `StorageFileListPanelComponent.swift`: + +- 105: `Icon.media(Media, TelegramMediaImageRepresentation)` enum case. + +Future wave targets either the preferences-view observation sites (substantial — `EngineData`-subscription rewrite + peer-category classification via engine APIs) or the `Icon.media` split (trivial — 3 sites to update). + +Plan / record: `docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-8.md`. diff --git a/docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-9.md b/docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-9.md new file mode 100644 index 0000000000..28e5c85c9a --- /dev/null +++ b/docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-9.md @@ -0,0 +1,162 @@ +# Postbox → TelegramEngine Wave 9 Implementation Plan + +> **For agentic workers:** This plan was executed in a single session; steps below are a post-hoc record of the work landed, not a to-do list. + +**Goal:** Finish the `StorageUsageScreen` de-Postbox work started in wave 8 by rewriting the two remaining direct-postbox sites that observe `AccountSpecificCacheStorageSettings`, and drop `import Postbox` from `StorageUsageScreen.swift`. + +**Architecture:** Replace `postbox.combinedView(keys: [.preferences(...)]) + PreferencesView` observation with `context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key:))`, which returns `PreferencesEntry?` and is then decoded the same way (`.get(AccountSpecificCacheStorageSettings.self)`). Replace the transaction-based per-peer classification (`transaction.getPeer` + `transaction.getPeerCachedData as? CachedGroupData/CachedChannelData`) with an `EngineDataMap` of `TelegramEngine.EngineData.Item.Peer.Peer.init(id:)` lookups producing `EnginePeer?` values that pattern-match on `.user` / `.legacyGroup` / `.channel(channel)` / `.secretChat`. The `FoundPeer(peer:subscribers:)` wrapper in the signal's element type is dropped entirely since downstream consumers (`peerExceptions.isEmpty`, `.count`, `.prefix(3).map { EnginePeer($0.peer.peer) }`) never read `subscribers`. + +**Tech Stack:** Swift / Bazel. No unit tests. + +**Build command:** + +```bash +source ~/.zshrc 2>/dev/null; PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber 1 --configuration debug_sim_arm64 --continueOnError +``` + +--- + +## Scope + +Two direct-postbox site clusters rewritten in `StorageUsageScreen.swift`: + +1. **Site 1 (former lines 1047–1087)** — `cacheSettingsExceptionCount` signal. Preserved its downstream `EngineDataMap` + `EnginePeer` per-category counting logic unchanged; only the preferences observation replaced. +2. **Site 2 (former lines 3131–3196)** — `peerExceptions` signal inside `openKeepMediaCategory`. Both the preferences observation AND the `postbox.transaction { transaction.getPeer / transaction.getPeerCachedData ... FoundPeer(...) }` block replaced. Signal element type changed from `[(peer: FoundPeer, value: Int32)]` to `[(peer: EnginePeer, value: Int32)]`; `FoundPeer` and the unread `subscribers` field dropped. + +One consumer-side edit: `peerExceptions.prefix(3).map { EnginePeer($0.peer.peer) }` → `peerExceptions.prefix(3).map { $0.peer }` (at the `MultiplePeerAvatarsContextItem` construction). + +One typealias fixup: `var mergedMedia: [MessageId: Int64]` → `[EngineMessage.Id: Int64]` (required once `import Postbox` is removed, since `MessageId` is the raw Postbox name, not a TelegramCore typealias). + +`import Postbox` removed from `StorageUsageScreen.swift`. + +--- + +## Tasks + +### Task 1: Rewrite site 1 — cacheSettingsExceptionCount + +**Files:** +- Modify: `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift` (former 1047–1058). + +Replace the preferences observation header. The downstream `mapToSignal { ... EngineDataMap ... EnginePeer ... }` body is already Engine-only and unchanged. + +Before: + +```swift +let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings])) +let cacheSettingsExceptionCount: Signal<[CacheStorageSettings.PeerStorageCategory: Int32], NoError> = component.context.account.postbox.combinedView(keys: [viewKey]) +|> map { views -> AccountSpecificCacheStorageSettings in + let cacheSettings: AccountSpecificCacheStorageSettings + if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) { + cacheSettings = value + } else { + cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings + } + return cacheSettings +} +|> distinctUntilChanged +|> mapToSignal { ... } +``` + +After: + +```swift +let cacheSettingsExceptionCount: Signal<[CacheStorageSettings.PeerStorageCategory: Int32], NoError> = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.accountSpecificCacheStorageSettings) +) +|> map { preferencesEntry -> AccountSpecificCacheStorageSettings in + return preferencesEntry?.get(AccountSpecificCacheStorageSettings.self) ?? AccountSpecificCacheStorageSettings.defaultSettings +} +|> distinctUntilChanged +|> mapToSignal { ... } +``` + +### Task 2: Rewrite site 2 — peerExceptions + +**Files:** +- Modify: `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift` (former 3131–3196). + +Replace both the preferences observation (as in Task 1) AND the subsequent `mapToSignal { context.account.postbox.transaction { ... } }` block. Signal element type changes from `[(peer: FoundPeer, value: Int32)]` to `[(peer: EnginePeer, value: Int32)]`. `subscriberCount` is not preserved — it's computed but never read by downstream consumers. + +After (showing the `peerExceptions` signal in full): + +```swift +let peerExceptions: Signal<[(peer: EnginePeer, value: Int32)], NoError> = accountSpecificSettings +|> mapToSignal { accountSpecificSettings -> Signal<[(peer: EnginePeer, value: Int32)], NoError> in + return context.engine.data.get( + EngineDataMap(accountSpecificSettings.peerStorageTimeoutExceptions.map(\.key).map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> map { peers -> [(peer: EnginePeer, value: Int32)] in + var result: [(peer: EnginePeer, value: Int32)] = [] + for item in accountSpecificSettings.peerStorageTimeoutExceptions { + guard let peer = peers[item.key] ?? nil else { continue } + let peerCategory: CacheStorageSettings.PeerStorageCategory + switch peer { + case .user, .secretChat: + peerCategory = .privateChats + case .legacyGroup: + peerCategory = .groups + case let .channel(channel): + if case .group = channel.info { + peerCategory = .groups + } else { + peerCategory = .channels + } + } + if peerCategory != mappedCategory { continue } + result.append((peer: peer, value: item.value)) + } + return result.sorted(by: { lhs, rhs in + if lhs.value != rhs.value { + return lhs.value < rhs.value + } + return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle + }) + } +} +``` + +### Task 3: Update consumer of `peerExceptions` + +**Files:** +- Modify: `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift` (former 3288). + +`peerExceptions.prefix(3).map { EnginePeer($0.peer.peer) }` → `peerExceptions.prefix(3).map { $0.peer }`. The `MultiplePeerAvatarsContextItem(context:, peers: [EnginePeer], totalCount:, action:)` signature is unchanged — we simply drop the redundant `EnginePeer(...)` wrap because `$0.peer` is now already an `EnginePeer`. + +### Task 4: Drop `import Postbox` + +**Files:** +- Modify: `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift` (line 12). + +Remove the `import Postbox` line. + +### Task 5: Typealias fixup for `MessageId` + +**Files:** +- Modify: `submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift` (former 2397). + +`var mergedMedia: [MessageId: Int64]` → `[EngineMessage.Id: Int64]`. `MessageId` is the raw Postbox type name; with `import Postbox` removed, the type must be named through the `EngineMessage.Id` typealias. Discovered by first-pass build failure `cannot find type 'MessageId' in scope`. + +### Task 6: Full project build + +Expected green. Incremental build: ~60s (cached), 27 actions. + +### Task 7: Commit + +Single wave-9 atomic commit. CLAUDE.md updates the wave 8 outcome's "future-wave candidates" note since this wave closes both of them. `StorageUsageScreen` (the module as a whole) now has `StorageUsageScreen.swift` Postbox-free; the module's `StorageFileListPanelComponent.swift` still imports Postbox because of the `Icon.media(Media, TelegramMediaImageRepresentation)` enum case (trivial future wave, as previously noted). + +--- + +## Outcome (2026-04-20) + +Single atomic commit. Build verified green (27 actions, ~60s incremental). + +Net change: 1 file, +30 / -54 lines (-24 simplification). + +Lessons: + +- **`ApplicationSpecificPreference(key:)` is the general-purpose engine replacement** for any `postbox.combinedView(keys: [.preferences(keys: Set([key]))])` idiom. Takes a `ValueBoxKey`, returns `PreferencesEntry?`, decodes via `.get(T.self)`. Usable from any module that imports `TelegramCore` even without `import Postbox`, because the `ValueBoxKey`-typed input is obtained through a statically-named `PreferencesKeys.*` member (no `ValueBoxKey` identifier appears in the consumer). +- **`MessageId` is raw Postbox, not a TelegramCore typealias.** CLAUDE.md's "engine typealias cheat sheet" labels `PeerId`, `MessageId`, etc. as migration *targets*, not existing aliases. Files that drop `import Postbox` must rename these to `EngineMessage.Id` / `EnginePeer.Id`. Caught by the first-pass build failure. +- **Dead-code detection during rewrites.** The transaction block's `subscriberCount` computation and the `FoundPeer.subscribers` field it populated were never consumed downstream. The rewrite simply dropped them, shrinking the code further than a 1:1 rewrite would have. + +`StorageUsageScreen.swift` is now Postbox-free. The `StorageUsageScreen` consumer module as a whole is still not fully Postbox-free because `StorageFileListPanelComponent.swift` retains `import Postbox` for its `Icon.media(Media, TelegramMediaImageRepresentation)` enum case (3 construction sites; trivial future wave splits into `.mediaFile(TelegramMediaFile, ...)` / `.mediaImage(TelegramMediaImage, ...)`). diff --git a/docs/superpowers/plans/2026-04-21-swifttl-layered-schema-generation.md b/docs/superpowers/plans/2026-04-21-swifttl-layered-schema-generation.md new file mode 100644 index 0000000000..c5f5bc9549 --- /dev/null +++ b/docs/superpowers/plans/2026-04-21-swifttl-layered-schema-generation.md @@ -0,0 +1,1037 @@ +# SwiftTL — Layered Schema Generation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Extend `build-system/SwiftTL` to parse `.tl` schemas containing `===N===` layer markers and emit one cumulative-snapshot `{apiPrefix}Layer{N}.swift` file per layer, while leaving the flat schema pipeline byte-identical. + +**Architecture:** Approach 1 from the design spec (`docs/superpowers/specs/2026-04-21-swifttl-layered-schema-generation-design.md`). `DescriptionParser` returns a new `ParsedSchema` enum (`.flat` or `.layered`). `Resolver` gains `resolveLayeredTypes(layers:)` that snapshots a running constructor map per layer. `CodeGenerator` gains `generateLayered(apiPrefix:, layerNumber:, types:)` that emits a single-file-per-layer shape matching the existing hand-written `SecretApiLayer{N}.swift`. `main.swift` branches on `ParsedSchema`. + +**Tech Stack:** Swift 5.5 executable target (`build-system/SwiftTL/Package.swift`). Vendored `Parser` combinator library for parsing. Project itself uses Bazel (`Make.py build`) for the full iOS build — `swift build` and `swift run` work at the package level for SwiftTL itself. + +**Testing note:** per `CLAUDE.md`, no unit tests exist in this project. Verification is end-to-end: `swift run SwiftTL` on real schema files, diff against existing hand-written output, full Bazel build of the iOS app. No TDD steps in this plan — each task's verification is either (a) `swift build` (compiles), (b) `swift run SwiftTL` producing expected files, or (c) full Bazel build succeeds. + +**Baseline:** this plan is written against a pre-existing prep commit landed just before Task 1 begins, which threads `apiPrefix: String` through `CodeGenerator.generate` / `generateMainFile` / `generateImplFile` / `typeReferenceRepresentation`, removes dead `--stub-functions` and `--print-constructors` CLI flags from `main.swift`, and deletes the unused `LegacyOrderParser.swift`. All task-level edits below assume these changes are already committed — code snippets and line numbers reference the post-prep-commit state of the files. + +--- + +## File Structure + +All edits within `build-system/SwiftTL/Sources/SwiftTL/`: + +- `DescriptionParsing.swift` — add `ParsedSchema` enum, rework `parse(data:)` to detect `===N===` markers and route lines per layer. +- `Resolution.swift` — add `resolveLayeredTypes(layers:)` that walks sections and snapshots a running map. +- `CodeGeneration.swift` — add `generateLayered(apiPrefix:, layerNumber:, types:)` that emits one `{apiPrefix}Layer{N}.swift` string. +- `main.swift` — branch on `ParsedSchema`, loop for layered, unchanged for flat. + +Downstream changes in the repo: + +- `submodules/TelegramApi/Sources/SecretApiLayer{8,17,20,23,45,46,66,73,101,143,144}.swift` — replace existing 5 (of 11) with generator output; 6 new files added. BUILD uses `glob("Sources/**/*.swift")` so no BUILD update needed (confirmed — `submodules/TelegramApi/BUILD` line 9). + +Reference artifacts to consult during implementation: + +- Input schema: `/Users/isaac/build/telegram/telegram-ios-shared/tools/secret_scheme.tl` (112 lines). +- Legacy per-layer output to match: `submodules/TelegramApi/Sources/SecretApiLayer{8,46,73,101,144}.swift`. +- Flat reference: `submodules/TelegramApi/Sources/Api0.swift` and sharded `Api{1..5}.swift` — shows the `useStructPattern = true` shape to contrast against. +- Invocation script (not to be edited in this plan — spec calls it out as a follow-up in the sibling repo): `/Users/isaac/build/telegram/telegram-ios-shared/tools/generate_and_copy_scheme.sh`. + +--- + +### Task 1: `ParsedSchema` type + layered parsing in `DescriptionParser` + +**Files:** +- Modify: `build-system/SwiftTL/Sources/SwiftTL/DescriptionParsing.swift` + +**Goal:** Replace `DescriptionParser.parse(data:) -> (constructors, functions)` with `parse(data:) -> ParsedSchema`, where `ParsedSchema` is a new enum with `.flat` and `.layered` cases. Layered mode triggers on any line matching `^===\d+===\s*$`. Flat mode is byte-identical to today. + +- [ ] **Step 1: Add the `ParsedSchema` enum at the top of `DescriptionParser`** + +Insert immediately after the `enum DescriptionParser {` opening brace, before `enum TypeReferenceDescription`: + +```swift +enum ParsedSchema { + case flat(constructors: [ConstructorDescription], functions: [ConstructorDescription]) + case layered(layers: [(layerNumber: Int, constructors: [ConstructorDescription])]) +} + +struct SchemaParsingError: Error, CustomStringConvertible { + var text: String + var description: String { text } +} +``` + +- [ ] **Step 2: Rewrite `parse(data:)` signature and dispatch logic** + +Replace the current `static func parse(data: String) throws -> (constructors: [ConstructorDescription], functions: [ConstructorDescription])` (currently at lines 27–99) with: + +```swift +static func parse(data: String) throws -> ParsedSchema { + let lines = data.components(separatedBy: "\n") + + // Detect layered mode: any line of the form ===N=== + let layerMarker = try NSRegularExpression(pattern: "^===\\d+===\\s*$") + let hasLayerMarker = lines.contains { line in + let range = NSRange(line.startIndex..., in: line) + return layerMarker.firstMatch(in: line, range: range) != nil + } + + if hasLayerMarker { + return try parseLayered(lines: lines) + } else { + return try parseFlat(lines: lines) + } +} +``` + +- [ ] **Step 3: Extract the existing flat-parsing body into `parseFlat(lines:)`** + +Add directly below `parse(data:)`. This is the existing logic verbatim, just accepting pre-split lines and returning the new enum case: + +```swift +private static func parseFlat(lines: [String]) throws -> ParsedSchema { + var typeLines: [String] = [] + var functionLines: [String] = [] + + let skipPrefixes: [String] = [ + "true#3fedd339 = True;", + "vector#1cb5c415 {t:Type} # [ t ] = Vector t;", + "error#c4b9f9bb code:int text:string = Error;", + "null#56730bcc = Null;" + ] + let skipContains: [String] = ["{X:Type}"] + + var isParsingFunctions = false + loop: for line in lines { + if line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty { + continue + } else if line == "---functions---" { + isParsingFunctions = true + } else { + for string in skipPrefixes { + if line.hasPrefix(string) { continue loop } + } + for string in skipContains { + if line.contains(string) { continue loop } + } + if isParsingFunctions { + functionLines.append(line) + } else { + typeLines.append(line) + } + } + } + + var constructors: [ConstructorDescription] = [] + var functions: [ConstructorDescription] = [] + + for line in typeLines { + do { + constructors.append(try parseConstructor(string: line)) + } catch let e { + print("Error while parsing line:\n\(line)\n") + print("\(e)") + throw e + } + } + for line in functionLines { + do { + functions.append(try parseConstructor(string: line)) + } catch let e { + print("Error while parsing line:\n\(line)\n") + print("\(e)") + throw e + } + } + + return .flat(constructors: constructors, functions: functions) +} +``` + +- [ ] **Step 4: Add `parseLayered(lines:)`** + +Add directly below `parseFlat`: + +```swift +private static func parseLayered(lines: [String]) throws -> ParsedSchema { + let skipPrefixes: [String] = [ + "true#3fedd339 = True;", + "vector#1cb5c415 {t:Type} # [ t ] = Vector t;", + "error#c4b9f9bb code:int text:string = Error;", + "null#56730bcc = Null;" + ] + let skipContains: [String] = ["{X:Type}"] + let layerMarker = try NSRegularExpression(pattern: "^===(\\d+)===\\s*$") + + // Pre-marker constructor lines accumulate here and are attached to the first declared layer. + var preMarkerLines: [String] = [] + var sections: [(layerNumber: Int, lines: [String])] = [] + var lastLayerNumber: Int? = nil + + loop: for line in lines { + let trimmed = line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + if trimmed.isEmpty { continue } + + if line == "---functions---" { + throw SchemaParsingError(text: "Layered schemas may not declare ---functions---; secret/layered schemas are types-only.") + } + + let range = NSRange(line.startIndex..., in: line) + if let match = layerMarker.firstMatch(in: line, range: range), + let numberRange = Range(match.range(at: 1), in: line), + let layerNumber = Int(line[numberRange]) + { + if let previous = lastLayerNumber, layerNumber <= previous { + throw SchemaParsingError(text: "Layer markers must appear in strictly ascending order; found ===\(layerNumber)=== after ===\(previous)===.") + } + sections.append((layerNumber, [])) + lastLayerNumber = layerNumber + continue + } + + // Apply the same skip rules as flat mode. + for string in skipPrefixes { + if line.hasPrefix(string) { continue loop } + } + for string in skipContains { + if line.contains(string) { continue loop } + } + + if sections.isEmpty { + preMarkerLines.append(line) + } else { + sections[sections.count - 1].lines.append(line) + } + } + + if sections.isEmpty { + throw SchemaParsingError(text: "Layered schema has a layer marker regex match but no ===N=== sections were extracted; this indicates a parser bug.") + } + + // Attach pre-marker lines to the first (lowest) declared layer. + if !preMarkerLines.isEmpty { + sections[0].lines.insert(contentsOf: preMarkerLines, at: 0) + } + + var layers: [(layerNumber: Int, constructors: [ConstructorDescription])] = [] + for (layerNumber, sectionLines) in sections { + var constructors: [ConstructorDescription] = [] + for line in sectionLines { + do { + constructors.append(try parseConstructor(string: line)) + } catch let e { + print("Error while parsing line (layer \(layerNumber)):\n\(line)\n") + print("\(e)") + throw e + } + } + layers.append((layerNumber, constructors)) + } + + return .layered(layers: layers) +} +``` + +- [ ] **Step 5: Verify SwiftTL compiles** + +Run: `cd build-system/SwiftTL && swift build 2>&1` +Expected: build succeeds OR fails only at call sites in `main.swift` (the next task fixes main.swift). Errors inside `DescriptionParsing.swift` mean the rewrite has a syntax/type issue — fix before proceeding. + +Note: at this point `main.swift` still calls `parse(data:)` expecting the old tuple return type, so `swift build` from the package root will fail at the main-file callsite with a type-mismatch error. That's expected; Task 4 fixes it. + +- [ ] **Step 6: Commit** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +git add build-system/SwiftTL/Sources/SwiftTL/DescriptionParsing.swift +git commit -m "$(cat <<'EOF' +SwiftTL: add ParsedSchema + layered schema parsing + +DescriptionParser.parse(data:) now returns ParsedSchema (.flat or +.layered) based on the presence of ===N=== markers. Layered schemas +split constructor lines per layer; pre-marker constructors attach to +the lowest-numbered layer; ---functions--- is rejected in layered +mode; non-ascending markers throw. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 2: `Resolver.resolveLayeredTypes` for per-layer cumulative snapshots + +**Files:** +- Modify: `build-system/SwiftTL/Sources/SwiftTL/Resolution.swift` + +**Goal:** Add `Resolver.resolveLayeredTypes(layers:)` that walks per-layer constructor descriptions in order, threads a running name→constructor map with last-wins semantics, and snapshots `[SumType]` at the end of each layer. Shares argument-resolution logic with the existing `resolveTypes(constructors:)` (by factoring a shared helper closure/method). + +- [ ] **Step 1: Add `resolveLayeredTypes` to the `Resolver` enum** + +Insert this method immediately after `resolveTypes(constructors:)` (which ends at Resolution.swift:201, `return types.values.sorted(by: { $0.name < $1.name })`). The method threads mutable state (a running map of constructor name → resolved SumType.Constructor and target type) and snapshots at each layer boundary: + +```swift +static func resolveLayeredTypes( + layers: [(layerNumber: Int, constructors: [DescriptionParser.ConstructorDescription])] +) throws -> [(layerNumber: Int, types: [SumType])] { + // Running state: for each constructor name, the target type name and the raw description. + // We keep raw descriptions (not resolved forms) because a later-layer constructor may + // introduce new target-type names, and resolveTypeReference needs the final target-type set. + var liveConstructors: [QualifiedName: (typeName: QualifiedName, description: DescriptionParser.ConstructorDescription)] = [:] + var result: [(layerNumber: Int, types: [SumType])] = [] + + for (layerNumber, layerConstructors) in layers { + // Apply this layer's constructors to the running map with last-wins semantics. + for constructorDescription in layerConstructors { + switch constructorDescription.type { + case let .type(name): + if !name.value[name.value.startIndex].isUppercase { + throw ResolutionError(text: "Type constructor \(constructorDescription.name) -> \(name): the resulting type name should begin with a capital letter") + } + liveConstructors[constructorDescription.name] = (name, constructorDescription) + case let .generic(name, argumentType): + throw ResolutionError(text: "Type constructor \(constructorDescription.name) can not be used to construct a generic type \(name)<\(argumentType)>") + } + } + + // Snapshot: group by target type, resolve. + var constructedTypes: [QualifiedName: [DescriptionParser.ConstructorDescription]] = [:] + var constructorNameToType: [QualifiedName: QualifiedName] = [:] + for (ctorName, entry) in liveConstructors { + constructedTypes[entry.typeName, default: []].append(entry.description) + constructorNameToType[ctorName] = entry.typeName + } + + func resolveTypeReference(description: DescriptionParser.TypeReferenceDescription) throws -> TypeReference { + switch description { + case let .type(name): + if let resolvedBuiltinType = resolveBuiltinType(name: name) { + return resolvedBuiltinType + } + if name.value[name.value.startIndex].isUppercase { + if let _ = constructedTypes[name] { + return .boxedType(name) + } else { + throw ResolutionError(text: "Unresolved type \(name) in layer \(layerNumber)") + } + } else { + if let typeName = constructorNameToType[name] { + return .bareConstructor(typeName: typeName, name: name) + } else { + throw ResolutionError(text: "Unresolved type constructor \(name) in layer \(layerNumber)") + } + } + case let .generic(name, argumentType): + if name == "vector" { + return .bareVector(try resolveTypeReference(description: .type(name: argumentType))) + } else if name == "Vector" { + return .boxedVector(try resolveTypeReference(description: .type(name: argumentType))) + } else { + throw ResolutionError(text: "Unresolved generic type \(name) in layer \(layerNumber)") + } + } + } + + func resolveArgument(existingArguments: [Argument], description: DescriptionParser.ArgumentDescription) throws -> Argument { + return Argument( + name: description.name, + type: try resolveTypeReference(description: description.type), + condition: try description.condition.flatMap { condition -> Argument.Condition in + if !existingArguments.contains(where: { $0.name == condition.fieldName }) { + throw ResolutionError(text: "Unresolved conditional field reference to \(condition.fieldName) in layer \(layerNumber)") + } + return Argument.Condition(fieldName: condition.fieldName, bitIndex: condition.bitIndex) + } + ) + } + + var types: [QualifiedName: SumType] = [:] + for (typeName, constructorDescriptions) in constructedTypes { + let type = SumType(name: typeName) + for constructorDescription in constructorDescriptions { + var arguments: [Argument] = [] + for argumentDescription in constructorDescription.arguments { + arguments.append(try resolveArgument(existingArguments: arguments, description: argumentDescription)) + } + guard let id = constructorDescription.explicitId else { + throw ResolutionError(text: "Constructor \(constructorDescription.name) does not have an id") + } + type.constructors[constructorDescription.name] = SumType.Constructor( + name: constructorDescription.name, + id: id, + arguments: arguments + ) + } + types[type.name] = type + } + + let sortedTypes = types.values.sorted(by: { $0.name < $1.name }) + result.append((layerNumber, sortedTypes)) + } + + return result +} +``` + +- [ ] **Step 2: Verify SwiftTL package compiles (DescriptionParsing + Resolution)** + +Run: `cd build-system/SwiftTL && swift build 2>&1 | head -50` +Expected: the only remaining errors are in `main.swift` (unchanged in this task). If `Resolution.swift` itself has errors, fix before proceeding. + +- [ ] **Step 3: Commit** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +git add build-system/SwiftTL/Sources/SwiftTL/Resolution.swift +git commit -m "$(cat <<'EOF' +SwiftTL: add Resolver.resolveLayeredTypes + +Walks layer sections in order, threads a running constructor-name map +with last-wins semantics, and snapshots [SumType] at each layer +boundary. Constructors appearing only in later layers do not leak into +earlier layers' snapshots. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 3: `CodeGenerator.generateLayered` — one file per layer + +**Files:** +- Modify: `build-system/SwiftTL/Sources/SwiftTL/CodeGeneration.swift` + +**Goal:** Emit one `{apiPrefix}Layer{N}.swift` file per layer, matching the shape of the existing hand-written `SecretApiLayer8.swift` (nested `public struct`, inline enum case args, `fileprivate parse_*`). Reuses `typeReferenceRepresentation`, `generateFieldSerialization`, `generateFieldParsing`, and `SumType.hasDirectReference(to:typeMap:)` unchanged. + +Reference for the expected output shape: read `submodules/TelegramApi/Sources/SecretApiLayer8.swift` (the first 80 lines show the header + struct body; lines ~85+ show the nested enums). The generator output will not byte-match but must match this *shape*. + +- [ ] **Step 1: Add `generateLayered` entry point to `CodeGenerator`** + +Insert the following method after the existing `generate(...)` method (which ends at CodeGeneration.swift:200). Reads directly; uses the same `CodeWriter` helper and private type helpers already in the file: + +```swift +static func generateLayered( + apiPrefix: String, + layerNumber: Int, + types: [Resolver.SumType] +) throws -> (filename: String, source: String) { + let structName = "\(apiPrefix)\(layerNumber)" + let filename = "\(apiPrefix)Layer\(layerNumber).swift" + + var typeMap: [QualifiedName: Resolver.SumType] = [:] + for type in types { + typeMap[type.name] = type + } + + // Detect whether any constructor argument uses Int256; if so, we need the int256 parser entry. + var usesInt256 = false + for type in types { + for (_, constructor) in type.constructors { + for argument in constructor.arguments { + if containsInt256(argument.type) { usesInt256 = true; break } + } + if usesInt256 { break } + } + if usesInt256 { break } + } + + var writer = CodeWriter() + writer.line() + + // File-scope dispatch table + writer.line("fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {") + writer.indent() + writer.line("var dict: [Int32 : (BufferReader) -> Any?] = [:]") + writer.line("dict[-1471112230] = { return $0.readInt32() }") + writer.line("dict[570911930] = { return $0.readInt64() }") + writer.line("dict[571523412] = { return $0.readDouble() }") + writer.line("dict[-1255641564] = { return parseString($0) }") + if usesInt256 { + writer.line("dict[0x0929C32F] = { return parseInt256($0) }") + } + + let sortedTypes = types.sorted(by: { $0.name < $1.name }) + for type in sortedTypes { + let sortedConstructors = type.constructors.values.sorted(by: { $0.name < $1.name }) + for constructor in sortedConstructors { + writer.line("dict[\(Int32(bitPattern: constructor.id))] = { return \(structName).\(type.name).parse_\(constructor.name.value)($0) }") + } + } + writer.line("return dict") + writer.dedent() + writer.line("}()") + writer.line() + + // public struct {apiPrefix}{N} { + writer.line("public struct \(structName) {") + writer.indent() + + // public static func parse(_ buffer: Buffer) -> Any? + writer.line("public static func parse(_ buffer: Buffer) -> Any? {") + writer.indent() + writer.line("let reader = BufferReader(buffer)") + writer.line("if let signature = reader.readInt32() {") + writer.indent() + writer.line("return parse(reader, signature: signature)") + writer.dedent() + writer.line("}") + writer.line("return nil") + writer.dedent() + writer.line("}") + writer.line() + + // fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? + writer.line("fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? {") + writer.indent() + writer.line("if let parser = parsers[signature] {") + writer.indent() + writer.line("return parser(reader)") + writer.dedent() + writer.line("}") + writer.line("else {") + writer.indent() + writer.line("telegramApiLog(\"Type constructor \\(String(signature, radix: 16, uppercase: false)) not found\")") + writer.line("return nil") + writer.dedent() + writer.line("}") + writer.dedent() + writer.line("}") + writer.line() + + // fileprivate static func parseVector + writer.line("fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? {") + writer.indent() + writer.line("if let count = reader.readInt32() {") + writer.indent() + writer.line("var array = [T]()") + writer.line("var i: Int32 = 0") + writer.line("while i < count {") + writer.indent() + writer.line("var signature = elementSignature") + writer.line("if elementSignature == 0 {") + writer.indent() + writer.line("if let unboxedSignature = reader.readInt32() {") + writer.indent() + writer.line("signature = unboxedSignature") + writer.dedent() + writer.line("}") + writer.line("else {") + writer.indent() + writer.line("return nil") + writer.dedent() + writer.line("}") + writer.dedent() + writer.line("}") + writer.line("if let item = \(structName).parse(reader, signature: signature) as? T {") + writer.indent() + writer.line("array.append(item)") + writer.dedent() + writer.line("}") + writer.line("else {") + writer.indent() + writer.line("return nil") + writer.dedent() + writer.line("}") + writer.line("i += 1") + writer.dedent() + writer.line("}") + writer.line("return array") + writer.dedent() + writer.line("}") + writer.line("return nil") + writer.dedent() + writer.line("}") + writer.line() + + // public static func serializeObject + writer.line("public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) {") + writer.indent() + writer.line("switch object {") + for type in sortedTypes { + writer.line("case let _1 as \(structName).\(type.name):") + writer.indent() + writer.line("_1.serialize(buffer, boxed)") + writer.dedent() + } + writer.line("default:") + writer.indent() + writer.line("break") + writer.dedent() + writer.line("}") + writer.dedent() + writer.line("}") + writer.line() + + // Nested public enum { ... } for each type + for type in sortedTypes { + try emitLayeredType(writer: &writer, apiPrefix: apiPrefix, structName: structName, type: type, typeMap: typeMap) + } + + writer.dedent() + writer.line("}") // close public struct + + return (filename, writer.output()) +} +``` + +- [ ] **Step 2: Add `containsInt256` helper** + +Insert directly below `generateLayered` (or above it, before `private static func generateFieldParsing`): + +```swift +private static func containsInt256(_ type: Resolver.TypeReference) -> Bool { + switch type { + case .int256: + return true + case .bareVector(let element), .boxedVector(let element): + return containsInt256(element) + case .int32, .int64, .double, .bytes, .string, .bool, .boolTrue, .bareConstructor, .boxedType: + return false + } +} +``` + +- [ ] **Step 3: Add `emitLayeredType` helper** + +Insert directly below `containsInt256`. This emits a single nested `public enum { ... }` with inline-args cases, a `serialize(_:_:)` method, and `fileprivate parse_*` methods. It mirrors the existing `generate`/`generateImplFile` logic for the type body but: +- renders with `useStructPattern = false` (no `Cons_*` wrapper) directly, +- drops `TypeConstructorDescription` conformance, +- drops the `descriptionFields()` method, +- uses `fileprivate` (not `public`) for `parse_*`, +- nests inside the outer struct writer's indent rather than using a `public extension`: + +```swift +private static func emitLayeredType( + writer: inout CodeWriter, + apiPrefix: String, + structName: String, + type: Resolver.SumType, + typeMap: [QualifiedName: Resolver.SumType] +) throws { + let sortedConstructors = type.constructors.values.sorted(by: { $0.name < $1.name }) + + let indirectPrefix = try type.hasDirectReference(to: [type], typeMap: typeMap) ? "indirect " : "" + writer.line("\(indirectPrefix)public enum \(type.name.value) {") + writer.indent() + + // case () -- inline-args shape + for constructor in sortedConstructors { + var argumentsString = "" + for argument in constructor.arguments { + if case .boolTrue = argument.type { continue } + if !argumentsString.isEmpty { argumentsString.append(", ") } + argumentsString.append(argument.name.camelCased) + argumentsString.append(": ") + // NOTE: layered generator uses structName (e.g. "SecretApi8") as the "apiPrefix" + // for nested-type references, because nested types live inside the struct. + argumentsString.append(typeReferenceRepresentation(structName, argument.type)) + if argument.condition != nil { argumentsString.append("?") } + } + writer.line("case \(constructor.name.value)\(argumentsString.isEmpty ? "" : "(\(argumentsString))")") + } + writer.line() + + // public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) + writer.line("public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {") + writer.indent() + writer.line("switch self {") + for constructor in sortedConstructors { + var bindString = "" + for argument in constructor.arguments { + if case .boolTrue = argument.type { continue } + if !bindString.isEmpty { bindString.append(", ") } + bindString.append("let ") + bindString.append(argument.name.camelCasedAndEscaped) + } + writer.line("case .\(constructor.name.value)\(bindString.isEmpty ? "" : "(\(bindString))"):") + writer.indent() + writer.line("if boxed {") + writer.indent() + writer.line("buffer.appendInt32(\(Int32(bitPattern: constructor.id)))") + writer.dedent() + writer.line("}") + + for argument in constructor.arguments { + if case .boolTrue = argument.type { continue } + var argumentAccessor = "\(argument.name.camelCasedAndEscaped)" + if let condition = argument.condition { + writer.line("if Int(\(condition.fieldName)) & Int(1 << \(condition.bitIndex)) != 0 {") + writer.indent() + argumentAccessor.append("!") + generateFieldSerialization(writer: &writer, argument: argument, argumentAccessor: argumentAccessor) + writer.dedent() + writer.line("}") + } else { + generateFieldSerialization(writer: &writer, argument: argument, argumentAccessor: argumentAccessor) + } + } + writer.line("break") + writer.dedent() + } + writer.line("}") + writer.dedent() + writer.line("}") + writer.line() + + // fileprivate static func parse_(_ reader: BufferReader) -> ? + for constructor in sortedConstructors { + writer.line("fileprivate static func parse_\(constructor.name.value)(_ reader: BufferReader) -> \(type.name.value)? {") + writer.indent() + if constructor.arguments.contains(where: { if case .boolTrue = $0.type { return false } else { return true } }) { + var argumentIndex = 0 + var argumentCheckString = "" + var argumentCollectionString = "" + for argument in constructor.arguments { + if case .boolTrue = argument.type { continue } + + writer.line("var _\(argumentIndex + 1): \(typeReferenceRepresentation(structName, argument.type))?") + + if let condition = argument.condition { + guard let fieldIndex = constructor.arguments.filter({ if case .boolTrue = $0.type { return false } else { return true } }).firstIndex(where: { $0.name == condition.fieldName }) else { + throw CodeGenerationError(text: "Condition field \(condition.fieldName) not found") + } + writer.line("if Int(_\(fieldIndex + 1)!) & Int(1 << \(condition.bitIndex)) != 0 {") + writer.indent() + try generateFieldParsing(apiPrefix: structName, writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)") + writer.dedent() + writer.line("}") + } else { + try generateFieldParsing(apiPrefix: structName, writer: &writer, typeMap: typeMap, argument: argument, argumentAccessor: "_\(argumentIndex + 1)") + } + + if !argumentCheckString.isEmpty { argumentCheckString.append(" && ") } + argumentCheckString.append("_c\(argumentIndex + 1)") + + if !argumentCollectionString.isEmpty { argumentCollectionString.append(", ") } + argumentCollectionString.append("\(argument.name.camelCased): _\(argumentIndex + 1)") + if argument.condition == nil { argumentCollectionString.append("!") } + + argumentIndex += 1 + } + + var checkIndex = 0 + for argument in constructor.arguments { + if case .boolTrue = argument.type { continue } + if let condition = argument.condition { + guard let fieldIndex = constructor.arguments.filter({ if case .boolTrue = $0.type { return false } else { return true } }).firstIndex(where: { $0.name == condition.fieldName }) else { + throw CodeGenerationError(text: "Condition field \(condition.fieldName) not found") + } + writer.line("let _c\(checkIndex + 1) = (Int(_\(fieldIndex + 1)!) & Int(1 << \(condition.bitIndex)) == 0) || _\(checkIndex + 1) != nil") + } else { + writer.line("let _c\(checkIndex + 1) = _\(checkIndex + 1) != nil") + } + checkIndex += 1 + } + + writer.line("if \(argumentCheckString) {") + writer.indent() + writer.line("return \(structName).\(type.name).\(constructor.name.value)\(argumentCollectionString.isEmpty ? "" : "(\(argumentCollectionString))")") + writer.dedent() + writer.line("}") + writer.line("else {") + writer.indent() + writer.line("return nil") + writer.dedent() + writer.line("}") + } else { + writer.line("return \(structName).\(type.name).\(constructor.name.value)") + } + writer.dedent() + writer.line("}") + } + + writer.dedent() + writer.line("}") + writer.line() +} +``` + +**IMPORTANT:** `typeReferenceRepresentation`, `generateFieldSerialization`, and `generateFieldParsing` take an `apiPrefix` parameter that they inject as a prefix on type names (e.g. `"Api.ChatFull"`). For layered output, the prefix becomes the per-layer struct name (`"SecretApi8"`), so inline-args like `media: SecretApi8.DecryptedMessageMedia` render correctly. We pass `structName` (not `apiPrefix`) to these helpers inside the layered emitter. + +Also: `camelCasedAndEscaped` is a private String extension at the top of `CodeGeneration.swift`. The layered emitter uses it as-is — no duplication. + +- [ ] **Step 4: Verify CodeGeneration.swift compiles** + +Run: `cd build-system/SwiftTL && swift build 2>&1 | head -50` +Expected: only `main.swift` errors (unchanged in this task). Any error inside `CodeGeneration.swift` itself means the emitter has a syntax/type issue — fix before proceeding. + +- [ ] **Step 5: Commit** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +git add build-system/SwiftTL/Sources/SwiftTL/CodeGeneration.swift +git commit -m "$(cat <<'EOF' +SwiftTL: add CodeGenerator.generateLayered for per-layer output + +Emits one {apiPrefix}Layer{N}.swift file per layer: file-scope +dispatch table, public struct {apiPrefix}{N} with parse/parseVector/ +serializeObject, nested public enums for each sum type using the +inline-args shape. Int256 dispatch entry emitted only when a layer's +constructors reference it. Reuses typeReferenceRepresentation / +generateFieldSerialization / generateFieldParsing unchanged, passing +the struct name as the apiPrefix so nested type refs render correctly. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 4: Wire `main.swift` to branch on `ParsedSchema` + +**Files:** +- Modify: `build-system/SwiftTL/Sources/SwiftTL/main.swift:47-98` + +**Goal:** Update the top-level `do { ... }` block to pattern-match on `ParsedSchema`. Flat path uses today's body unchanged; layered path iterates `resolveLayeredTypes` output and calls `generateLayered` once per layer. + +- [ ] **Step 1: Replace the `do { ... } catch let e { ... }` block** + +Replace the existing block from line 47 (`do {`) to line 98 (the closing `}` of the catch) with: + +```swift +do { + let parsedSchema = try DescriptionParser.parse(data: data) + + try FileManager.default.createDirectory(at: URL(fileURLWithPath: outputDirectoryPath), withIntermediateDirectories: true, attributes: nil) + + switch parsedSchema { + case let .flat(constructors, functions): + let resolvedTypes = try Resolver.resolveTypes(constructors: constructors) + var resolvedFunctions = try Resolver.resolveFunctions(types: resolvedTypes, functionDescriptions: functions) + + resolvedFunctions.append(Resolver.Function(name: QualifiedName(namespace: "help", value: "test"), id: 0xc0e202f7, arguments: [], result: .boxedType(QualifiedName(namespace: nil, value: "Bool")))) + + var constructorOrder: [(typeName: QualifiedName, constructorName: String)] = [] + var typeOrder: [(types: [(typeName: QualifiedName, constructorNames: [String])], functions: [QualifiedName])] = [] + + let sortedTypes = resolvedTypes.sorted(by: { $0.name < $1.name }) + + for type in sortedTypes { + for constructor in type.constructors.values.sorted(by: { $0.name < $1.name }) { + constructorOrder.append((type.name, constructor.name.value)) + } + } + + var totalConstructorCount = 0 + var currentConstructorCount = 0 + for type in sortedTypes { + if typeOrder.isEmpty || currentConstructorCount >= 32 { + typeOrder.append(([], [])) + currentConstructorCount = 0 + } + typeOrder[typeOrder.count - 1].types.append((type.name, type.constructors.values.sorted(by: { $0.name < $1.name }).map(\.name.value))) + currentConstructorCount += type.constructors.count + totalConstructorCount += type.constructors.count + if totalConstructorCount > 40 { } + } + + typeOrder.append(([], [])) + for function in resolvedFunctions.sorted(by: { $0.name < $1.name }) { + typeOrder[typeOrder.count - 1].functions.append(function.name) + } + + let generatedFiles = try CodeGenerator.generate(apiPrefix: apiPrefix, types: resolvedTypes, functions: resolvedFunctions, constructorOrder: constructorOrder, typeOrder: typeOrder) + + for (name, fileData) in generatedFiles { + let filePath = URL(fileURLWithPath: outputDirectoryPath).appendingPathComponent(name).path + let _ = try? FileManager.default.removeItem(atPath: filePath) + try fileData.write(toFile: filePath, atomically: true, encoding: .utf8) + } + + case let .layered(layers): + let resolvedLayers = try Resolver.resolveLayeredTypes(layers: layers) + for (layerNumber, types) in resolvedLayers { + let (filename, source) = try CodeGenerator.generateLayered(apiPrefix: apiPrefix, layerNumber: layerNumber, types: types) + let filePath = URL(fileURLWithPath: outputDirectoryPath).appendingPathComponent(filename).path + let _ = try? FileManager.default.removeItem(atPath: filePath) + try source.write(toFile: filePath, atomically: true, encoding: .utf8) + } + } +} catch let e { + print("\(e)") +} +``` + +Note the flat branch body is the same 40-odd lines that were in the original `do`, lightly reindented. The `createDirectory` call is hoisted above the switch since both branches need it. + +- [ ] **Step 2: Verify SwiftTL builds** + +Run: `cd build-system/SwiftTL && swift build 2>&1` +Expected: `Build complete!` with no errors. + +- [ ] **Step 3: Dry-run layered generation on secret_scheme.tl** + +```bash +cd /Users/isaac/build/telegram/telegram-ios/build-system/SwiftTL +rm -rf /tmp/swifttl-layered-out +swift run SwiftTL /Users/isaac/build/telegram/telegram-ios-shared/tools/secret_scheme.tl /tmp/swifttl-layered-out --api-prefix=SecretApi +ls /tmp/swifttl-layered-out/ +``` + +Expected output: 11 files named `SecretApiLayer{8,17,20,23,45,46,66,73,101,143,144}.swift`. If any are missing or extra, inspect the parser's layer-marker handling. If the tool errors, the error message should point at the offending layer or constructor. + +- [ ] **Step 4: Dry-run flat generation on swift_scheme.tl (regression check)** + +```bash +cd /Users/isaac/build/telegram/telegram-ios/build-system/SwiftTL +rm -rf /tmp/swifttl-flat-out +swift run SwiftTL /Users/isaac/build/telegram/telegram-ios-shared/tools/swift_scheme.tl /tmp/swifttl-flat-out +ls /tmp/swifttl-flat-out/ +diff -q /tmp/swifttl-flat-out /Users/isaac/build/telegram/telegram-ios/submodules/TelegramApi/Sources/ 2>&1 | grep -E "^(Only in|Files)" | head +``` + +Expected: 6 files (`Api0.swift` through `Api5.swift`). The diff against `submodules/TelegramApi/Sources/` should show only "Only in submodules" entries for non-`Api*.swift` files (e.g. `SecretApiLayer*.swift`, `Api+*.swift` helpers). `Api0.swift`-`Api5.swift` must either be identical or show only trivially-different content — any structural diff would indicate a regression in the flat pipeline that must be fixed before proceeding. + +- [ ] **Step 5: Commit** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +git add build-system/SwiftTL/Sources/SwiftTL/main.swift +git commit -m "$(cat <<'EOF' +SwiftTL: main.swift branches on ParsedSchema + +Flat schemas keep the existing generate(...) pipeline. Layered +schemas iterate resolveLayeredTypes and write one +{apiPrefix}Layer{N}.swift per layer via generateLayered. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 5: Regenerate `SecretApiLayer*.swift` and verify full project build + +**Files:** +- Overwrite: `submodules/TelegramApi/Sources/SecretApiLayer{8,46,73,101,144}.swift` (existing, hand-written) +- Create: `submodules/TelegramApi/Sources/SecretApiLayer{17,20,23,45,66,143}.swift` (new) + +**Goal:** Regenerate all 11 layer files using the new SwiftTL and confirm the entire iOS project still compiles. Downstream consumers (`ManagedSecretChatOutgoingOperations.swift`, `ProcessSecretChatIncomingDecryptedOperations.swift`) already reference `SecretApi{8,46,73,101,144}..` symbols; they must continue to resolve. + +- [ ] **Step 1: Pre-flight — snapshot current state of `submodules/TelegramApi/Sources/SecretApiLayer*.swift`** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +ls submodules/TelegramApi/Sources/SecretApiLayer*.swift +git log --oneline -5 -- submodules/TelegramApi/Sources/SecretApiLayer*.swift +``` + +Expected: 5 files (`SecretApiLayer{8,46,73,101,144}.swift`), each tracked by git. If there are uncommitted modifications to any of these files, stop and investigate — they should be clean before regeneration. + +- [ ] **Step 2: Regenerate into a staging directory** + +```bash +cd /Users/isaac/build/telegram/telegram-ios/build-system/SwiftTL +rm -rf /tmp/secretapi-staging +swift run SwiftTL /Users/isaac/build/telegram/telegram-ios-shared/tools/secret_scheme.tl /tmp/secretapi-staging --api-prefix=SecretApi +ls /tmp/secretapi-staging/ +``` + +Expected: 11 files, one per layer. + +- [ ] **Step 3: Spot-check layer 8 output against the hand-written file** + +```bash +diff /tmp/secretapi-staging/SecretApiLayer8.swift /Users/isaac/build/telegram/telegram-ios/submodules/TelegramApi/Sources/SecretApiLayer8.swift | head -80 +``` + +Expected: cosmetic differences (whitespace, maybe case ordering) but each enum case name must appear in both; each `buffer.appendInt32()` must have the same ID in both files for the same constructor; the `parsers` dict must contain the same set of `dict[]` entries. If the generator added a `Bool` type (from the pre-marker `boolFalse`/`boolTrue`), that's an expected addition per the spec — not a failure. + +If any *constructor ID* or *enum case name* differs for an existing constructor, STOP. That means either the schema's legacy hand-written content drifted from the `.tl` source (spec risk section covers this — schema wins) or the generator has a bug. Decide on the fly: if the legacy hand-written file has a typo'd ID, the regenerated file is correct and should land as-is. If the generator has a bug, fix before proceeding. + +- [ ] **Step 4: Copy staging output into place** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +rm -f submodules/TelegramApi/Sources/SecretApiLayer*.swift +cp /tmp/secretapi-staging/SecretApiLayer*.swift submodules/TelegramApi/Sources/ +ls submodules/TelegramApi/Sources/SecretApiLayer*.swift +``` + +Expected: 11 files, one per layer (5 overwrites + 6 new). + +- [ ] **Step 5: Full Bazel build** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +source ~/.zshrc 2>/dev/null +python3 build-system/Make/Make.py build --continueOnError 2>&1 | tee /tmp/swifttl-wave-build.log | tail -40 +``` + +Expected: build completes with no errors. The `Telegram.ipa` target builds successfully. If compile errors surface, they will most likely be in `ManagedSecretChatOutgoingOperations.swift` or `ProcessSecretChatIncomingDecryptedOperations.swift` — cases where a specific `SecretApi{N}..` symbol the consumer expects doesn't appear in the regenerated file. Triage: + +- If the missing symbol is a constructor present in `secret_scheme.tl` under some layer: verify the resolver captured it correctly in the snapshot for that layer. Likely a bug in `resolveLayeredTypes` (e.g. pre-marker handling) or in the emitter (e.g. case-name mis-generation). Fix in SwiftTL and regenerate. +- If the missing symbol names a constructor NOT in `secret_scheme.tl` under that layer's cumulative set: the hand-written file and the consumer code drifted from the schema. Not a generator bug. Escalate with the user before modifying consumer code. + +- [ ] **Step 6: Commit regenerated files** + +```bash +cd /Users/isaac/build/telegram/telegram-ios +git add submodules/TelegramApi/Sources/SecretApiLayer*.swift +git status --short submodules/TelegramApi/Sources/ +git commit -m "$(cat <<'EOF' +TelegramApi: regenerate SecretApiLayer*.swift via SwiftTL + +Replaces the hand-written layer 8/46/73/101/144 files with SwiftTL +output and adds the previously-unpublished layers 17/20/23/45/66/143. +Per-layer struct names (SecretApi8, SecretApi46, ...) and public +enum case signatures are unchanged; downstream consumers +(ManagedSecretChatOutgoingOperations, ProcessSecretChatIncomingDecryptedOperations) +compile unchanged. + +BUILD uses glob("Sources/**/*.swift") so no BUILD update required. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 6: Final flat-path regression check + +**Files:** none modified — this is a verification-only task. + +**Goal:** Confirm the flat pipeline is structurally unchanged — `Api*.swift` files regenerated from `swift_scheme.tl` match what's currently committed in `submodules/TelegramApi/Sources/`. + +- [ ] **Step 1: Regenerate `Api*.swift` into staging** + +```bash +cd /Users/isaac/build/telegram/telegram-ios/build-system/SwiftTL +rm -rf /tmp/api-flat-staging +swift run SwiftTL /Users/isaac/build/telegram/telegram-ios-shared/tools/swift_scheme.tl /tmp/api-flat-staging +ls /tmp/api-flat-staging/ +``` + +Expected: `Api0.swift` through `Api5.swift` (exact count depends on the schema's constructor count; 6 files is the current count). + +- [ ] **Step 2: Diff against committed flat output** + +```bash +for f in /tmp/api-flat-staging/Api*.swift; do + base=$(basename "$f") + diff -q "$f" "/Users/isaac/build/telegram/telegram-ios/submodules/TelegramApi/Sources/$base" || echo "DIFFERS: $base" +done +``` + +Expected: every file identical to the committed version, or at most whitespace differences. Any structural diff (different enum cases, different IDs, different method signatures) is a regression in the flat pipeline introduced by this wave's edits — must be fixed. + +- [ ] **Step 3: (If no diff) confirm in conversation** + +If step 2 reports all files identical, the wave is complete — note in the PR description / commit log that the flat pipeline is verified unchanged. No further commit. + +- [ ] **Step 4: (If diff found) investigate and fix** + +If diffs exist: most likely cause is that a shared helper in `CodeGeneration.swift` was touched with an effect that cascades into `generate(...)`. Revisit Task 3 and ensure all edits added new methods only, with no modifications to `generate`, `generateMainFile`, `generateImplFile`, or the top-level `CodeGenerator` API. Fix and re-run step 2. + +--- + +## Out-of-scope follow-ups (do not execute in this plan) + +Documented in the spec; not part of this plan's delivered scope. + +- Update `/Users/isaac/build/telegram/telegram-ios-shared/tools/generate_and_copy_scheme.sh` to `rm -f` and `cp` the `SecretApiLayer*.swift` output alongside the existing `Api*.swift` copy step. Lives in a sibling repo; the user handles when ready. +- Delete `build-system/SwiftTL/Sources/SwiftTL/LegacyOrderParser.swift` was ALREADY deleted in the uncommitted working tree (per `git status` at plan writing time — `D Sources/SwiftTL/LegacyOrderParser.swift`). Not in this plan; the deletion can land with Task 1 if still convenient or as a separate cleanup. diff --git a/docs/superpowers/plans/2026-04-21-textstyleeditscreen-caret-tracking.md b/docs/superpowers/plans/2026-04-21-textstyleeditscreen-caret-tracking.md new file mode 100644 index 0000000000..d404d91583 --- /dev/null +++ b/docs/superpowers/plans/2026-04-21-textstyleeditscreen-caret-tracking.md @@ -0,0 +1,351 @@ +# TextStyleEditScreen caret-tracking Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** On every text change inside `TextStyleEditScreen`, scroll the enclosing `ResizableSheetComponent` scroll view so the caret in the active `ListMultilineTextFieldItemComponent` stays visible ~24pt above the keyboard/bottom button area. + +**Architecture:** Single-file change in `submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift`. Give each text field a `ListMultilineTextFieldItemComponent.Tag`; at the end of `TextStyleEditContentComponent.View.update(...)`, read `TextFieldComponent.AnimationHint` off the transition's userData; on a `.textChanged` hint, resolve the editing field, compute the caret rect via `UITextInput.caretRect(for:)`, walk `superview` to the enclosing `UIScrollView`, and adjust its `bounds.origin.y` using the direct-assign + additive-animate pattern from `ComposePollScreen.swift:2873-2895`. + +**Tech Stack:** Swift, UIKit, Telegram's ComponentFlow (`ComponentView`, `ComponentTransition`, `TextFieldComponent.AnimationHint`), Bazel via `Make.py`. No unit tests exist in this project — verification is a full build + manual smoke test per `CLAUDE.md`. + +**Reference spec:** `docs/superpowers/specs/2026-04-21-textstyleeditscreen-caret-tracking-design.md`. + +**Reference precedent:** `submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift:2733-2895` (field-bounds variant of this same pattern). + +--- + +## File Structure + +Only one file is touched: + +- **Modify:** `submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift` + - Add two stored `ListMultilineTextFieldItemComponent.Tag` properties on `TextStyleEditContentComponent.View`. + - Thread those tags into the two existing `ListMultilineTextFieldItemComponent(...)` constructions inside `update(...)`. + - Add a private `recenterCaret(hintView:transition:)` method on `TextStyleEditContentComponent.View`. + - Call `recenterCaret` from the tail of `update(...)` when the transition carries a `.textChanged` `TextFieldComponent.AnimationHint`. + +No other files are modified. Public API of `ResizableSheetComponent`, `ListMultilineTextFieldItemComponent`, and `TextFieldComponent` is used as-is. + +--- + +## Task 1: Add field tags and wire them into the two text field constructors + +**Files:** +- Modify: `submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift` (around lines 64-77, 277, 322) + +- [ ] **Step 1: Add the two `Tag` stored properties to `TextStyleEditContentComponent.View`** + +In `TextStyleEditScreen.swift`, locate the stored-property block at the top of `final class View: UIView` (lines 64-77). Below `private let linkOption = ComponentView()` (line 76) add: + +```swift + private let titleFieldTag = ListMultilineTextFieldItemComponent.Tag() + private let textFieldTag = ListMultilineTextFieldItemComponent.Tag() +``` + +Keep them above the `override init(frame: CGRect)` at line 78. + +- [ ] **Step 2: Pass `self.titleFieldTag` into the title field constructor** + +Locate the `ListMultilineTextFieldItemComponent(...)` construction for the title section (starts at line 260). Its last argument currently reads `tag: nil` (line 277). Change it to: + +```swift + tag: self.titleFieldTag +``` + +- [ ] **Step 3: Pass `self.textFieldTag` into the prompt field constructor** + +Locate the second `ListMultilineTextFieldItemComponent(...)` construction for the text section (starts at line 304). Its last argument currently reads `tag: nil` (line 322). Change it to: + +```swift + tag: self.textFieldTag +``` + +- [ ] **Step 4: Verify the change compiles** + +Run: + +```bash +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 \ + --continueOnError +``` + +Expected: build succeeds (or the same pre-existing failures unrelated to `TextStyleEditScreen.swift`). A failure in `TextStyleEditScreen.swift` means the tag types or property names are wrong — fix before moving on. + +- [ ] **Step 5: Do not commit yet** — tag wiring is inert without the recenter logic. Defer commit to Task 4. + +--- + +## Task 2: Add the `recenterCaret` helper + +**Files:** +- Modify: `submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift` + +- [ ] **Step 1: Add the method on `TextStyleEditContentComponent.View`** + +Inside `final class View: UIView` (the class that starts at line 64), directly **before** the `func update(component:availableSize:state:environment:transition:)` method (line 86), add this private method. It covers steps 1–6 from the design spec (locate field view → caret rect → scroll view → scroll view coordinates → visible region → adjust bounds). + +```swift + private func recenterCaret(hintView: UIView, transition: ComponentTransition) { + var fieldView: ListMultilineTextFieldItemComponent.View? + var ancestor: UIView? = hintView + while let current = ancestor { + if let candidate = current as? ListMultilineTextFieldItemComponent.View { + fieldView = candidate + break + } + ancestor = current.superview + } + guard let fieldView else { + return + } + if !(fieldView.matches(tag: self.titleFieldTag) || fieldView.matches(tag: self.textFieldTag)) { + return + } + guard let inputTextView = fieldView.textFieldView?.inputTextView else { + return + } + let caretPosition = inputTextView.selectedTextRange?.end ?? inputTextView.endOfDocument + let caretRect = inputTextView.caretRect(for: caretPosition) + if caretRect.isNull || caretRect.isInfinite { + return + } + + var scrollAncestor: UIView? = self.superview + var scrollView: UIScrollView? + while let current = scrollAncestor { + if let candidate = current as? UIScrollView { + scrollView = candidate + break + } + scrollAncestor = current.superview + } + guard let scrollView, let environment = self.environment else { + return + } + + let caretInScroll = inputTextView.convert(caretRect, to: scrollView) + + let bottomActionAreaHeight: CGFloat = 60.0 + let caretTopInset: CGFloat = 24.0 + let caretBottomInset: CGFloat = 24.0 + let visibleTop = scrollView.bounds.minY + caretTopInset + let visibleBottom = scrollView.bounds.maxY - environment.inputHeight - bottomActionAreaHeight - caretBottomInset + + let previousBounds = scrollView.bounds + var newBounds = previousBounds + if caretInScroll.maxY > visibleBottom { + newBounds.origin.y += (caretInScroll.maxY - visibleBottom) + } else if caretInScroll.minY < visibleTop { + newBounds.origin.y -= (visibleTop - caretInScroll.minY) + } + let maxOriginY = max(0.0, scrollView.contentSize.height - scrollView.bounds.height) + newBounds.origin.y = min(max(0.0, newBounds.origin.y), maxOriginY) + + if newBounds != previousBounds { + scrollView.bounds = newBounds + if !transition.animation.isImmediate { + let offsetY = previousBounds.origin.y - newBounds.origin.y + transition.animateBoundsOrigin(view: scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + } +``` + +Notes on key choices: + +- `bottomActionAreaHeight: 60.0` = `52.0` (bottom item height — see `ResizableSheetComponent.swift:750`) + `8.0` gap above the button (matches `ResizableSheetComponent.swift:732`). +- `caretTopInset` / `caretBottomInset` (both `24.0`) provide the "small inset" biased positioning the user confirmed. +- The hint's view ancestor walk is used (rather than `self.titleFieldTag`'s / `self.textFieldTag`'s views directly) because the hint already carries the `TextFieldComponent.View` that actually fired the change — this is safer than guessing which of our two fields is editing when both may have briefly claimed focus. +- `transition.animateBoundsOrigin` is the proven pattern from `ComposePollScreen.swift:2891-2894`; `transition.animation.isImmediate` gating avoids an unnecessary animation when the transition is immediate. +- Silent bails on missing scroll view or text view keep the code robust against host refactors (they should never happen in normal operation). + +- [ ] **Step 2: Verify compilation** + +Re-run the build command from Task 1 Step 4. Expected: the method compiles cleanly. Common failure modes to watch for: + +- `cannot find 'ListMultilineTextFieldItemComponent.View' in scope` → wrong type path; check the import and the class name in `ListMultilineTextFieldItemComponent.swift:196` (it is the nested `View` class of `ListMultilineTextFieldItemComponent`). +- `value of type 'TextFieldComponent.View' has no member 'inputTextView'` → the property is defined at `TextFieldComponent.swift:359`; ensure you're reading `fieldView.textFieldView?.inputTextView`, not reaching into private internals. +- `'ComponentTransition' has no member 'animateBoundsOrigin'` → this is a ComponentFlow method; grep confirms it exists and is used at `ComposePollScreen.swift:2893`. If missing, the import line (`import ComponentFlow`) at file top is the place to check. + +- [ ] **Step 3: Do not commit yet** — the helper is unreferenced and unused. Defer commit to Task 4. + +--- + +## Task 3: Hook up the `.textChanged` trigger in `update(...)` + +**Files:** +- Modify: `submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift` + +- [ ] **Step 1: Add the trigger at the tail of `update(...)`** + +At the end of `func update(component:availableSize:state:environment:transition:)` on `TextStyleEditContentComponent.View`, locate lines 455-460: + +```swift + contentHeight += 104.0 + + let _ = alphaTransition + + return CGSize(width: availableSize.width, height: contentHeight) +``` + +Insert the trigger block **before** `return`: + +```swift + contentHeight += 104.0 + + let _ = alphaTransition + + if let hint = transition.userData(TextFieldComponent.AnimationHint.self), case .textChanged = hint.kind, let hintView = hint.view { + self.recenterCaret(hintView: hintView, transition: transition) + } + + return CGSize(width: availableSize.width, height: contentHeight) +``` + +Do NOT match on `.textFocusChanged` — per the user's requirement, scrolling fires only on text edits. + +- [ ] **Step 2: Ensure `TextFieldComponent` is importable** + +`TextFieldComponent.AnimationHint` is vended from the `TextFieldComponent` module. Check the file's import list at the top (lines 1-25). `TextFieldComponent` is used transitively today via `ListMultilineTextFieldItemComponent`, but the type is only re-exposed if we explicitly import it. + +Locate the import block (around lines 1-25). If `import TextFieldComponent` is not present, add it alphabetically — for example, between `import ResizableSheetComponent` and `import TelegramCore`: + +```swift +import TextFieldComponent +``` + +If it is already present, skip this sub-step. + +- [ ] **Step 3: Ensure the BUILD dep is present** + +Locate the sibling `BUILD` file: + +```bash +cat submodules/TelegramUI/Components/TextProcessingScreen/BUILD +``` + +Look for `//submodules/TelegramUI/Components/TextFieldComponent:TextFieldComponent` in the `deps` list. If present, skip to the next step. If absent, add it to the `deps` array (preserving alphabetical order where the BUILD file follows that convention). For example: + +``` + "//submodules/TelegramUI/Components/TextFieldComponent:TextFieldComponent", +``` + +- [ ] **Step 4: Verify compilation** + +Re-run the build command from Task 1 Step 4. + +Expected: clean build for `TextStyleEditScreen.swift` and its host module (`TextProcessingScreen`). Common failure modes: + +- `cannot find 'TextFieldComponent' in scope` → missing `import TextFieldComponent` (fix in Step 2). +- Bazel link error naming `TextFieldComponent` → missing BUILD dep (fix in Step 3). +- `instance method requires the types 'X' and 'Y' to be equivalent` on the `case .textChanged = hint.kind` line → the `case let` pattern binding; verify with `grep -n 'case \\.textChanged' submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift` that the case is payload-less (it is, per `TextFieldComponent.swift:95-103` where `Kind` declares `case textChanged` without associated values and `case textFocusChanged(isFocused: Bool)` with one). + +- [ ] **Step 5: Do not commit yet** — verify end-to-end behavior in Task 4 first. + +--- + +## Task 4: Manual smoke test and commit + +**Files:** +- Modify (commit): `submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift` +- Possibly modify (commit): `submodules/TelegramUI/Components/TextProcessingScreen/BUILD` + +- [ ] **Step 1: Launch the app on the simulator** + +Run: + +```bash +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 +``` + +Expected: `Telegram.ipa` target built successfully, 0 errors. + +Note: this project has no unit tests; feature correctness for UI changes requires a manual check on device or simulator. Install the built app on the iOS simulator (`xcrun simctl install booted ...` if not done by the build script) and navigate to the AI style-edit sheet — this is typically reached from a chat's AI compose-mode style selector or from Settings, depending on build flavour. If the entry point is unclear, grep for `TextStyleEditScreen(` to find a test harness or the production call site: + +```bash +grep -rn "TextStyleEditScreen(" submodules --include="*.swift" +``` + +- [ ] **Step 2: Smoke test — short content path** + +1. Tap the "Style Name" field. Confirm the keyboard slides up and the "Create" button rides above the keyboard (pre-existing behavior from the earlier `inputHeight` work). +2. Type one character. With short content no scroll should occur; the scroll view should remain at origin zero (visual check: the emoji icon at the top stays visible). + +Pass criterion: no visual regression; the title field is visible and typable. + +- [ ] **Step 3: Smoke test — long prompt path** + +1. Tap the "Instructions" field. +2. Type enough text (or paste a paragraph) to make the prompt field taller than the viewport with the keyboard up. +3. Continue typing so new characters appear at the caret. + +Pass criterion: as each newline is added, the caret stays approximately 24pt above the keyboard/button area. The field's top may scroll out of view — that's expected. + +- [ ] **Step 4: Smoke test — manual-scroll-then-type** + +1. Still in the "Instructions" field with enough content that scroll is possible. +2. Manually drag the sheet content up so the caret is pushed above the visible area. +3. Type one character. + +Pass criterion: the scroll view snaps downward so the caret is visible again, above the keyboard with the configured inset. + +- [ ] **Step 5: Smoke test — edit-mode mid-field tap (non-goal regression check)** + +1. Trigger the screen in edit mode on a style with a long pre-populated prompt (enough text to exceed the viewport). +2. Tap **in the middle** of the prompt so the caret lands off-screen-top (no text change). + +Pass criterion: **no** scroll occurs (this is per the non-goal — we only scroll on text change). A follow-up text edit is expected to trigger a scroll; that is covered by Step 3. + +- [ ] **Step 6: Check for regressions in adjacent flows** + +Briefly exercise: + +1. The emoji-selection sheet (tap the big round emoji area at the top) — must still open, select, and dismiss without issue. +2. The "Add a link to my account" checkbox — toggling still flips the check. +3. The "Delete Style" row (edit mode) — still pushes the confirm alert. + +Pass criterion: all three work as before. + +- [ ] **Step 7: Commit** + +```bash +git add submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift +# Only stage the BUILD file if it was modified in Task 3 Step 3: +git status --short submodules/TelegramUI/Components/TextProcessingScreen/BUILD +# If the BUILD file shows up modified, stage it too: +git add submodules/TelegramUI/Components/TextProcessingScreen/BUILD +git commit -m "$(cat <<'EOF' +TextStyleEditScreen: scroll caret into view on text change + +Tag both ListMultilineTextFieldItemComponents and, at the tail of +TextStyleEditContentComponent.View.update(...), read TextFieldComponent. +AnimationHint off the transition userData. On a .textChanged hint, locate +the editing field, compute the caret rect, walk up to the enclosing +ResizableSheetComponent scroll view, and adjust bounds.origin.y so the +caret sits ~24pt above the keyboard/bottom action area. + +Scroll runs only on text edits (not on focus/selection changes) per spec. +Uses the direct-assign + additive-animate pattern from ComposePollScreen. +EOF +)" +``` + +Expected: commit succeeds. The diff is ~50 lines added across one .swift file (and possibly one line added to BUILD). + +--- + +## Out-of-scope / follow-ups + +None planned. The non-goals called out in the spec (scroll on focus change, scroll on selection change, scroll on keyboard show/hide independently of a text edit) are intentional omissions, not deferred work. + +If manual smoke testing reveals that focus-gain keyboard appearance creates a bad UX (user taps a field near the bottom and the keyboard covers it until they type), consider adding back the `.textFocusChanged(isFocused: true)` case in the trigger block. That is a one-line change to the conditional in Task 3 Step 1 and does not require any design iteration. diff --git a/docs/superpowers/plans/2026-04-24-contactlistpeer-engine-peer-migration.md b/docs/superpowers/plans/2026-04-24-contactlistpeer-engine-peer-migration.md new file mode 100644 index 0000000000..8732b7a790 --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-contactlistpeer-engine-peer-migration.md @@ -0,0 +1,944 @@ +# Wave 36: `ContactListPeer.peer: Peer → EnginePeer` Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate the public enum case `ContactListPeer.peer(peer: Peer, isGlobal: Bool, participantCount: Int32?)` from the Postbox `Peer` protocol to the TelegramCore `EnginePeer` enum in a single atomic commit. Cascading changes: change `ContactListPeer.indexName` return type from `PeerIndexNameRepresentation` to `EnginePeer.IndexName` (drops 2 `EnginePeer.IndexName(...)` wraps at one call site); rewrite the enum's custom `==` to use `EnginePeer`'s synthesized Equatable; drop 20 outflow `._asPeer()` bridges, 16 inflow `EnginePeer(peer)` wraps; rewrite 2 Postbox-concrete cast chains to EnginePeer case patterns. + +**Architecture:** One atomic commit. The enum-case payload change is necessarily atomic. `ContactListPeer` lives in `submodules/AccountContext/Sources/ContactSelectionController.swift`; 7 consumer files touched in addition. 2 consumer files verified untouched (`ComposeController.swift`, `ChatSendAudioMessageContextPreview.swift`). No new wrappers, no new typealiases. `import Postbox` stays in every touched consumer (follow-up unused-import sweep handles it). + +**Tech Stack:** Swift, Bazel build via Make.py wrapper. No tests — verification is build success + targeted grep checks. + +**Spec:** `docs/superpowers/specs/2026-04-24-contactlistpeer-engine-peer-migration-design.md` + +--- + +## File Structure + +**Modified files (8 expected — 1 definition + 7 consumer. Plus 2 verify-only.)** + +| File | Edits | Categories | +|---|---|---| +| `submodules/AccountContext/Sources/ContactSelectionController.swift` | 3 (case type + indexName return type + `==` body) | α | +| `submodules/ContactListUI/Sources/ContactListNode.swift` | ~21 (12 outflow + 4 inflow + 2 cast rewrites [L182-186, L1968] + 2 IndexName wraps [L517]) | β + δ + φ + ε′ | +| `submodules/ContactListUI/Sources/ContactsController.swift` | 1 (inflow wrap at L294) | δ | +| `submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift` | 7 (3 outflow + 4 inflow) | β + δ | +| `submodules/TelegramUI/Sources/ContactMultiselectionController.swift` | 6 (2 outflow + 4 inflow) | β + δ | +| `submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift` | 2 (1 outflow + 1 inflow) | β + δ | +| `submodules/TelegramUI/Sources/ContactSelectionController.swift` | 2 (inflow wraps L517/527) | δ | +| `submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift` | 2 (outflow bridges L160/230) | β | + +**Verify-only (no edits expected):** + +| File | Reason | +|---|---| +| `submodules/TelegramUI/Sources/ComposeController.swift` | Destructures at L120/160 access `.id` only. Same-type access works on EnginePeer. | +| `submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift` | Only holds `[ContactListPeer]` at collection level; no `.peer` destructures. | + +**EnginePeer enum case mapping (used in cast rewrites):** + +| Postbox concrete | EnginePeer case | +|---|---| +| `TelegramUser` | `.user(TelegramUser)` | +| `TelegramGroup` | `.legacyGroup(TelegramGroup)` | +| `TelegramChannel` | `.channel(TelegramChannel)` | + +**Sites that stay as `._asPeer()` bridges (NOT in wave scope):** + +- `submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift:488, 528, 562` — `canSendMessagesToPeer(peer._asPeer())` / `canSendMessagesToPeer(peer.peer._asPeer())`. `canSendMessagesToPeer(_: Peer)` migration is a deferred future wave. +- `submodules/TelegramUI/Sources/ContactMultiselectionController.swift:171, 201, 748` — `peerTokenTitle(accountPeerId:..., peer: peer._asPeer(), strings:...)`. `peerTokenTitle(peer: Peer)` migration is out of scope. + +--- + +## Task 1: Edit `AccountContext/Sources/ContactSelectionController.swift` — definition + +**Files:** +- Modify: `submodules/AccountContext/Sources/ContactSelectionController.swift` + +Foundational change. Without it, none of the consumer edits compile. + +- [ ] **Step 1.1: Update the case payload type, `indexName` return type, and `==` operator body** + +Edit using the Edit tool: + +```swift +// OLD (lines 61-99) +public enum ContactListPeer: Equatable { + case peer(peer: Peer, isGlobal: Bool, participantCount: Int32?) + case deviceContact(DeviceContactStableId, DeviceContactBasicData) + + public var id: ContactListPeerId { + switch self { + case let .peer(peer, _, _): + return .peer(peer.id) + case let .deviceContact(id, _): + return .deviceContact(id) + } + } + + public var indexName: PeerIndexNameRepresentation { + switch self { + case let .peer(peer, _, _): + return peer.indexName + case let .deviceContact(_, contact): + return .personName(first: contact.firstName, last: contact.lastName, addressNames: [], phoneNumber: "") + } + } + + public static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool { + switch lhs { + case let .peer(lhsPeer, lhsIsGlobal, lhsParticipantCount): + if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs, lhsPeer.isEqual(rhsPeer), lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount { + return true + } else { + return false + } + case let .deviceContact(id, contact): + if case .deviceContact(id, contact) = rhs { + return true + } else { + return false + } + } + } +} +``` + +```swift +// NEW +public enum ContactListPeer: Equatable { + case peer(peer: EnginePeer, isGlobal: Bool, participantCount: Int32?) + case deviceContact(DeviceContactStableId, DeviceContactBasicData) + + public var id: ContactListPeerId { + switch self { + case let .peer(peer, _, _): + return .peer(peer.id) + case let .deviceContact(id, _): + return .deviceContact(id) + } + } + + public var indexName: EnginePeer.IndexName { + switch self { + case let .peer(peer, _, _): + return peer.indexName + case let .deviceContact(_, contact): + return .personName(first: contact.firstName, last: contact.lastName, addressNames: [], phoneNumber: "") + } + } + + public static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool { + switch lhs { + case let .peer(lhsPeer, lhsIsGlobal, lhsParticipantCount): + if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs, lhsPeer == rhsPeer, lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount { + return true + } else { + return false + } + case let .deviceContact(id, contact): + if case .deviceContact(id, contact) = rhs { + return true + } else { + return false + } + } + } +} +``` + +Three changes in this edit: +1. Line 62: `peer: Peer` → `peer: EnginePeer` +2. Line 74: return type `PeerIndexNameRepresentation` → `EnginePeer.IndexName` +3. Line 86 (inside the `==` operator): `lhsPeer.isEqual(rhsPeer)` → `lhsPeer == rhsPeer` + +`EnginePeer.IndexName.personName(first:last:addressNames:phoneNumber:)` has the same labels/types as `PeerIndexNameRepresentation.personName`, so line 79 body is untouched — only its return target enum changes. + +- [ ] **Step 1.2: Verify** + +Run: + +```bash +grep -nE "case peer\(peer:|public var indexName:|\.isEqual\(" submodules/AccountContext/Sources/ContactSelectionController.swift +``` + +Expected output: +- Line 62: `case peer(peer: EnginePeer, ...)` +- Line 74: `public var indexName: EnginePeer.IndexName {` +- No `isEqual(` match on the `==` path (the only remaining occurrences would be unrelated). + +Do not commit yet. + +--- + +## Task 2: Edit `ContactListNode.swift` — largest consumer, multi-category + +**Files:** +- Modify: `submodules/ContactListUI/Sources/ContactListNode.swift` + +Most changes happen here: 12 outflow bridges + 4 inflow wraps + 2 cast chain rewrites + 2 IndexName wrap drops. + +- [ ] **Step 2.1: Drop the 12 outflow `._asPeer()` bridges via `replace_all`** + +All 12 `._asPeer()` bridges at ContactListPeer.peer construction sites follow the shape `._asPeer(), isGlobal:`. Non-construction `._asPeer()` uses in this file (if any) feed other functions and do NOT use this exact substring. + +Pre-flight verify: + +```bash +grep -cE "\._asPeer\(\), isGlobal:" submodules/ContactListUI/Sources/ContactListNode.swift +``` + +Expected: `12`. + +If the count is 12, apply the Edit tool with `replace_all=true`: +- `old_string`: `._asPeer(), isGlobal:` +- `new_string`: `, isGlobal:` + +If the count is not 12, fall back to per-site Edits at lines 632, 690, 701, 747, 765, 1365, 1647, 1656, 1693, 1731, 1942, 1944 using enough surrounding context to make each `old_string` unique. + +- [ ] **Step 2.2: Verify the 12 outflow drops** + +Run: + +```bash +grep -nE "\._asPeer\(\), isGlobal:" submodules/ContactListUI/Sources/ContactListNode.swift +``` + +Expected: zero matches. + +- [ ] **Step 2.3: Drop 2 inflow wraps at L204** + +Read lines 200–210 first to confirm the line text. + +Edit: + +```swift +// OLD (line 204) + itemPeer = .peer(peer: EnginePeer(peer), chatPeer: EnginePeer(peer)) +``` + +```swift +// NEW + itemPeer = .peer(peer: peer, chatPeer: peer) +``` + +- [ ] **Step 2.4: Drop 1 inflow wrap at L252** + +Read lines 248–256 first to confirm. + +Edit: + +```swift +// OLD (line 252) + interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic) +``` + +```swift +// NEW + interaction.openDisabledPeer(peer, requiresPremiumForMessaging ? .premiumRequired : .generic) +``` + +- [ ] **Step 2.5: Drop 1 inflow wrap at L844** + +Read lines 840–848 first to confirm. + +Edit: + +```swift +// OLD (line 844) + if let isPeerEnabled, !isPeerEnabled(EnginePeer(peer)) { +``` + +```swift +// NEW + if let isPeerEnabled, !isPeerEnabled(peer) { +``` + +- [ ] **Step 2.6: Rewrite the L182-186 cast chain to EnginePeer case patterns** + +Read lines 176–200 first. The cast chain is inside the ContactListPeer.peer destructure at line 177. + +Edit: + +```swift +// OLD (lines 182-186) + } else { + if let _ = peer as? TelegramUser { + status = .presence(presence ?? EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0), dateTimeFormat) + } else if let group = peer as? TelegramGroup { + status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount))), multiline: false, isActive: false, icon: nil) + } else if let channel = peer as? TelegramChannel { +``` + +```swift +// NEW + } else { + if case .user = peer { + status = .presence(presence ?? EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0), dateTimeFormat) + } else if case let .legacyGroup(group) = peer { + status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount))), multiline: false, isActive: false, icon: nil) + } else if case let .channel(channel) = peer { +``` + +`channel.info` access inside the surviving inner block continues to compile unchanged (`EnginePeer.channel` wraps `TelegramChannel`). `group.participantCount` inside the `legacyGroup` branch works identically. The first branch doesn't bind the user — the `case .user = peer` form preserves that. + +- [ ] **Step 2.7: Rewrite the L1968 cast to an EnginePeer case pattern** + +Read lines 1964–1976 first. The cast is inside the ContactListPeer.peer destructure at line 1966. + +Edit: + +```swift +// OLD (lines 1967-1968) + if requirePhoneNumbers, + let user = peer as? TelegramUser { +``` + +```swift +// NEW + if requirePhoneNumbers, + case let .user(user) = peer { +``` + +`user.phone` on the following line continues to compile (`EnginePeer.user` wraps `TelegramUser`). + +- [ ] **Step 2.8: Drop 2 IndexName wraps at L517** + +Read lines 515–522 first. + +Edit: + +```swift +// OLD (line 517) + let result = EnginePeer.IndexName(lhs.indexName).isLessThan(other: EnginePeer.IndexName(rhs.indexName), ordering: sortOrder) +``` + +```swift +// NEW + let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: sortOrder) +``` + +`ContactListPeer.indexName` now returns `EnginePeer.IndexName` (from Task 1), and `isLessThan(other:ordering:)` is defined on `EnginePeer.IndexName` at `submodules/LocalizedPeerData/Sources/PeerTitle.swift:64`, so the wrap idiom is no longer required. + +- [ ] **Step 2.9: Verify ContactListNode.swift changes** + +Run: + +```bash +grep -nE "\._asPeer\(\), isGlobal:|EnginePeer\(peer\)|peer as\? Telegram(User|Group|Channel)\b|EnginePeer\.IndexName\(lhs\.indexName\)|EnginePeer\.IndexName\(rhs\.indexName\)" submodules/ContactListUI/Sources/ContactListNode.swift +``` + +Expected output: only `EnginePeer(peer)` matches at lines 1819 and 1825 (out-of-scope; `peer` there is from `entryData.renderedPeer.peer`, raw `Peer`, wraps stay). Similarly, `peer as? TelegramChannel` at 1802/1820 and `peer is TelegramGroup` at 1818 stay. + +If any other match appears, re-examine that site and apply the matching fix. + +--- + +## Task 3: Edit `ContactsController.swift` — 1 inflow wrap drop + +**Files:** +- Modify: `submodules/ContactListUI/Sources/ContactsController.swift` + +- [ ] **Step 3.1: Drop inflow wrap at L294** + +Read lines 285–300 first. + +Edit: + +```swift +// OLD (line 294) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), purposefulAction: { [weak self] in +``` + +```swift +// NEW + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), purposefulAction: { [weak self] in +``` + +`peer` here is destructured from the ContactListPeer.peer case at line 287; post-migration it is already `EnginePeer`. `chatLocation: .peer(EnginePeer)` case takes `EnginePeer`. + +- [ ] **Step 3.2: Verify** + +Run: + +```bash +grep -nE "chatLocation: \.peer\(EnginePeer\(peer\)\)" submodules/ContactListUI/Sources/ContactsController.swift +``` + +Expected: zero matches. + +--- + +## Task 4: Edit `ContactsSearchContainerNode.swift` — 3 outflow + 4 inflow + +**Files:** +- Modify: `submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift` + +- [ ] **Step 4.1: Drop the 3 outflow `._asPeer()` bridges at L494/535/569** + +Use the same `._asPeer(), isGlobal:` pattern as Task 2.1. The 3 bridges at `ContactListPeer.peer(...)` constructions all match this substring; the 3 unrelated bridges at L488/528/562 (`canSendMessagesToPeer(...)` sites) do NOT match (they lack the `, isGlobal:` suffix). + +Pre-flight verify: + +```bash +grep -cE "\._asPeer\(\), isGlobal:" submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift +``` + +Expected: `3`. + +Apply Edit with `replace_all=true`: +- `old_string`: `._asPeer(), isGlobal:` +- `new_string`: `, isGlobal:` + +- [ ] **Step 4.2: Drop 4 inflow wraps at L164/165/181** + +Read lines 160–185 first. + +Three edits, each targeting one source line. + +Edit (line 164 — 2 wraps in one expression): + +```swift +// OLD + peerItem = .peer(peer: EnginePeer(peer), chatPeer: EnginePeer(peer)) +``` + +```swift +// NEW + peerItem = .peer(peer: peer, chatPeer: peer) +``` + +Edit (line 165): + +```swift +// OLD + nativePeer = EnginePeer(peer) +``` + +```swift +// NEW + nativePeer = peer +``` + +Edit (line 181): + +```swift +// OLD + openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic) +``` + +```swift +// NEW + openDisabledPeer(peer, requiresPremiumForMessaging ? .premiumRequired : .generic) +``` + +- [ ] **Step 4.3: Verify** + +Run: + +```bash +grep -nE "\._asPeer\(\), isGlobal:|EnginePeer\(peer\)" submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift +``` + +Expected: zero matches. + +The `._asPeer()` calls at L488/528/562 (feeding `canSendMessagesToPeer`) should remain. Verify: + +```bash +grep -nE "canSendMessagesToPeer\(.*\._asPeer\(\)\)" submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift +``` + +Expected: 3 matches (L488, L528, L562). + +--- + +## Task 5: Edit `TelegramUI/Sources/ContactMultiselectionController.swift` — 2 outflow + 4 inflow + +**Files:** +- Modify: `submodules/TelegramUI/Sources/ContactMultiselectionController.swift` + +- [ ] **Step 5.1: Drop 2 outflow bridges at L451/459 via `replace_all`** + +Pre-flight verify: + +```bash +grep -cE "\._asPeer\(\), isGlobal:" submodules/TelegramUI/Sources/ContactMultiselectionController.swift +``` + +Expected: `2`. + +Apply Edit with `replace_all=true`: +- `old_string`: `._asPeer(), isGlobal:` +- `new_string`: `, isGlobal:` + +Unrelated `._asPeer()` calls at L171/201/748 (feeding `peerTokenTitle(peer: Peer, ...)`) do NOT use this substring and stay. + +- [ ] **Step 5.2: Drop 4 inflow wraps at L386/403/481/491** + +Read the file around each site to confirm exact text. Two wraps (L386, L403) have identical text; the other two (L481, L491) have distinct tails. + +Edit for L386 and L403 — `replace_all=true` on the substring: + +Pre-flight verify: + +```bash +grep -cE "subject: \.peer\(EnginePeer\(peer\)\)" submodules/TelegramUI/Sources/ContactMultiselectionController.swift +``` + +Expected: `2`. + +Apply Edit with `replace_all=true`: +- `old_string`: `subject: .peer(EnginePeer(peer))` +- `new_string`: `subject: .peer(peer)` + +Edit for L481: + +```swift +// OLD + self.params.sendMessage?(EnginePeer(peer)) +``` + +```swift +// NEW + self.params.sendMessage?(peer) +``` + +Edit for L491: + +```swift +// OLD + self.params.openProfile?(EnginePeer(peer)) +``` + +```swift +// NEW + self.params.openProfile?(peer) +``` + +- [ ] **Step 5.3: Verify** + +Run: + +```bash +grep -nE "\._asPeer\(\), isGlobal:|subject: \.peer\(EnginePeer\(peer\)\)|sendMessage\?\(EnginePeer\(peer\)\)|openProfile\?\(EnginePeer\(peer\)\)" submodules/TelegramUI/Sources/ContactMultiselectionController.swift +``` + +Expected: zero matches. + +Preserved bridge sites (sanity check): + +```bash +grep -nE "peerTokenTitle\(.*\._asPeer\(\)" submodules/TelegramUI/Sources/ContactMultiselectionController.swift +``` + +Expected: 3 matches (L171, L201, L748). + +--- + +## Task 6: Edit `TelegramUI/Sources/ContactMultiselectionControllerNode.swift` — 1 outflow + 1 inflow + +**Files:** +- Modify: `submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift` + +- [ ] **Step 6.1: Drop 1 outflow bridge at L317** + +Read lines 315–320 first. + +Edit: + +```swift +// OLD (line 317) + self?.openPeer?(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil)) +``` + +```swift +// NEW + self?.openPeer?(.peer(peer: peer, isGlobal: false, participantCount: nil)) +``` + +- [ ] **Step 6.2: Drop 1 inflow wrap at L492** + +Read lines 488–495 first. + +Edit: + +```swift +// OLD (line 492) + callTitle = self.presentationData.strings.NewCall_ActionCallSingle(EnginePeer(peer).compactDisplayTitle).string +``` + +```swift +// NEW + callTitle = self.presentationData.strings.NewCall_ActionCallSingle(peer.compactDisplayTitle).string +``` + +- [ ] **Step 6.3: Verify** + +Run: + +```bash +grep -nE "\._asPeer\(\), isGlobal:|EnginePeer\(peer\)\.compactDisplayTitle" submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +``` + +Expected: zero matches. + +--- + +## Task 7: Edit `TelegramUI/Sources/ContactSelectionController.swift` — 2 inflow wraps + +**Files:** +- Modify: `submodules/TelegramUI/Sources/ContactSelectionController.swift` + +- [ ] **Step 7.1: Drop 2 inflow wraps at L517/527** + +Read lines 510–535 first. Both sites are inside the destructure at L504. + +Edit for L517: + +```swift +// OLD + self.sendMessage?(EnginePeer(peer)) +``` + +```swift +// NEW + self.sendMessage?(peer) +``` + +Edit for L527: + +```swift +// OLD + self.openProfile?(EnginePeer(peer)) +``` + +```swift +// NEW + self.openProfile?(peer) +``` + +- [ ] **Step 7.2: Verify** + +Run: + +```bash +grep -nE "sendMessage\?\(EnginePeer\(peer\)\)|openProfile\?\(EnginePeer\(peer\)\)" submodules/TelegramUI/Sources/ContactSelectionController.swift +``` + +Expected: zero matches. + +--- + +## Task 8: Edit `TelegramUI/Sources/ContactSelectionControllerNode.swift` — 2 outflow bridges + +**Files:** +- Modify: `submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift` + +- [ ] **Step 8.1: Drop 2 outflow bridges at L160/230 via `replace_all`** + +Pre-flight verify: + +```bash +grep -cE "\._asPeer\(\), isGlobal:" submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +``` + +Expected: `2`. + +Apply Edit with `replace_all=true`: +- `old_string`: `._asPeer(), isGlobal:` +- `new_string`: `, isGlobal:` + +- [ ] **Step 8.2: Verify** + +Run: + +```bash +grep -nE "\._asPeer\(\), isGlobal:" submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +``` + +Expected: zero matches. + +--- + +## Task 9: Verify no-edit consumer files + +**Files (read only):** +- Read: `submodules/TelegramUI/Sources/ComposeController.swift` +- Read: `submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift` + +- [ ] **Step 9.1: Confirm ComposeController.swift has no inflow wraps, casts, or outflow bridges** + +Run: + +```bash +grep -nE "\.peer\(peer:|EnginePeer\(peer\)|peer as\? Telegram|\._asPeer\(\)" submodules/TelegramUI/Sources/ComposeController.swift +``` + +Expected: zero matches (destructures at L120/160 only access `.id`). + +If any match appears, add the appropriate fix step here and re-run Task 9.1 before proceeding. + +- [ ] **Step 9.2: Confirm ChatSendAudioMessageContextPreview.swift has no ContactListPeer.peer destructures** + +Run: + +```bash +grep -nE "case let \.peer\(peer, _, _\)|case \.peer\(let peer|EnginePeer\(peer\)|\.peer\(peer: " submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +``` + +Expected: zero matches. The file only references `[ContactListPeer]` at the collection level. + +--- + +## Task 10: Build verification (first pass) + +- [ ] **Step 10.1: Run the full build with `--continueOnError`** + +Run: + +```bash +source ~/.zshrc 2>/dev/null && python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError 2>&1 | tee /tmp/wave36-build.log +``` + +Expected outcome: ideally clean. Realistic: 0–3 inventory-missed sites (wave 35 trend was 14% miss rate on a 7-file wave; this 8-file wave has a larger surface area, so budget for up to 3 misses). + +- [ ] **Step 10.2: Triage build errors** + +Likely patterns and fixes: + +| Error | Fix | +|---|---| +| `cannot convert value of type 'EnginePeer' to expected argument type 'Peer'` at a call site | Add `._asPeer()` bridge. The callee takes raw `Peer` and is out of wave scope. | +| `cannot convert value of type 'Peer' to expected argument type 'EnginePeer'` at a `.peer(peer:, ...)` construction | Wrap raw peer with `EnginePeer(...)`. The raw-Peer source is probably from `transaction.getPeer(...)` or similar. | +| `value of type 'EnginePeer' has no member 'isEqual'` | Replace with `==`. | +| `type 'EnginePeer' cannot be cast to 'TelegramUser'` / `TelegramGroup` / `TelegramChannel` | Missed φ-category cast — rewrite to `case .user = peer` / `case let .legacyGroup(x) = peer` / `case let .channel(x) = peer`. | +| `cannot invoke initializer for type 'EnginePeer' with an argument list of type '(EnginePeer)'` | Missed inflow drop — strip `EnginePeer(...)` wrap. | +| `cannot convert value of type 'EnginePeer.IndexName' to expected argument type 'PeerIndexNameRepresentation'` | Either wrap the call site's expected-type change or adjust the consumer to accept `EnginePeer.IndexName`. Probably rare — ContactListPeer.indexName consumers were grepped in pre-flight and found only in ContactListNode. | +| `value of type 'EnginePeer' has no member ''` | That method is only on the Postbox `Peer` protocol. Bridge via `._asPeer()` OR find the EnginePeer-native equivalent. | + +For each error: identify file:line, apply the fix, re-run the build until clean. + +- [ ] **Step 10.3: Iterate to clean build** + +Re-run the build after each batch of fixes. The wave is complete when the build returns 0 errors for the targeted configuration. + +If 10+ unexpected errors surface, halt and reassess: the inventory may have significantly undercounted and the wave may need to be split. Discuss with the user before continuing. + +--- + +## Task 11: Post-build grep validations + +- [ ] **Step 11.1: Outflow-bridge-drop validation** + +Run: + +```bash +grep -rnE "\.peer\(peer: \w+\._asPeer\(\), isGlobal:" submodules/ --include="*.swift" +``` + +Expected: zero hits. Any remaining site is a missed outflow-bridge drop. + +- [ ] **Step 11.2: Inflow-wrap-drop validation** + +Run: + +```bash +for f in submodules/ContactListUI/Sources/ContactListNode.swift \ + submodules/ContactListUI/Sources/ContactsController.swift \ + submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift \ + submodules/TelegramUI/Sources/ContactMultiselectionController.swift \ + submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift \ + submodules/TelegramUI/Sources/ContactSelectionController.swift; do + echo "=== $f ===" + grep -nE "EnginePeer\(peer\)" "$f" +done +``` + +Expected hits: +- ContactListNode.swift L1819, L1825 (raw `renderedPeer.peer`, out-of-scope wraps stay) +- Any other hit in the 6 listed files is a missed inflow drop — inspect and fix. + +- [ ] **Step 11.3: Cast-rewrite validation** + +Run: + +```bash +grep -nE "\bpeer (as\?|as!|is) Telegram(User|Group|Channel)\b" submodules/ContactListUI/Sources/ContactListNode.swift +``` + +Expected: only L1802, L1818, L1820 remain (out-of-scope, `peer` is raw from `renderedPeer.peer`). + +If L182, L184, L186, or L1968 appear, those are missed φ rewrites. + +- [ ] **Step 11.4: IndexName wrap validation** + +Run: + +```bash +grep -nE "EnginePeer\.IndexName\(lhs\.indexName\)|EnginePeer\.IndexName\(rhs\.indexName\)" submodules/ContactListUI/Sources/ContactListNode.swift +``` + +Expected: zero matches. + +- [ ] **Step 11.5: isEqual-in-==-operator validation** + +Run: + +```bash +grep -nE "lhsPeer\.isEqual\(rhsPeer\)" submodules/AccountContext/Sources/ContactSelectionController.swift +``` + +Expected: zero matches. + +- [ ] **Step 11.6: Construction-site sanity sweep** + +Run: + +```bash +grep -rnE "ContactListPeer\.peer\(peer: |\.peer\(peer: \w+, isGlobal:" submodules/ --include="*.swift" | head -40 +``` + +Inspect each hit. Expected forms: +- `.peer(peer: , isGlobal: …)` where `` is either a local already typed `EnginePeer` or `EnginePeer()`. +- Anything of the form `.peer(peer: , isGlobal: …)` where `` is a Postbox `Peer` value is a miss (would surface as a build error — this is a belt-and-suspenders check). + +If any validation fails, return to Task 10. + +--- + +## Task 12: Atomic commit + memory + log update + +- [ ] **Step 12.1: Stage and review** + +Run: + +```bash +git status --short +git diff --stat +``` + +Confirm exactly 8 modified Swift files: +- `submodules/AccountContext/Sources/ContactSelectionController.swift` +- `submodules/ContactListUI/Sources/ContactListNode.swift` +- `submodules/ContactListUI/Sources/ContactsController.swift` +- `submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift` +- `submodules/TelegramUI/Sources/ContactMultiselectionController.swift` +- `submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift` +- `submodules/TelegramUI/Sources/ContactSelectionController.swift` +- `submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift` + +Pre-existing WIP (`build-system/bazel-rules/sourcekit-bazel-bsp`, `ChatListFilterPresetController.swift`, `ChatListFilterPresetListController.swift`, untracked `build-system/tulsi/` / `submodules/TgVoip/` / `third-party/libx264/` / `docs/superpowers/plans/2026-04-22-claude-md-reorganization.md`) should NOT be staged. + +- [ ] **Step 12.2: Stage only the wave-36 files** + +Run: + +```bash +git add submodules/AccountContext/Sources/ContactSelectionController.swift \ + submodules/ContactListUI/Sources/ContactListNode.swift \ + submodules/ContactListUI/Sources/ContactsController.swift \ + submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift \ + submodules/TelegramUI/Sources/ContactMultiselectionController.swift \ + submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift \ + submodules/TelegramUI/Sources/ContactSelectionController.swift \ + submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +``` + +If Task 10 introduced additional files (inventory-miss fixes), append them. + +- [ ] **Step 12.3: Commit** + +Run: + +```bash +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 36: ContactListPeer.peer Peer -> EnginePeer + +Migrates the public enum case `ContactListPeer.peer(peer: Peer, isGlobal:, +participantCount:)` from the Postbox `Peer` protocol to the TelegramCore +`EnginePeer` enum. Also cascades `ContactListPeer.indexName` return type +from `PeerIndexNameRepresentation` to `EnginePeer.IndexName` and rewrites +the enum's custom `==` operator to use EnginePeer's synthesized Equatable. + +Consumer-side cascade in 7 files: + - 20 `._asPeer()` outflow bridge-drops at ContactListPeer.peer + construction sites (the payload is now EnginePeer) + - 16 `EnginePeer(peer)` inflow wrap-drops at destructure sites (the + destructured `peer` is already EnginePeer) + - 2 `EnginePeer.IndexName(...)` wrap-drops at a sort-comparator (the + indexName property now returns EnginePeer.IndexName directly) + - 2 Postbox-concrete cast chains rewritten to EnginePeer case patterns + (`peer as? TelegramUser` → `case .user = peer`, etc.) + - `lhsPeer.isEqual(rhsPeer)` → `lhsPeer == rhsPeer` in the ==operator + +Files modified: + submodules/AccountContext/Sources/ContactSelectionController.swift + submodules/ContactListUI/Sources/ContactListNode.swift + submodules/ContactListUI/Sources/ContactsController.swift + submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift + submodules/TelegramUI/Sources/ContactMultiselectionController.swift + submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift + submodules/TelegramUI/Sources/ContactSelectionController.swift + submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift + +Bridges intentionally retained (out-of-wave scope): + - `canSendMessagesToPeer(peer._asPeer())` — callee takes Peer, deferred + - `peerTokenTitle(peer: peer._asPeer(), ...)` — callee takes Peer, + deferred + +Plan: docs/superpowers/plans/2026-04-24-contactlistpeer-engine-peer-migration.md +Spec: docs/superpowers/specs/2026-04-24-contactlistpeer-engine-peer-migration-design.md + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 12.4: Update CLAUDE.md wave counter** + +Edit `CLAUDE.md` to bump the "Waves landed so far" line from "35 waves" to "36 waves" and update the "as of" date if the commit lands after 2026-04-24. + +- [ ] **Step 12.5: Append wave outcome to the postbox-refactor-log** + +Append a "Wave 36 outcome" section to `docs/superpowers/postbox-refactor-log.md` documenting: +- Actual files touched + edit counts vs. plan +- Any inventory undercounts surfaced by Task 10 (file:line + missed-category) +- Any lessons learned (e.g., whether the γ category really had zero sites; how the φ cast-rewrites behaved; post-migration undercount percentage vs wave 35's 14%) +- Ratio of bridge-drops to bridge-additions (wave theme: removal-dominated) + +Keep concise. + +- [ ] **Step 12.6: Commit the docs update** + +Run: + +```bash +git add CLAUDE.md docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: add wave 36 outcome (ContactListPeer.peer Peer→EnginePeer) + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 12.7: Update the next-wave memory** + +Edit `/Users/isaac/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`: +- Add wave 36 to the "Latest commits" section. +- Move ContactListPeer migration from "Recommended wave 36 candidates" to landed. +- Record the inventory undercount ratio (actual-files-touched ÷ pre-flight-file-count) for calibration. +- Update the "Recommended wave 37" section. Promote candidates: `canSendMessagesToPeer(_:)` parameter (the ContactsSearchContainerNode `._asPeer()` bridges at L488/528/562 plus others elsewhere drop when this lands); `peerTokenTitle(peer:)` parameter (drops 3 bridges in ContactMultiselectionController); `makePeerInfoController` / `makeChatQrCodeScreen` / `makeChatRecentActionsController` AccountContext protocol methods (largest remaining Peer-typed-API); accountManager engine path; Shape-C `resourceData` module. + +Use the Edit tool on the memory file. No git commit needed. + +--- + +## Risks and notes + +- **Inventory undercount.** Pre-flight caught several sites the Explore agent missed (inflow wraps at L481/491/517/527/492/844, cast rewrites at L182-186 and L1968). Budget for 1–3 additional misses surfacing in Task 10. If the build surfaces 5+ misses in new categories, stop and reassess. +- **`replace_all` usage.** Every `replace_all=true` Edit in this plan is gated by a pre-flight `grep -c` count check. If the count is wrong, fall back to per-site Edits with surrounding context. +- **Cast rewrite at L182-186.** The original cast chain binds `group` and `channel` (but not `user`). The EnginePeer case-pattern form preserves this: `case .user = peer` is a binding-free match, mirroring `if let _ = peer as? TelegramUser`. +- **`._asPeer()` sites that stay.** Tasks 4.3 and 5.3 explicitly verify that the 3 `canSendMessagesToPeer(peer._asPeer())` bridges and 3 `peerTokenTitle(peer: peer._asPeer(), ...)` bridges remain intact. Dropping these would be out-of-scope migration. +- **WIP isolation.** Pre-existing `ChatListFilterPresetController.swift` / `ChatListFilterPresetListController.swift` edits and untracked `build-system/tulsi/`, `submodules/TgVoip/`, `third-party/libx264/` paths are user WIP — do NOT stage them. Use the explicit `git add ` form in Step 12.2. +- **Scope boundary.** Task 10 errors surfacing in `TelegramCore`, `Postbox`, or `TelegramApi` mean the migration cascaded beyond its intended consumer scope. Halt and investigate — do NOT edit TelegramCore in this wave. +- **No new typealiases/wrappers.** Rule 2 and 3 of the Postbox refactor guidance apply — this wave stays inside. diff --git a/docs/superpowers/plans/2026-04-24-foundpeer-engine-peer-migration.md b/docs/superpowers/plans/2026-04-24-foundpeer-engine-peer-migration.md new file mode 100644 index 0000000000..f596e3270e --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-foundpeer-engine-peer-migration.md @@ -0,0 +1,1287 @@ +# Wave 34: `FoundPeer.peer: Peer → EnginePeer` Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate the public field `FoundPeer.peer` from the Postbox `Peer` protocol to the TelegramCore `EnginePeer` enum in a single atomic commit. Drops 5 `._asPeer()` bridges (most added in wave 33), eliminates 22 `EnginePeer(peer.peer)` redundant wraps, rewrites 30 Postbox-concrete-type downcasts to enum patterns, and adds bridges where `peer.peer` flows out to APIs that still take raw `Peer`. + +**Architecture:** One atomic commit. The field-type change is necessarily atomic (half-migrated FoundPeer doesn't compile), so all edits land together. TelegramCore's `_internal_searchPeers` keeps `import Postbox` — only `FoundPeer`'s public surface changes. No new wrappers, no new typealiases beyond what already exists. + +**Tech Stack:** Swift, Bazel build via Make.py wrapper. No tests — verification is build success + targeted grep checks. + +**Spec:** `docs/superpowers/specs/2026-04-24-foundpeer-engine-peer-migration-design.md` + +--- + +## File Structure + +**Modified files (8 total — 1 TelegramCore + 7 consumer):** + +| File | Edit count | +|---|---| +| `submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift` | ~13 spot edits (struct change + 6 filter rewrites + 4 constructor wraps + manual `==` body) | +| `submodules/TelegramCallsUI/Sources/VideoChatScreen.swift` | 1 (bridge-drop at line 1833) | +| `submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift` | 7 (3 C2 downcasts + 4 C3 wraps) | +| `submodules/ContactListUI/Sources/ContactListNode.swift` | ~21 (3 C4 + 13 C2 + 0 C3 + 3 outflow bridges; some lines have multiple edits) | +| `submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift` | ~17 (8 C2 + ~9 C3) | +| `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift` | ~10 (2 C4 + 3 C2 + 4 C3 + 1 outflow bridge at line 161 — verify) | +| `submodules/TelegramBaseController/Sources/TelegramBaseController.swift` | 6 (1 C4 + 3 C2 + 2 C3) | +| `submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift` | 3 (1 C4 + 2 C3) | + +Note: line counts above are approximations from spec — execution may surface additional outflow-bridge sites caught by the build pass (Task 10). + +**EnginePeer enum case mapping (used throughout):** + +| Postbox concrete | EnginePeer case | +|---|---| +| `TelegramUser` | `.user(TelegramUser)` | +| `TelegramSecretChat` | `.secretChat(TelegramSecretChat)` | +| `TelegramGroup` | `.legacyGroup(TelegramGroup)` | +| `TelegramChannel` | `.channel(TelegramChannel)` | + +--- + +## Task 1: Edit `SearchPeers.swift` — struct definition + body rewrites + +**Files:** +- Modify: `submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift` + +This is the foundational change. Without it, none of the consumer edits compile in the right direction. + +- [ ] **Step 1.1: Update the FoundPeer struct field, init parameter, and `==` body** + +Edit: + +```swift +// OLD +public struct FoundPeer: Equatable { + public let peer: Peer + public let subscribers: Int32? + + public init(peer: Peer, subscribers: Int32?) { + self.peer = peer + self.subscribers = subscribers + } + + public static func ==(lhs: FoundPeer, rhs: FoundPeer) -> Bool { + return lhs.peer.isEqual(rhs.peer) && lhs.subscribers == rhs.subscribers + } +} +``` + +```swift +// NEW +public struct FoundPeer: Equatable { + public let peer: EnginePeer + public let subscribers: Int32? + + public init(peer: EnginePeer, subscribers: Int32?) { + self.peer = peer + self.subscribers = subscribers + } + + public static func ==(lhs: FoundPeer, rhs: FoundPeer) -> Bool { + return lhs.peer == rhs.peer && lhs.subscribers == rhs.subscribers + } +} +``` + +Use the Edit tool with the OLD block as `old_string` and the NEW block as `new_string`. + +- [ ] **Step 1.2: Wrap raw peer values in the four constructor sites inside `_internal_searchPeers`** + +There are four `FoundPeer(peer: peer, subscribers: …)` calls inside `_internal_searchPeers` at lines 70, 72, 85, 87. Each wraps `peer` (a raw `Peer` from `parsedPeers.get(peerId)`) with `EnginePeer(peer)`. + +Edit (replace_all=false because there are 4 distinct contexts; use enough surrounding context per edit to make each unique): + +For lines 70 and 72 (inside the `myResults` loop): + +```swift +// OLD + if let user = peer as? TelegramUser { + renderedMyPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount)) + } else { + renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + } +``` + +```swift +// NEW + if let user = peer as? TelegramUser { + renderedMyPeers.append(FoundPeer(peer: EnginePeer(peer), subscribers: user.subscriberCount)) + } else { + renderedMyPeers.append(FoundPeer(peer: EnginePeer(peer), subscribers: subscribers[peerId])) + } +``` + +For lines 85 and 87 (inside the `results` loop): + +```swift +// OLD + if let user = peer as? TelegramUser { + renderedPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount)) + } else { + renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + } +``` + +```swift +// NEW + if let user = peer as? TelegramUser { + renderedPeers.append(FoundPeer(peer: EnginePeer(peer), subscribers: user.subscriberCount)) + } else { + renderedPeers.append(FoundPeer(peer: EnginePeer(peer), subscribers: subscribers[peerId])) + } +``` + +- [ ] **Step 1.3: Rewrite the six scope-filter expressions to enum-pattern form** + +For `.channels` scope (two filter blocks; identical bodies — use one Edit per block, NOT replace_all, since the surrounding `renderedMyPeers =` vs `renderedPeers =` differs): + +```swift +// OLD (renderedMyPeers, lines ~96-102) + case .channels: + renderedMyPeers = renderedMyPeers.filter { item in + if let channel = item.peer as? TelegramChannel, case .broadcast = channel.info { + return true + } else { + return false + } + } + renderedPeers = renderedPeers.filter { item in + if let channel = item.peer as? TelegramChannel, case .broadcast = channel.info { + return true + } else { + return false + } + } +``` + +```swift +// NEW + case .channels: + renderedMyPeers = renderedMyPeers.filter { item in + if case let .channel(channel) = item.peer, case .broadcast = channel.info { + return true + } else { + return false + } + } + renderedPeers = renderedPeers.filter { item in + if case let .channel(channel) = item.peer, case .broadcast = channel.info { + return true + } else { + return false + } + } +``` + +For `.groups` scope (two filter blocks): + +```swift +// OLD + case .groups: + renderedMyPeers = renderedMyPeers.filter { item in + if let channel = item.peer as? TelegramChannel, case .group = channel.info { + return true + } else if item.peer is TelegramGroup { + return true + } else { + return false + } + } + renderedPeers = renderedPeers.filter { item in + if let channel = item.peer as? TelegramChannel, case .group = channel.info { + return true + } else if item.peer is TelegramGroup { + return true + } else { + return false + } + } +``` + +```swift +// NEW + case .groups: + renderedMyPeers = renderedMyPeers.filter { item in + if case let .channel(channel) = item.peer, case .group = channel.info { + return true + } else if case .legacyGroup = item.peer { + return true + } else { + return false + } + } + renderedPeers = renderedPeers.filter { item in + if case let .channel(channel) = item.peer, case .group = channel.info { + return true + } else if case .legacyGroup = item.peer { + return true + } else { + return false + } + } +``` + +For `.privateChats` scope: + +```swift +// OLD + case .privateChats: + renderedMyPeers = renderedMyPeers.filter { item in + if item.peer is TelegramUser { + return true + } else { + return false + } + } + renderedPeers = renderedPeers.filter { item in + if item.peer is TelegramUser { + return true + } else { + return false + } + } +``` + +```swift +// NEW + case .privateChats: + renderedMyPeers = renderedMyPeers.filter { item in + if case .user = item.peer { + return true + } else { + return false + } + } + renderedPeers = renderedPeers.filter { item in + if case .user = item.peer { + return true + } else { + return false + } + } +``` + +- [ ] **Step 1.4: Verify** — read the updated file from line 1 to ~155 and confirm there are no remaining `item.peer as? Telegram*` or `item.peer is Telegram*` patterns and the four `FoundPeer(peer: peer, ...)` constructions are now `FoundPeer(peer: EnginePeer(peer), ...)`. Do not commit yet. + +--- + +## Task 2: Edit `VideoChatScreen.swift` + +**Files:** +- Modify: `submodules/TelegramCallsUI/Sources/VideoChatScreen.swift` + +- [ ] **Step 2.1: Drop `._asPeer()` bridge in the FoundPeer constructor at line 1833** + +Edit: + +```swift +// OLD + |> map { peer in + return [FoundPeer(peer: peer._asPeer(), subscribers: nil)] + } +``` + +```swift +// NEW + |> map { peer in + return [FoundPeer(peer: peer, subscribers: nil)] + } +``` + +The surrounding context (`|> mapToSignal { peer -> Signal` two lines earlier) confirms `peer` is `EnginePeer`. The `._asPeer()` bridge becomes unnecessary. + +--- + +## Task 3: Edit `VideoChatScreenMoreMenu.swift` + +**Files:** +- Modify: `submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift` + +7 edits in this file: 4 C3 wrap-drops + 3 C2 downcast rewrites. + +- [ ] **Step 3.1: Drop the two `EnginePeer(peer.peer)` wraps on line 171** + +Edit: + +```swift +// OLD + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: currentCall.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { [weak self] c, _ in +``` + +```swift +// NEW + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: environment.strings, displayOrder: currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: currentCall.accountContext.account, peer: peer.peer, size: avatarSize)), action: { [weak self] c, _ in +``` + +- [ ] **Step 3.2: Rewrite the C2 downcasts at lines 628–648** + +Edit: + +```swift +// OLD (around lines 627-635) + for peer in displayAsPeers { + if peer.peer is TelegramGroup { + isGroup = true + break + } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + isGroup = true + break + } + } +``` + +```swift +// NEW + for peer in displayAsPeers { + if case .legacyGroup = peer.peer { + isGroup = true + break + } else if case let .channel(channel) = peer.peer, case .group = channel.info { + isGroup = true + break + } + } +``` + +(Note the `else if let peer = peer.peer as? TelegramChannel` shadowed the outer loop `peer` with a new `peer: TelegramChannel`. The rewrite uses `channel` to avoid further shadowing the EnginePeer loop variable.) + +Edit (around line 648): + +```swift +// OLD + } else if let subscribers = peer.subscribers { + if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info { + subtitle = environment.strings.Conversation_StatusSubscribers(subscribers) + } else { + subtitle = environment.strings.Conversation_StatusMembers(subscribers) + } + } +``` + +```swift +// NEW + } else if let subscribers = peer.subscribers { + if case let .channel(channel) = peer.peer, case .broadcast = channel.info { + subtitle = environment.strings.Conversation_StatusSubscribers(subscribers) + } else { + subtitle = environment.strings.Conversation_StatusMembers(subscribers) + } + } +``` + +- [ ] **Step 3.3: Drop the `EnginePeer(peer.peer)` wrap at line 658** + +Edit: + +```swift +// OLD + let avatarSignal = peerAvatarCompleteImage(account: groupCall.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize) +``` + +```swift +// NEW + let avatarSignal = peerAvatarCompleteImage(account: groupCall.accountContext.account, peer: peer.peer, size: avatarSize) +``` + +- [ ] **Step 3.4: Drop the `EnginePeer(peer.peer)` wrap at line 679** + +Edit: + +```swift +// OLD + items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { [weak self] _, f in +``` + +```swift +// NEW + items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { [weak self] _, f in +``` + +--- + +## Task 4: Edit `ContactListNode.swift` + +**Files:** +- Modify: `submodules/ContactListUI/Sources/ContactListNode.swift` + +The largest file: 3 C4 (constructor) edits, 13 C2 (downcast) rewrites, and 3 outflow bridges where `peer.peer` is passed to `.peer(peer:)` (raw-Peer-taking enum case). + +- [ ] **Step 4.1: Bridge-drop at constructor lines 1485 and 1517** + +Edit: + +```swift +// OLD (line 1485) + resultPeers.append(FoundPeer(peer: mainPeer._asPeer(), subscribers: nil)) +``` + +```swift +// NEW + resultPeers.append(FoundPeer(peer: mainPeer, subscribers: nil)) +``` + +(Verification: `mainPeer` is bound from `peer.chatMainPeer` at line 1479. `chatMainPeer` returns `EnginePeer?` on `EngineRenderedPeer`. After migration this needs no bridge.) + +Edit: + +```swift +// OLD (line 1517) + return (peers.map({ FoundPeer(peer: $0._asPeer(), subscribers: nil) }), presences) +``` + +```swift +// NEW + return (peers.map({ FoundPeer(peer: $0, subscribers: nil) }), presences) +``` + +(Verification: `peers` comes from `context.engine.contacts.searchContacts(query:)` whose return type's first element is `[EnginePeer]`. Confirmed by inspection.) + +- [ ] **Step 4.2: Rewrite the C2 downcast at line 1501** + +Edit: + +```swift +// OLD + if let _ = peer.peer as? TelegramChannel { +``` + +```swift +// NEW + if case .channel = peer.peer { +``` + +- [ ] **Step 4.3: Rewrite the three identical C2 sites at lines 1563, 1569, 1574** + +These three lines have the SAME pattern (`if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) {`). Use `replace_all=true` since the line is identical (verify uniqueness by grepping first). + +Edit (`replace_all=true`): + +```swift +// OLD + if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) { +``` + +```swift +// NEW + if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) { +``` + +Verify `replace_all` actually replaced exactly 3 sites (count occurrences before and after; if not 3, something else uses the same pattern and you must split into individual edits). + +- [ ] **Step 4.4: Add `._asPeer()` outflow bridges at lines 1656, 1693, 1731** + +These are `peers.append(.peer(peer: peer.peer, ...))` where `.peer(peer:)` is `ContactListPeer.peer(peer:isGlobal:participantCount:)` taking raw `Peer`. Add bridge. + +Edit (line 1656): + +```swift +// OLD + peers.append(.peer(peer: peer.peer, isGlobal: false, participantCount: peer.subscribers)) +``` + +```swift +// NEW + peers.append(.peer(peer: peer.peer._asPeer(), isGlobal: false, participantCount: peer.subscribers)) +``` + +Edit (line 1693): + +```swift +// OLD + peers.append(.peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers)) +``` + +```swift +// NEW + peers.append(.peer(peer: peer.peer._asPeer(), isGlobal: true, participantCount: peer.subscribers)) +``` + +The same pattern appears at line 1731 — apply the same edit. Use `replace_all` if and only if the second occurrence is identical (it is, but verify). + +- [ ] **Step 4.5: Rewrite the C2 sites at lines 1658, 1665, 1673, 1675, 1695, 1703, 1711, 1713, 1733** + +These are scattered through three nearly-identical loop bodies (`localPeersAndStatuses.0`, `remotePeers.0`, `remotePeers.1`). The patterns: + +For lines 1658, 1695, 1733 (`if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone`): + +Edit (`replace_all=true` if the 3 occurrences are textually identical — verify): + +```swift +// OLD + if searchDeviceContacts, + let user = peer.peer as? TelegramUser, + let phone = user.phone { +``` + +```swift +// NEW + if searchDeviceContacts, + case let .user(user) = peer.peer, + let phone = user.phone { +``` + +For line 1665 and 1703 (single-condition `if let user = peer.peer as? TelegramUser {`): + +Edit (`replace_all=true` if textually identical): + +```swift +// OLD + if let user = peer.peer as? TelegramUser { +``` + +```swift +// NEW + if case let .user(user) = peer.peer { +``` + +For line 1673 (`if peer.peer is TelegramGroup && searchGroups`): + +Edit: + +```swift +// OLD + if peer.peer is TelegramGroup && searchGroups { + matches = true + } else if let channel = peer.peer as? TelegramChannel { +``` + +```swift +// NEW + if case .legacyGroup = peer.peer, searchGroups { + matches = true + } else if case let .channel(channel) = peer.peer { +``` + +For line 1711 (`if peer.peer is TelegramGroup`): + +Edit: + +```swift +// OLD + if peer.peer is TelegramGroup { + matches = searchGroups + } else if let channel = peer.peer as? TelegramChannel { +``` + +```swift +// NEW + if case .legacyGroup = peer.peer { + matches = searchGroups + } else if case let .channel(channel) = peer.peer { +``` + +(Note that line 1675 and 1713 carry the same `else if let channel = peer.peer as? TelegramChannel` pattern that is folded into the edits above. Confirm both got rewritten.) + +- [ ] **Step 4.6: Verify** — grep for remaining FoundPeer-relevant Postbox patterns in the file: + +Run: `grep -nE "peer\.peer\s+(as\?|is)\s+Telegram|EnginePeer\(peer\.peer\)|FoundPeer\(peer:\s+\w+\._asPeer\(\)" submodules/ContactListUI/Sources/ContactListNode.swift` + +Expected: zero matches if the C2/C3/C4 edits are complete. + +--- + +## Task 5: Edit `ChatListSearchListPaneNode.swift` + +**Files:** +- Modify: `submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift` + +8 C2 downcasts + 9 C3 wrap drops. + +- [ ] **Step 5.1a: Add outflow bridge at line 1018** + +Edit: + +```swift +// OLD + enabled = canSendMessagesToPeer(peer.peer) +``` + +```swift +// NEW + enabled = canSendMessagesToPeer(peer.peer._asPeer()) +``` + +(`canSendMessagesToPeer(_ peer: Peer, ...)` in `submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift` takes raw `Peer`, so the bridge is required.) + +- [ ] **Step 5.1b: Rewrite the disjunction at line 1024 using `switch`** + +Edit: + +```swift +// OLD + if filter.contains(.onlyPrivateChats) { + if !(peer.peer is TelegramUser || peer.peer is TelegramSecretChat) { + enabled = false + } + } +``` + +```swift +// NEW + if filter.contains(.onlyPrivateChats) { + switch peer.peer { + case .user, .secretChat: + break + default: + enabled = false + } + } +``` + +The `switch ... case .user, .secretChat: break / default: enabled = false` form preserves the negation semantics of the original `if !(... || ...)` and reads cleanly. + +- [ ] **Step 5.1c: Rewrite C2 downcasts at lines 1029-1030** + +Edit (lines 1029-1030): + +```swift +// OLD + if let _ = peer.peer as? TelegramGroup { + } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { +``` + +```swift +// NEW + if case .legacyGroup = peer.peer { + } else if case let .channel(channel) = peer.peer, case .group = channel.info { +``` + +Edit (lines 1038-1040): + +```swift +// OLD + if peer.peer is TelegramUser { +``` + +(continues `} else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info {` at line 1040) + +```swift +// NEW + if case .user = peer.peer { +``` + +```swift +// OLD + } else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { +``` + +```swift +// NEW + } else if case let .channel(channel) = peer.peer, case .broadcast = channel.info { +``` + +(If line 1040's old form is used elsewhere in the file, scope the Edit by including more surrounding context.) + +- [ ] **Step 5.2: Drop C3 wraps on line 1075** + +Edit: + +```swift +// OLD + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, isAd: false, action: { _ in +``` + +```swift +// NEW + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, isAd: false, action: { _ in +``` + +- [ ] **Step 5.3: Drop C3 wraps on lines 1076, 1078, 1081** + +Edit (line 1076): + +```swift +// OLD + interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil, false) +``` + +```swift +// NEW + interaction.peerSelected(peer.peer, nil, nil, nil, false) +``` + +Edit (line 1078): + +```swift +// OLD + interaction.disabledPeerSelected(EnginePeer(peer.peer), nil, requiresPremiumForMessaging ? .premiumRequired : .generic) +``` + +```swift +// NEW + interaction.disabledPeerSelected(peer.peer, nil, requiresPremiumForMessaging ? .premiumRequired : .generic) +``` + +Edit (line 1081): + +```swift +// OLD + peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture, location) +``` + +```swift +// NEW + peerContextAction(peer.peer, .search(nil), node, gesture, location) +``` + +- [ ] **Step 5.4: Rewrite the C2 downcasts at lines 1500 and 1507 (inside `filteredPeerSearchQueryResults`)** + +Edit (line 1500, inside `value.0.filter`): + +```swift +// OLD + value.0.filter { peer in + if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { + return true + } else { + return false + } + }, +``` + +```swift +// NEW + value.0.filter { peer in + if case let .channel(channel) = peer.peer, case .broadcast = channel.info { + return true + } else { + return false + } + }, +``` + +Edit (line 1507, inside `value.1.filter`): + +```swift +// OLD + value.1.filter { peer in + if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { + return true + } else { + return false + } + } +``` + +```swift +// NEW + value.1.filter { peer in + if case let .channel(channel) = peer.peer, case .broadcast = channel.info { + return true + } else { + return false + } + } +``` + +- [ ] **Step 5.5: Drop C3 wraps in `foundRemotePeers` loops (lines 3088, 3096, 3214, 3216, 3241)** + +Edit (`replace_all=true` for the lines that match exactly the same pattern, otherwise individual Edits): + +```swift +// OLD (occurs at 3088, 3096, 3214, 3241 — the FoundPeer wrap; the EnginePeer(accountPeer) wrap stays since `accountPeer` is raw Peer) + if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) { +``` + +```swift +// NEW + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, EnginePeer(accountPeer)) { +``` + +If `replace_all=true`, verify the count is exactly 4 by grep before and after. + +Edit (line 3216 separately — different pattern): + +```swift +// OLD + entries.append(.localPeer(EnginePeer(peer.peer), nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType, nil, false, false)) +``` + +```swift +// NEW + entries.append(.localPeer(peer.peer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType, nil, false, false)) +``` + +(Note: this assumes `.localPeer(EnginePeer, ...)` accepts EnginePeer directly. If the build fails saying it expected raw `Peer`, add `._asPeer()` instead. Verify in build pass.) + +- [ ] **Step 5.6: Verify** — grep: + +Run: `grep -nE "peer\.peer\s+(as\?|is)\s+Telegram|EnginePeer\(peer\.peer\)" submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift` + +Expected: zero matches. + +--- + +## Task 6: Edit `PeerInfoScreenCallActions.swift` + +**Files:** +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift` + +2 C4 bridge-drops + 3 C2 downcasts + 4 C3 wraps. The function is duplicated almost verbatim at the second site (line 156 vs 265). + +- [ ] **Step 6.1: Bridge-drops at lines 156 and 265** + +Edit (`replace_all=true` if the line is literally identical at both sites): + +```swift +// OLD + return [FoundPeer(peer: peer._asPeer(), subscribers: nil)] +``` + +```swift +// NEW + return [FoundPeer(peer: peer, subscribers: nil)] +``` + +Verify `replace_all` got exactly 2 sites. + +- [ ] **Step 6.2: Rewrite C2 downcasts at lines 175, 178, 193** + +Edit (lines 175 and 178 form one if-else chain): + +```swift +// OLD + for peer in peers { + if peer.peer is TelegramGroup { + isGroup = true + break + } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + isGroup = true + break + } + } +``` + +```swift +// NEW + for peer in peers { + if case .legacyGroup = peer.peer { + isGroup = true + break + } else if case let .channel(channel) = peer.peer, case .group = channel.info { + isGroup = true + break + } + } +``` + +Edit (line 193 — inside the second `for peer in peers` loop): + +```swift +// OLD + } else if let subscribers = peer.subscribers { + if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info { + subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers) + } else { + subtitle = strongSelf.presentationData.strings.Conversation_StatusMembers(subscribers) + } + } +``` + +```swift +// NEW + } else if let subscribers = peer.subscribers { + if case let .channel(channel) = peer.peer, case .broadcast = channel.info { + subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers) + } else { + subtitle = strongSelf.presentationData.strings.Conversation_StatusMembers(subscribers) + } + } +``` + +- [ ] **Step 6.3: Drop C3 wraps at lines 201, 202** + +Edit: + +```swift +// OLD + let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: EnginePeer(peer.peer), size: avatarSize) + items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { _, f in +``` + +```swift +// NEW + let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize) + items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { _, f in +``` + +- [ ] **Step 6.4: Drop two C3 wraps on line 288** + +Edit: + +```swift +// OLD + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { c, f in +``` + +```swift +// NEW + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)), action: { c, f in +``` + +- [ ] **Step 6.5: Verify** — grep: + +Run: `grep -nE "peer\.peer\s+(as\?|is)\s+Telegram|EnginePeer\(peer\.peer\)" submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift` + +Expected: zero matches. + +--- + +## Task 7: Edit `TelegramBaseController.swift` + +**Files:** +- Modify: `submodules/TelegramBaseController/Sources/TelegramBaseController.swift` + +1 C4 bridge-drop + 3 C2 downcasts + 2 C3 wraps. + +- [ ] **Step 7.1: Bridge-drop at line 208** + +Edit: + +```swift +// OLD + |> map { peer in + return [FoundPeer(peer: peer._asPeer(), subscribers: nil)] + } +``` + +```swift +// NEW + |> map { peer in + return [FoundPeer(peer: peer, subscribers: nil)] + } +``` + +- [ ] **Step 7.2: Rewrite C2 downcasts at lines 243, 246, 258** + +Edit (lines 243 and 246 form one if-else chain): + +```swift +// OLD + for peer in peers { + if peer.peer is TelegramGroup { + isGroup = true + break + } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + isGroup = true + break + } + } +``` + +```swift +// NEW + for peer in peers { + if case .legacyGroup = peer.peer { + isGroup = true + break + } else if case let .channel(channel) = peer.peer, case .group = channel.info { + isGroup = true + break + } + } +``` + +Edit (line 258): + +```swift +// OLD + } else if let subscribers = peer.subscribers { + if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info { + subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers) + } else { + subtitle = strongSelf.presentationData.strings.Conversation_StatusMembers(subscribers) + } + } +``` + +```swift +// NEW + } else if let subscribers = peer.subscribers { + if case let .channel(channel) = peer.peer, case .broadcast = channel.info { + subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers) + } else { + subtitle = strongSelf.presentationData.strings.Conversation_StatusMembers(subscribers) + } + } +``` + +- [ ] **Step 7.3: Drop two C3 wraps on line 265** + +Edit: + +```swift +// OLD + items.append(VoiceChatPeerActionSheetItem(context: context, peer: EnginePeer(peer.peer), title: EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), subtitle: subtitle ?? "", action: { +``` + +```swift +// NEW + items.append(VoiceChatPeerActionSheetItem(context: context, peer: peer.peer, title: peer.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), subtitle: subtitle ?? "", action: { +``` + +- [ ] **Step 7.4: Verify** — grep: + +Run: `grep -nE "peer\.peer\s+(as\?|is)\s+Telegram|EnginePeer\(peer\.peer\)" submodules/TelegramBaseController/Sources/TelegramBaseController.swift` + +Expected: zero matches. + +--- + +## Task 8: Edit `StorageUsageExceptionsScreen.swift` + +**Files:** +- Modify: `submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift` + +1 C4 wrap-needed + 2 C3 wrap-drops. + +- [ ] **Step 8.1: Drop C3 wrap at line 173** + +Edit: + +```swift +// OLD + title = EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: .firstLast) +``` + +```swift +// NEW + title = peer.peer.displayTitle(strings: presentationData.strings, displayOrder: .firstLast) +``` + +- [ ] **Step 8.2: Drop C3 wrap at line 176** + +Edit: + +```swift +// OLD + return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: EnginePeer(peer.peer), title: title, enabled: true, titleFont: .bold, label: optionText, labelStyle: .text, additionalDetailLabel: additionalDetailLabel, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { +``` + +```swift +// NEW + return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: peer.peer, title: title, enabled: true, titleFont: .bold, label: optionText, labelStyle: .text, additionalDetailLabel: additionalDetailLabel, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { +``` + +- [ ] **Step 8.3: Add EnginePeer wrap at constructor line 288** + +Edit: + +```swift +// OLD + result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value)) +``` + +```swift +// NEW + result.append((peer: FoundPeer(peer: EnginePeer(peer), subscribers: subscriberCount), value: value)) +``` + +(Verification: `peer` here is bound from a Postbox transaction higher up — it is a raw `Peer`. The wrap is required.) + +- [ ] **Step 8.4: Verify** — grep: + +Run: `grep -nE "EnginePeer\(peer\.peer\)" "submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift"` + +Expected: zero matches. + +--- + +## Task 9: Build verification (first pass) + +- [ ] **Step 9.1: Run the full build with `--continueOnError`** + +Run: + +```bash +source ~/.zshrc 2>/dev/null && python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError 2>&1 | tee /tmp/wave34-build.log +``` + +Expected outcome: ideally clean. Realistic outcome: a small number of errors at sites the inventory missed. + +- [ ] **Step 9.2: Triage build errors** + +Likely error patterns and their fixes: + +| Error | Fix | +|---|---| +| `cannot convert value of type 'EnginePeer' to expected argument type 'Peer'` at site `(peer.peer, ...)` | Add `._asPeer()` bridge: `(peer.peer._asPeer(), ...)` | +| `value of type 'EnginePeer' has no member 'isEqual'` | Replace with `==`: `peer.peer == otherPeer` | +| `cannot convert value of type 'EnginePeer' to expected argument type 'Peer?'` | Same as above: `._asPeer()` bridge | +| `pattern of type 'TelegramX' cannot match values of type 'EnginePeer'` | Missed C2 site — rewrite to `if case .X = peer.peer` form | +| `extraneous argument 'EnginePeer'` (when `.localPeer` etc. expected raw Peer) | Add `._asPeer()` bridge | + +For each error, identify the file:line, apply the appropriate fix, and re-run the build (Step 9.1) until clean. + +- [ ] **Step 9.3: Iterate to clean build** + +Re-run the build after each batch of fixes. The wave is complete when the build returns 0 errors. + +If 10+ unexpected errors surface, halt and reassess: the inventory was significantly incomplete and the wave may need to be split into pre-cleanup commits. Discuss with user. + +--- + +## Task 10: Post-build grep validations + +- [ ] **Step 10.1: Bridge-drop validation** + +Run: + +```bash +grep -rn "FoundPeer(peer:.*\._asPeer()" submodules/ --include="*.swift" | grep -v "^submodules/TelegramCore/" | grep -v "^submodules/Postbox/" +``` + +Expected: zero hits. If any remain, they are missed bridge-drops — fix and re-build. + +- [ ] **Step 10.2: C3 wrap validation** + +Run for each touched consumer file: + +```bash +for f in submodules/TelegramCallsUI/Sources/VideoChatScreen.swift \ + submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift \ + submodules/ContactListUI/Sources/ContactListNode.swift \ + submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift \ + submodules/TelegramBaseController/Sources/TelegramBaseController.swift \ + "submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift"; do + echo "=== $f ===" + grep -n "EnginePeer(peer\.peer)" "$f" +done +``` + +Expected: zero hits in each touched file. + +- [ ] **Step 10.3: C2 downcast validation** + +Run: + +```bash +for f in submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift \ + submodules/ContactListUI/Sources/ContactListNode.swift \ + submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift \ + submodules/TelegramBaseController/Sources/TelegramBaseController.swift; do + echo "=== $f ===" + grep -nE "peer\.peer\s+(as\?|is)\s+Telegram" "$f" +done +``` + +Expected: zero hits. + +If any of the validations fail, return to Task 9 to fix. + +--- + +## Task 11: Single atomic commit + memory + log update + +- [ ] **Step 11.1: Stage and review** + +Run: + +```bash +git status --short +git diff --stat +``` + +Confirm exactly 8 modified files (1 TelegramCore + 7 consumer) and no other unintended changes. WIP from earlier (`build-system/bazel-rules/sourcekit-bazel-bsp`, `ChatListFilterPresetController.swift`, `ChatListFilterPresetListController.swift`, untracked `build-system/tulsi/`, `submodules/TgVoip/`, `third-party/libx264/`) should NOT be staged. + +- [ ] **Step 11.2: Stage only the wave-34 files** + +Run: + +```bash +git add submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift \ + submodules/TelegramCallsUI/Sources/VideoChatScreen.swift \ + submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift \ + submodules/ContactListUI/Sources/ContactListNode.swift \ + submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift \ + submodules/TelegramBaseController/Sources/TelegramBaseController.swift \ + "submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift" +``` + +- [ ] **Step 11.3: Commit** + +Run: + +```bash +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 34: FoundPeer.peer Peer -> EnginePeer + +Migrates the public field `FoundPeer.peer` from the Postbox `Peer` protocol +to the TelegramCore `EnginePeer` enum. The `_internal_searchPeers` body +keeps `import Postbox` (it still calls `postbox.transaction`) and wraps raw +peer values with `EnginePeer(peer)` at the FoundPeer constructor sites. + +Consumer-side cascade in 7 files: + - 5 `._asPeer()` bridge-drops at FoundPeer constructor sites + - 22 redundant `EnginePeer(peer.peer)` wrap drops (the field is now + EnginePeer, so the wrap fails to compile) + - 30 `peer.peer as? TelegramX` / `is TelegramX` Postbox-concrete + downcasts rewritten to `if case .X = peer.peer` enum-pattern form + - 3 `._asPeer()` bridges added where `peer.peer` flows into + `ContactListPeer.peer(peer:)` (downstream API still takes raw Peer) + - Manual `==` body updated from `lhs.peer.isEqual(rhs.peer)` to + `lhs.peer == rhs.peer` (EnginePeer is Equatable) + +Files modified: + submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift + submodules/TelegramCallsUI/Sources/VideoChatScreen.swift + submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift + submodules/ContactListUI/Sources/ContactListNode.swift + submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift + submodules/TelegramBaseController/Sources/TelegramBaseController.swift + submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift + +Plan: docs/superpowers/plans/2026-04-24-foundpeer-engine-peer-migration.md +Spec: docs/superpowers/specs/2026-04-24-foundpeer-engine-peer-migration-design.md + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 11.4: Update CLAUDE.md wave counter** + +Edit `CLAUDE.md` to bump the "Waves landed so far" line from "33 waves" to "34 waves" and update the "as of" date if changed. + +- [ ] **Step 11.5: Append wave outcome to the postbox-refactor-log** + +Append a new "Wave 34 outcome" section to `docs/superpowers/postbox-refactor-log.md` documenting the migration, the actual edit count (in case it differed from the planned ~70), and any lessons learned. Keep concise. + +- [ ] **Step 11.6: Commit the docs update** + +Run: + +```bash +git add CLAUDE.md docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: add wave 34 outcome (FoundPeer.peer Peer→EnginePeer) + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 11.7: Update the next-wave memory** + +Update `/Users/isaac/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`: +- Add wave 34 to the "Latest commits" section +- Move FoundPeer migration from "Wave 34+ candidates" to landed +- Reframe remaining work: SendAsPeer, makePeerInfoController, etc., are now the next ring of Peer-typed-API waves +- Note any newly-surfaced bridge-add sites (the 3 `._asPeer()` bridges in ContactListNode point to `ContactListPeer.peer(peer:)` as the next downstream-API migration target) + +Use the Edit tool on the memory file. No git commit needed for the memory file (it's outside the repo). + +--- + +## Risks and notes + +- **Inner `peer` shadowing.** Several rewrites collapse `else if let peer = peer.peer as? TelegramChannel` patterns where the inner `peer` shadows the loop variable. The rewrites use `channel` as the new binding name to avoid double shadowing of the EnginePeer loop variable. Confirm subsequent uses of the bound name within the if-let scope are updated (e.g., `peer.info` becomes `channel.info`). +- **`replace_all` correctness.** Whenever the plan suggests `replace_all=true`, verify the count first via grep. If the count is unexpected, revert to per-site Edits with surrounding context. +- **Outflow-bridge surprises.** The plan enumerates 3 `._asPeer()` outflow bridges in ContactListNode. The build pass (Task 9) may surface 1–3 more in other touched files (e.g., ChatListSearchListPaneNode's `.localPeer` case). Apply the bridge pattern to each and iterate. +- **WIP isolation.** Pre-existing modifications to `ChatListFilterPresetController.swift`, `ChatListFilterPresetListController.swift`, the `sourcekit-bazel-bsp` submodule marker, and untracked `build-system/tulsi/` / `submodules/TgVoip/` / `third-party/libx264/` are user WIP — do NOT stage them. Use the explicit `git add ` form in Step 11.2. diff --git a/docs/superpowers/plans/2026-04-24-makeChatQrCodeScreen-recentActions-engine-peer-migration.md b/docs/superpowers/plans/2026-04-24-makeChatQrCodeScreen-recentActions-engine-peer-migration.md new file mode 100644 index 0000000000..f22b33ca5e --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-makeChatQrCodeScreen-recentActions-engine-peer-migration.md @@ -0,0 +1,411 @@ +# Wave 40 — `makeChatQrCodeScreen` + `makeChatRecentActionsController` Peer → EnginePeer Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Bundle migrate two sibling `AccountContext` methods deferred from wave 39 — `makeChatQrCodeScreen` (4 consumer sites) and `makeChatRecentActionsController` (3 consumer sites) — from raw `peer: Peer` to `peer: EnginePeer`, applying the body-shadow pattern. + +**Architecture:** Body-shadow pattern (wave-38/39 style). Protocol + impl signatures change to `peer: EnginePeer`; each impl body gets a `let peer = peer._asPeer()` shadow so the downstream constructors (`ChatQrCodeScreenImpl`, `ChatRecentActionsController`) remain raw-`Peer` consumers (out of scope). + +**Tech Stack:** Swift, Bazel, iOS; TelegramCore / AccountContext / TelegramUI / PeerInfoUI / StatisticsUI / SettingsUI / ContactListUI / PeerInfoScreen submodules. + +**Reference:** Wave-39 "Out of scope" section in `docs/superpowers/specs/2026-04-24-makePeerInfoController-engine-peer-migration-design.md`. + +--- + +## Pre-flight classification + +**`makeChatQrCodeScreen` (4 consumer sites):** + +| # | Site | Shape | Edit | +|---|---|---|---| +| 1 | `SettingsSearchableItems.swift:974` | **Shape-A-variant** | Rewrite upstream `guard let peer = peer?._asPeer() else { return }` (line 971) → `guard let peer = peer else { return }`. Call stays `peer: peer`. | +| 2 | `SettingsSearchableItems.swift:992` | **Shape-A-variant** | Same pattern as #1 (upstream guard at line 989). | +| 3 | `ContactsController.swift:478` | **Shape-A** | Drop `._asPeer()` from `peer: peer._asPeer()` → `peer: peer`. Source: `Signal`. | +| 4 | `PeerInfoScreen.swift:4623` | **Shape-C** | Wrap: `peer: peer` → `peer: EnginePeer(peer)`. Source: `data.peer: Peer?`. | + +**`makeChatRecentActionsController` (3 consumer sites):** + +| # | Site | Shape | Edit | +|---|---|---|---| +| 5 | `ChannelAdminsController.swift:734` | **Shape-A** | Drop `._asPeer()`. Source: `engine.data.get(Peer.Peer(id:))` — `peer` is `EnginePeer` in the `guard let peer` on line 729. | +| 6 | `GroupStatsController.swift:915` | **Shape-A** | Drop `._asPeer()`. Source: `Signal` (mapToSignal at 906). | +| 7 | `PeerInfoScreenOpenChat.swift:115` | **Shape-C** | Wrap: `peer: peer` → `peer: EnginePeer(peer)`. Source: `self.data?.peer: Peer?`. | + +**Net bridge delta:** −5 `_asPeer()` drops (sites 1, 2, 3, 5, 6) + 2 `EnginePeer(...)` wraps (sites 4, 7) = **−3 net**. Sites 4 and 7 become ratchet markers for a future `PeerInfoScreenData.peer Peer → EnginePeer` wave. + +--- + +## File touch summary + +8 files: + +1. `submodules/AccountContext/Sources/AccountContext.swift` — protocol decls (2 lines). +2. `submodules/TelegramUI/Sources/SharedAccountContext.swift` — impl signatures + body shadows (2 sites). +3. `submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift` — 2 Shape-A-variant upstream guard rewrites. +4. `submodules/ContactListUI/Sources/ContactsController.swift` — 1 Shape-A drop. +5. `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift` — 1 Shape-C wrap. +6. `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift` — 1 Shape-A drop. +7. `submodules/StatisticsUI/Sources/GroupStatsController.swift` — 1 Shape-A drop. +8. `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift` — 1 Shape-C wrap. + +--- + +### Task 1: Update `AccountContext` protocol signatures + +**Files:** +- Modify: `submodules/AccountContext/Sources/AccountContext.swift:1401` and `:1461` + +- [ ] **Step 1: Update `makeChatRecentActionsController` decl** + +```swift +// old_string + func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController + +// new_string + func makeChatRecentActionsController(context: AccountContext, peer: EnginePeer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController +``` + +- [ ] **Step 2: Update `makeChatQrCodeScreen` decl** + +```swift +// old_string + func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController + +// new_string + func makeChatQrCodeScreen(context: AccountContext, peer: EnginePeer, threadId: Int64?, temporary: Bool) -> ViewController +``` + +--- + +### Task 2: Update `SharedAccountContext` impls with body-shadow + +**Files:** +- Modify: `submodules/TelegramUI/Sources/SharedAccountContext.swift:2302` (makeChatRecentActionsController) +- Modify: `submodules/TelegramUI/Sources/SharedAccountContext.swift:2730` (makeChatQrCodeScreen) + +- [ ] **Step 1: Update `makeChatRecentActionsController` impl** + +```swift +// old_string + public func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController { + return ChatRecentActionsController(context: context, peer: peer, adminPeerId: adminPeerId, starsState: starsState) + } + +// new_string + public func makeChatRecentActionsController(context: AccountContext, peer: EnginePeer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController { + let peer = peer._asPeer() + return ChatRecentActionsController(context: context, peer: peer, adminPeerId: adminPeerId, starsState: starsState) + } +``` + +- [ ] **Step 2: Update `makeChatQrCodeScreen` impl** + +```swift +// old_string + public func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController { + return ChatQrCodeScreenImpl(context: context, subject: .peer(peer: peer, threadId: threadId, temporary: temporary)) + } + +// new_string + public func makeChatQrCodeScreen(context: AccountContext, peer: EnginePeer, threadId: Int64?, temporary: Bool) -> ViewController { + let peer = peer._asPeer() + return ChatQrCodeScreenImpl(context: context, subject: .peer(peer: peer, threadId: threadId, temporary: temporary)) + } +``` + +--- + +### Task 3: `SettingsSearchableItems.swift` — two Shape-A-variant guard rewrites + +**Files:** +- Modify: `submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift:971` and `:989` + +Both sites share the same structure: an upstream `guard let peer = peer?._asPeer() else { return }` unwraps `EnginePeer?` to `Peer`. Rewrite the guard to keep the local as `EnginePeer`; the call site below stays unchanged. + +- [ ] **Step 1: Rewrite guard at line 971 (qr-code item)** + +```swift +// old_string + present: { context, _, present in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer?._asPeer() else { + return + } + let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false) + present(.push, controller) + }) + } + ) + ) + + //TODO:fix + items.append( + SettingsSearchableItem( + id: "qr-code/share", + +// new_string + present: { context, _, present in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer else { + return + } + let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false) + present(.push, controller) + }) + } + ) + ) + + //TODO:fix + items.append( + SettingsSearchableItem( + id: "qr-code/share", +``` + +- [ ] **Step 2: Rewrite guard at line 989 (qr-code/share item)** + +```swift +// old_string + id: "qr-code/share", + isVisible: false, + present: { context, _, present in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer?._asPeer() else { + return + } + let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false) + present(.push, controller) + }) + } + +// new_string + id: "qr-code/share", + isVisible: false, + present: { context, _, present in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer else { + return + } + let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false) + present(.push, controller) + }) + } +``` + +--- + +### Task 4: `ContactsController.swift` — Shape-A drop + +**Files:** +- Modify: `submodules/ContactListUI/Sources/ContactsController.swift:478` + +- [ ] **Step 1: Drop `._asPeer()` at the call site** + +```swift +// old_string + controller.present(strongSelf.context.sharedContext.makeChatQrCodeScreen(context: strongSelf.context, peer: peer._asPeer(), threadId: nil, temporary: false), in: .window(.root)) + +// new_string + controller.present(strongSelf.context.sharedContext.makeChatQrCodeScreen(context: strongSelf.context, peer: peer, threadId: nil, temporary: false), in: .window(.root)) +``` + +--- + +### Task 5: `PeerInfoScreen.swift` — Shape-C wrap + +**Files:** +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift:4623` + +Local `peer` comes from `data.peer` (type `Peer`). Wrap at the call site. + +- [ ] **Step 1: Wrap with `EnginePeer(...)`** + +```swift +// old_string + let qrController = self.context.sharedContext.makeChatQrCodeScreen(context: self.context, peer: peer, threadId: threadId, temporary: temporary) + +// new_string + let qrController = self.context.sharedContext.makeChatQrCodeScreen(context: self.context, peer: EnginePeer(peer), threadId: threadId, temporary: temporary) +``` + +--- + +### Task 6: `ChannelAdminsController.swift` — Shape-A drop + +**Files:** +- Modify: `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift:734` + +Local `peer` is `EnginePeer` (from `guard let peer` on :729 unwrapping `EnginePeer?` from `engine.data.get(Peer.Peer(id:))`). + +- [ ] **Step 1: Drop `._asPeer()`** + +```swift +// old_string + pushControllerImpl?(context.sharedContext.makeChatRecentActionsController(context: context, peer: peer._asPeer(), adminPeerId: nil, starsState: nil)) + +// new_string + pushControllerImpl?(context.sharedContext.makeChatRecentActionsController(context: context, peer: peer, adminPeerId: nil, starsState: nil)) +``` + +--- + +### Task 7: `GroupStatsController.swift` — Shape-A drop + +**Files:** +- Modify: `submodules/StatisticsUI/Sources/GroupStatsController.swift:915` + +Local `peer` is `EnginePeer` (from `Signal` via the `mapToSignal` at :906). + +- [ ] **Step 1: Drop `._asPeer()`** + +```swift +// old_string + let controller = context.sharedContext.makeChatRecentActionsController(context: context, peer: peer._asPeer(), adminPeerId: participantPeerId, starsState: nil) + +// new_string + let controller = context.sharedContext.makeChatRecentActionsController(context: context, peer: peer, adminPeerId: participantPeerId, starsState: nil) +``` + +--- + +### Task 8: `PeerInfoScreenOpenChat.swift` — Shape-C wrap + +**Files:** +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift:115` + +Local `peer` comes from `self.data?.peer` (type `Peer`). Wrap at the call site. + +- [ ] **Step 1: Wrap with `EnginePeer(...)`** + +```swift +// old_string + let controller = self.context.sharedContext.makeChatRecentActionsController(context: self.context, peer: peer, adminPeerId: nil, starsState: self.data?.starsRevenueStatsState) + +// new_string + let controller = self.context.sharedContext.makeChatRecentActionsController(context: self.context, peer: EnginePeer(peer), adminPeerId: nil, starsState: self.data?.starsRevenueStatsState) +``` + +--- + +### Task 9: Build + iterate + +- [ ] **Step 1: Run full build** + +```bash +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 \ + --continueOnError +``` + +Expected: first-pass-clean (wave 39's 50-file wave landed first-pass-clean; this 8-file wave should too). + +- [ ] **Step 2: If errors, iterate** + +Each error should point at a call site the plan missed. Fix, re-run. Do not widen the scope — if a call site not in the classification table above appears as an error, investigate whether the memory/inventory was stale. + +--- + +### Task 10: Verify no residue + +- [ ] **Step 1: Grep for raw-`Peer` sites** + +```bash +grep -rn "makeChatQrCodeScreen\|makeChatRecentActionsController" --include="*.swift" submodules/ +``` + +Expected output: 2 protocol-decl lines (AccountContext.swift), 2 impl-decl lines (SharedAccountContext.swift), and exactly 7 consumer sites — all with `peer: peer`, `peer: EnginePeer(peer)`, or similar (no `peer: x._asPeer()` remaining for these two methods). + +--- + +### Task 11: Commit + update refactor log + +**Files:** +- Modify: `docs/superpowers/postbox-refactor-log.md` — append wave-40 outcome section. + +- [ ] **Step 1: Stage exactly these 8 files (enumerate, do not use `git add -u`)** + +```bash +git add \ + submodules/AccountContext/Sources/AccountContext.swift \ + submodules/TelegramUI/Sources/SharedAccountContext.swift \ + submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift \ + submodules/ContactListUI/Sources/ContactsController.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift \ + submodules/PeerInfoUI/Sources/ChannelAdminsController.swift \ + submodules/StatisticsUI/Sources/GroupStatsController.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift \ + docs/superpowers/plans/2026-04-24-makeChatQrCodeScreen-recentActions-engine-peer-migration.md +``` + +- [ ] **Step 2: Verify staging with `git status --short`** + +Verify only the 9 files above are staged. If other files appear (e.g. `ChatMessageTransitionNode.swift` WIP, `sourcekit-bazel-bsp` submodule marker) — reset them out of the index with `git restore --staged ` and re-check. + +- [ ] **Step 3: Commit (wave 40)** + +```bash +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 40 + +makeChatQrCodeScreen + makeChatRecentActionsController peer Peer->EnginePeer. + +- AccountContext protocol: 2 decls updated +- SharedAccountContext impls: 2 signatures + 2 body-shadow `let peer = peer._asPeer()` +- 5 Shape-A `._asPeer()` drops (SettingsSearchableItems x2 guard-variant, ContactsController, ChannelAdminsController, GroupStatsController) +- 2 Shape-C `EnginePeer(peer)` wraps (PeerInfoScreen, PeerInfoScreenOpenChat) +- Net: -3 bridges + +Sibling follow-up to wave 39 (makePeerInfoController). Pre-flight classification +completed in wave-39 design doc's "Out of scope" section. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: Log wave 40 outcome** + +Append a new section to `docs/superpowers/postbox-refactor-log.md`: + +```markdown +## Wave 40 outcome + +Commit: ``. Bundle of `AccountContext.makeChatQrCodeScreen` + `makeChatRecentActionsController` peer `Peer → EnginePeer`. 8 files / ~12 lines changed. Pre-flight classification from wave-39 design doc held: 5 Shape-A drops + 2 Shape-C wraps + 2 impl body-shadows + 2 protocol decls. Net −3 bridges. Build outcome: . + +Sibling follow-up to wave 39 — completes the "Option 1 cluster" (makePeerInfoController family from wave-38 memory). Ratchet markers installed at PeerInfoScreen:4623 and PeerInfoScreenOpenChat:115 for a future `PeerInfoScreenData.peer Peer → EnginePeer` wave. +``` + +Then commit the log update: + +```bash +git add docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: log wave 40 outcome + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 5: Update memory** + +Update `/Users/isaac/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`: +- Move wave-40 (this bundle) from "candidates" to "Latest commits". +- Bump wave-41 recommendation to RenderedChannelParticipant.peer (Option 3) or RenderedPeer (Option 2). +- Add wave-40 lesson if any (e.g. "bundled sibling migration with shared pre-flight is cheap" or similar). + +--- + +## Self-review checklist (writing-plans skill) + +- **Spec coverage:** Every site from the memory/wave-39-doc pre-flight is a task. Sites 1+2 → Task 3; Site 3 → Task 4; Site 4 → Task 5; Site 5 → Task 6; Site 6 → Task 7; Site 7 → Task 8. Impl bodies → Task 2. Protocol → Task 1. Build → Task 9. Verify → Task 10. Commit+log → Task 11. ✓ +- **Placeholders:** None. Every Edit step has exact `old_string` / `new_string`. Commit message and log-update text are spelled out. ✓ +- **Type consistency:** Both methods take `peer: EnginePeer` everywhere — protocol decl, impl decl, and call sites' parameter passes. ✓ diff --git a/docs/superpowers/plans/2026-04-24-makePeerInfoController-engine-peer-migration.md b/docs/superpowers/plans/2026-04-24-makePeerInfoController-engine-peer-migration.md new file mode 100644 index 0000000000..16e9a16adb --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-makePeerInfoController-engine-peer-migration.md @@ -0,0 +1,1037 @@ +# Wave 39 — `makePeerInfoController` Peer → EnginePeer Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate the `peer:` parameter of `AccountContext.makePeerInfoController` from raw `Peer` to `EnginePeer`, dropping 61 `_asPeer()` bridges and adding 12 `EnginePeer(...)` wraps. + +**Architecture:** Body-shadow pattern (wave-38 style). The protocol and impl signatures change to `peer: EnginePeer`. Inside the impl body, a `let peer = peer._asPeer()` shadow preserves the downstream `peerInfoControllerImpl` (private, same file) as a raw-`Peer` consumer — out of scope for this wave. + +**Tech Stack:** Swift, Bazel, iOS; TelegramCore / AccountContext / TelegramUI submodules. + +**Spec:** `docs/superpowers/specs/2026-04-24-makePeerInfoController-engine-peer-migration-design.md` + +--- + +## File touch summary + +- **Signature** (2 files): `submodules/AccountContext/Sources/AccountContext.swift`, `submodules/TelegramUI/Sources/SharedAccountContext.swift`. +- **Shape-A-variant** (1 file): `submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift` (3 sites). +- **Shape-C** (8 files, 12 sites): BlockedPeersController, ChannelMembersController, ChannelBlacklistController, ChatRecentActionsControllerNode, PeerInfoScreen, ChatControllerNavigationButtonAction (4), ChatControllerOpenPeer (2), ChatControllerLoadDisplayNode. +- **Shape-A** (~42 files, 58 sites): inline `peer: x._asPeer()` → `peer: x` drops. + +Total: ~50 files. + +--- + +### Task 1: Update `AccountContext` protocol signature + +**Files:** +- Modify: `submodules/AccountContext/Sources/AccountContext.swift:1371` + +- [ ] **Step 1: Apply edit** + +Change the protocol declaration to take `peer: EnginePeer` instead of `peer: Peer`. + +```swift +// old_string + func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController? + +// new_string + func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController? +``` + +--- + +### Task 2: Update `SharedAccountContext` implementation + body-shadow + 3 self-call Shape-A drops + +**Files:** +- Modify: `submodules/TelegramUI/Sources/SharedAccountContext.swift:1937` (signature + body) +- Modify: `submodules/TelegramUI/Sources/SharedAccountContext.swift:3335, 3483, 4016` (Shape-A drops — 3 sites where `self.makePeerInfoController(...)` or `context.sharedContext.makePeerInfoController(...)` is called with `peer: peer._asPeer()`) + +- [ ] **Step 1: Update the impl signature and add body-shadow** + +```swift +// old_string + public func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController? { + let controller = peerInfoControllerImpl(context: context, updatedPresentationData: updatedPresentationData, peer: peer, mode: mode, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: fromChat) + controller?.navigationPresentation = .modalInLargeLayout + return controller + } + +// new_string + public func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController? { + let peer = peer._asPeer() + let controller = peerInfoControllerImpl(context: context, updatedPresentationData: updatedPresentationData, peer: peer, mode: mode, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: fromChat) + controller?.navigationPresentation = .modalInLargeLayout + return controller + } +``` + +- [ ] **Step 2: Drop `._asPeer()` at line 3335** (inside `openProfileImpl = { [weak self, weak controller] peer in ...`) + +```swift +// old_string + peer: peer._asPeer(), + mode: .generic, + avatarInitiallyExpanded: peer.smallProfileImage != nil, + fromChat: false, + requestsContext: nil + ) { + controller.replace(with: infoController) + +// new_string + peer: peer, + mode: .generic, + avatarInitiallyExpanded: peer.smallProfileImage != nil, + fromChat: false, + requestsContext: nil + ) { + controller.replace(with: infoController) +``` + +- [ ] **Step 3: Drop `._asPeer()` at line 3483** + +Read 10 lines of context around 3483 to select a unique `old_string` and replace the `peer: peer._asPeer(),` line with `peer: peer,`. The surrounding arguments vary from the line-3335 site, so use a larger surrounding context (6+ lines) to make `old_string` unique. + +- [ ] **Step 4: Drop `._asPeer()` at line 4016** (inside `navigateToPeer: { [weak self] peer in ...` in `makeStarsTransferScreen`) + +```swift +// old_string + peer: peer._asPeer(), + mode: .generic, + avatarInitiallyExpanded: peer.smallProfileImage != nil, + fromChat: false, + requestsContext: nil + ) { + if let navigationController = self.mainWindow?.viewController as? NavigationController { + navigationController.pushViewController(infoController) + +// new_string + peer: peer, + mode: .generic, + avatarInitiallyExpanded: peer.smallProfileImage != nil, + fromChat: false, + requestsContext: nil + ) { + if let navigationController = self.mainWindow?.viewController as? NavigationController { + navigationController.pushViewController(infoController) +``` + +--- + +### Task 3: Shape-A-variant drops in `SettingsSearchableItems.swift` + +**Files:** +- Modify: `submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift` (lines 1020, 1046, 1080 — the guard statements upstream of the makePeerInfoController calls at 1023, 1049, 1083) + +The call-site line (`peer: peer,`) does not change — the upstream `guard let peer = peer?._asPeer() else` changes to `guard let peer = peer else`, making the local `peer` stay as `EnginePeer` instead of being unwrapped to raw `Peer`. + +- [ ] **Step 1: Line 1020** (inside the `id: "my-profile"` item) + +```swift +// old_string + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer?._asPeer() else { + return + } + let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer, + mode: .myProfile, + +// new_string + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer else { + return + } + let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer, + mode: .myProfile, +``` + +- [ ] **Step 2: Line 1046** (inside the `id: "my-profile/edit"` item) + +The `old_string` is nearly identical to Step 1. Since Edit tool requires uniqueness, use a broader context including the distinguishing `controller.activateEdit()` suffix (at line ~1062) or use `replace_all=false` with a larger context block. Recommended: include the `Queue.mainQueue().justDispatch { if let controller = controller as? PeerInfoScreen { controller.activateEdit() } }` block below the `present(.push, controller)` line to disambiguate. + +```swift +// old_string + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer?._asPeer() else { + return + } + let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer, + mode: .myProfile, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) + present(.push, controller) + + Queue.mainQueue().justDispatch { + if let controller = controller as? PeerInfoScreen { + controller.activateEdit() + } + } + }) + +// new_string + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer else { + return + } + let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer, + mode: .myProfile, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) + present(.push, controller) + + Queue.mainQueue().justDispatch { + if let controller = controller as? PeerInfoScreen { + controller.activateEdit() + } + } + }) +``` + +- [ ] **Step 3: Line 1080** (inside the `id: "my-profile/gifts"` item — distinguished by `mode: .myProfileGifts`) + +```swift +// old_string + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer?._asPeer() else { + return + } + let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer, + mode: .myProfileGifts, + +// new_string + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer else { + return + } + let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer, + mode: .myProfileGifts, +``` + +--- + +### Task 4: Shape-C wraps (12 sites across 8 files) + +**Files:** +- Modify: `submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift:270` +- Modify: `submodules/PeerInfoUI/Sources/ChannelMembersController.swift:707` +- Modify: `submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift:381` +- Modify: `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift:1011` +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift:4306` +- Modify: `submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift:441, 461, 471, 492` +- Modify: `submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift:218, 359` +- Modify: `submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift:4362` + +- [ ] **Step 1: BlockedPeersController.swift:270** + +```swift +// old_string + }, openPeer: { peer in + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + }, openPeer: { peer in + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: EnginePeer(peer), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 2: ChannelMembersController.swift:707** + +```swift +// old_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: participant.peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + pushControllerImpl?(infoController) + } + } + }, inviteViaLink: { + +// new_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: EnginePeer(participant.peer), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + pushControllerImpl?(infoController) + } + } + }, inviteViaLink: { +``` + +- [ ] **Step 3: ChannelBlacklistController.swift:381** + +```swift +// old_string + } else if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: participant.peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + pushControllerImpl?(infoController) + } + })) + +// new_string + } else if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: EnginePeer(participant.peer), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + pushControllerImpl?(infoController) + } + })) +``` + +- [ ] **Step 4: ChatRecentActionsControllerNode.swift:1011** + +```swift +// old_string + } else { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + strongSelf.pushController(infoController) + } + } + +// new_string + } else { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: EnginePeer(peer), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + strongSelf.pushController(infoController) + } + } +``` + +- [ ] **Step 5: PeerInfoScreen.swift:4306** (inside `openPeerInfo(peer: Peer, isMember: Bool)`) + +```swift +// old_string + private func openPeerInfo(peer: Peer, isMember: Bool) { + let mode: PeerInfoControllerMode = .generic + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: mode, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + private func openPeerInfo(peer: Peer, isMember: Bool) { + let mode: PeerInfoControllerMode = .generic + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: EnginePeer(peer), mode: mode, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 6: ChatControllerNavigationButtonAction.swift:441** + +Context: the outer `if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer` binds `peer` to raw `Peer` (from `RenderedPeer.chatMainPeer` in `TelegramCore/Sources/Utils/PeerUtils.swift:512`). Wrap at the call site. + +```swift +// old_string + if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer, let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { + self.effectiveNavigationController?.pushViewController(infoController) + } + +// new_string + if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer, let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), mode: .generic, avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { + self.effectiveNavigationController?.pushViewController(infoController) + } +``` + +- [ ] **Step 7: ChatControllerNavigationButtonAction.swift:461** + +Context: `peer` here is the outer `peer` bound from `peerView.peers[peerView.peerId]` (raw Peer) at line 418. Wrap at call site. + +```swift +// old_string + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer, mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: true, requestsContext: self.contentData?.inviteRequestsContext) { + self.effectiveNavigationController?.pushViewController(infoController) + } + } + + let _ = self.dismissPreviewing?(false) + +// new_string + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: true, requestsContext: self.contentData?.inviteRequestsContext) { + self.effectiveNavigationController?.pushViewController(infoController) + } + } + + let _ = self.dismissPreviewing?(false) +``` + +- [ ] **Step 8: ChatControllerNavigationButtonAction.swift:471** + +Context: `if let peer = self.presentationInterfaceState.renderedPeer?.peer` — raw Peer from Postbox RenderedPeer. Wrap at call site. + +```swift +// old_string + if let peer = self.presentationInterfaceState.renderedPeer?.peer, case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.peerId == self.context.account.peerId { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer, mode: .forumTopic(thread: replyThreadMessage), avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { + self.effectiveNavigationController?.pushViewController(infoController) + } + +// new_string + if let peer = self.presentationInterfaceState.renderedPeer?.peer, case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.peerId == self.context.account.peerId { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), mode: .forumTopic(thread: replyThreadMessage), avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { + self.effectiveNavigationController?.pushViewController(infoController) + } +``` + +- [ ] **Step 9: ChatControllerNavigationButtonAction.swift:492** + +Context: `channel` is bound from `self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel` — raw `TelegramChannel`/`Peer`. Wrap at call site. + +```swift +// old_string + } else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum, case let .replyThread(message) = self.chatLocation { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: channel, mode: .forumTopic(thread: message), avatarInitiallyExpanded: false, fromChat: true, requestsContext: self.contentData?.inviteRequestsContext) { + +// new_string + } else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum, case let .replyThread(message) = self.chatLocation { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(channel), mode: .forumTopic(thread: message), avatarInitiallyExpanded: false, fromChat: true, requestsContext: self.contentData?.inviteRequestsContext) { +``` + +- [ ] **Step 10: ChatControllerOpenPeer.swift:218** + +Context: `peer` is raw Peer from the outer closure parameter. + +```swift +// old_string + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: false, requestsContext: nil) { + strongSelf.effectiveNavigationController?.pushViewController(infoController) + } + +// new_string + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: false, requestsContext: nil) { + strongSelf.effectiveNavigationController?.pushViewController(infoController) + } +``` + +- [ ] **Step 11: ChatControllerOpenPeer.swift:359** + +Context: `let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer` — raw Peer. + +```swift +// old_string + guard let self, let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer else { + return + } + + guard let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + +// new_string + guard let self, let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer else { + return + } + + guard let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: EnginePeer(peer), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { +``` + +- [ ] **Step 12: ChatControllerLoadDisplayNode.swift:4362** + +Context: `let peer = self.presentationInterfaceState.renderedPeer?.peer` — raw Peer. + +```swift +// old_string + guard let self, let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + if let controller = self.context.sharedContext.makePeerInfoController( + context: self.context, + updatedPresentationData: nil, + peer: peer, + mode: .gifts, + +// new_string + guard let self, let peer = self.presentationInterfaceState.renderedPeer?.peer else { + return + } + if let controller = self.context.sharedContext.makePeerInfoController( + context: self.context, + updatedPresentationData: nil, + peer: EnginePeer(peer), + mode: .gifts, +``` + +--- + +### Task 5: Shape-A drops across 42 files (55 remaining sites) + +Each Shape-A site swaps `peer: ._asPeer()` for `peer: ` at the listed line. Edits are mechanical; bundle into a single implementer dispatch if using subagent-driven development (wave-38 lesson). + +**Files with single sites (replace `peer: ._asPeer(),` or `peer: ._asPeer()` with the `_asPeer()` dropped):** + +Most sites have the pattern `peer: peer._asPeer()`. Use the full single-line `makePeerInfoController(...)` call as `old_string` when replacing to guarantee uniqueness. Two sites use different receivers: + +- `submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift:878` uses `peer: component.sourcePeer._asPeer(),` — drop to `peer: component.sourcePeer,`. +- `submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift:486` uses `peer: peer._asPeer()` where `peer` is `EnginePeer` (local name shadowed earlier inside `Task {}`). Drop to `peer: peer,`. + +- [ ] **Step 1: SelectivePrivacySettingsPeersController.swift:509** + +```swift +// old_string + guard let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + +// new_string + guard let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { +``` + +- [ ] **Step 2: InstantPageControllerNode.swift:1766** + +```swift +// old_string + if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 3: CallListController.swift:375** + +```swift +// old_string + if let strongSelf = self, let peer = peer, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .calls(messages: messages.map({ $0._asMessage() })), avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let strongSelf = self, let peer = peer, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .calls(messages: messages.map({ $0._asMessage() })), avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 4: ContactsController.swift:777** + +```swift +// old_string + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 5: ContactContextMenus.swift:42** + +```swift +// old_string + guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + +// new_string + guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { +``` + +- [ ] **Step 6: SecureIdAuthController.swift:343** + +```swift +// old_string + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 7: ChannelAdminController.swift:1233** + +```swift +// old_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 8: ChannelMembersController.swift:785** + +```swift +// old_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 9: ChannelBannedMemberController.swift:785** + +```swift +// old_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 10: ChannelPermissionsController.swift:1111** + +```swift +// old_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 11: GroupStatsController.swift:883** + +```swift +// old_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 12: MessageStatsController.swift:604** + +```swift +// old_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { + +// new_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 13: InviteRequestsController.swift:379** + +```swift +// old_string + if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { + +// new_string + if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 14: BrowserInstantPageContent.swift:1501** + +```swift +// old_string + if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 15: WebAppController.swift:3375** + +```swift +// old_string + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 16: PeersNearbyController.swift:629** + +```swift +// old_string + if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .nearbyPeer(distance: distance), avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { + +// new_string + if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .nearbyPeer(distance: distance), avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 17: ChatSendStarsScreen.swift:2296** (multi-line call; edit the `peer: peer._asPeer(),` line only; include surrounding lines for uniqueness) + +Read the 5-line context around line 2296, then edit: + +```swift +// old_string (substring) + peer: peer._asPeer(), + +// new_string + peer: peer, +``` + +For uniqueness, include at least one neighboring argument line (e.g., the `mode:` line above or below) in the `old_string`. If truly unique (no other `peer: peer._asPeer(),` lines in this file), `replace_all=false` with this substring works. + +- [ ] **Step 18: ChatRecentActionsControllerNode.swift:1031** + +```swift +// old_string + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 19: MiniAppListScreen.swift:230** + +```swift +// old_string + if let peerInfoScreen = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let peerInfoScreen = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 20: JoinSubjectScreen.swift:320** (multi-line) + +Same pattern as Step 17. The `peer: peer._asPeer(),` line at 320 is the only such substring in the file; include the surrounding `mode:` line for safety. + +- [ ] **Step 21: NewContactScreen.swift:586** + +```swift +// old_string + if let infoController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 22: StarsTransactionScreen.swift:1958** + +```swift +// old_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 23: StoryItemSetContainerComponent.swift:5629** + +```swift +// old_string + guard let chatController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + +// new_string + guard let chatController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { +``` + +- [ ] **Step 24: StoryItemSetContainerComponent.swift:7619** (multi-line; same pattern as Step 17) + +- [ ] **Step 25: StoryItemSetContainerViewSendMessage.swift:3132** + +```swift +// old_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 26: StoryItemSetContainerViewSendMessage.swift:3387** + +```swift +// old_string + if let infoController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer, mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 27: GiftViewScreen.swift:413, 561, 2252** (three multi-line sites in the same file; use `replace_all=false` with per-site surrounding context for uniqueness) + +Each site has `peer: peer._asPeer(),`. Read 4-line contexts around each line number to construct unique `old_string` blocks. Replace `peer: peer._asPeer(),` with `peer: peer,` in each. + +- [ ] **Step 28: GiftOptionsScreen.swift:930** (multi-line; same pattern as Step 17) + +- [ ] **Step 29: StorageUsageScreen.swift:2078** (multi-line; same pattern as Step 17) + +- [ ] **Step 30: TextProcessingScreen.swift:795** + +```swift +// old_string + if let peerInfoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let peerInfoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 31: PeerInfoScreen.swift:6915, 7218** (two sites — the `old_string`s look identical; use `replace_all=true` or larger per-site context) + +Both lines are identical: `if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {`. If identical in full, `replace_all=true` with this `old_string` handles both. Verify by reading the two surrounding blocks first — if surrounding context differs, use two focused Edit calls instead. + +```swift +// old_string (both occurrences) + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 32: PeerInfoScreenOpenURL.swift:28** + +```swift +// old_string + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 33: JoinAffiliateProgramScreen.swift:878** (multi-line; `peer: component.sourcePeer._asPeer(),` → `peer: component.sourcePeer,`) + +- [ ] **Step 34: ChatControllerScrollToPointInHistory.swift:162** (multi-line; `peer: peer._asPeer(),` → `peer: peer,`) + +- [ ] **Step 35: OpenUrl.swift:175** + +```swift +// old_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 36: OpenUrl.swift:535** (multi-line; `peer: peer._asPeer(),` → `peer: peer,`) + +- [ ] **Step 37: OpenResolvedUrl.swift:1810, 1831** (two multi-line sites; `peer: peer._asPeer(),` → `peer: peer,`; use per-site surrounding context for uniqueness) + +- [ ] **Step 38: TextLinkHandling.swift:45** + +```swift +// old_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 39: ChatController.swift:9437** + +```swift +// old_string + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 40: ChatManagingBotTitlePanelNode.swift:356** + +```swift +// old_string + if let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 41: NavigateToChatController.swift:143** + +```swift +// old_string + if let controller = params.context.sharedContext.makePeerInfoController(context: params.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let controller = params.context.sharedContext.makePeerInfoController(context: params.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 42: OverlayAudioPlayerControllerNode.swift:739** (multi-line) + +- [ ] **Step 43: OpenAddContact.swift:28** + +```swift +// old_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 44: PollResultsController.swift:451** + +```swift +// old_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 45: ChatControllerOpenWebApp.swift:485** + +```swift +// old_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 46: ChatControllerNavigationButtonAction.swift:486** + +Context: inside `Task { @MainActor [weak self] in ...` — local `peer` was bound from `context.engine.data.get(...).get()` (returns `EnginePeer?`), and `_asPeer()` is called to pass to the current Peer-typed API. + +```swift +// old_string + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer._asPeer(), mode: .monoforum(monoforumPeer.id), avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { + +// new_string + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer, mode: .monoforum(monoforumPeer.id), avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { +``` + +- [ ] **Step 47: ChatListController.swift:1913** + +```swift +// old_string + if let peerInfoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let peerInfoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +- [ ] **Step 48: ChatListController.swift:3309** + +```swift +// old_string + guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + +// new_string + guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { +``` + +- [ ] **Step 49: ChatListController.swift:3795** + +```swift +// old_string + guard let sourceController = sourceController, let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + +// new_string + guard let sourceController = sourceController, let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { +``` + +- [ ] **Step 50: ChatListController.swift:7301** + +```swift +// old_string + guard let self, let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + +// new_string + guard let self, let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { +``` + +- [ ] **Step 51: ChatListSearchListPaneNode.swift:4464** + +```swift +// old_string + if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + +// new_string + if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { +``` + +**Site count check:** 51 steps listed. Multi-line sites (Steps 17, 20, 24, 27, 28, 29, 33, 34, 36, 37, 42) contain 1–3 sub-sites each; Step 27 covers 3 GiftViewScreen sites, Step 31 covers 2 PeerInfoScreen sites, Step 37 covers 2 OpenResolvedUrl sites. Running total: 51 + 2 (step 27 extras) + 1 (step 31 extra) + 1 (step 37 extra) = 55 sites. Adding 3 self-call sites in Task 2 = 58 Shape-A total. Correct. + +--- + +### Task 6: Full project build verification + +- [ ] **Step 1: Run build with `--continueOnError`** + +```bash +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 \ + --configuration=debug_sim_arm64 --continueOnError +``` + +Expected: build succeeds. If errors, collect the error output for Task 7. + +Budget: 2–4 iterations. Expected runtime: 200–600 seconds per full build. + +--- + +### Task 7: Fix iteration-surfaced errors (if any) + +- [ ] **Step 1: Classify errors** + +Common expected categories: +- **Stored-field type mismatches:** a call site passes `peer` declared as raw `Peer` where the migrated API now wants `EnginePeer`. Fix with `EnginePeer(peer)` wrap at the call site (new Shape-C). +- **Closure-parameter type mismatches:** an outer closure types `peer` as `Peer`, fixed by wrapping at the call site with `EnginePeer(peer)`. +- **Downstream inference cascades:** transient errors in Peer-typed helpers not directly calling `makePeerInfoController`. Verify the helper is not `peerInfoControllerImpl` — if it is, investigate whether the body-shadow boundary was violated (abandonment criterion). + +- [ ] **Step 2: Apply fixes** + +For each unique error site, apply the minimum wrap/drop/shadow change. Do not edit `peerInfoControllerImpl` or any file outside the consumer set. If an error surfaces in `TelegramCore`/`Postbox`/`TelegramApi`, abandon. + +- [ ] **Step 3: Rerun the build** + +Loop back to Task 6 Step 1. Halt at iteration 5 per abandonment criterion. + +--- + +### Task 8: Commit atomically + +- [ ] **Step 1: Review staged diff** + +```bash +git status --short +git diff --stat +``` + +Expected: ~50 files touched. No unexpected files (e.g., `TelegramCore` or `Postbox` edits). + +- [ ] **Step 2: Stage and commit** + +```bash +git add \ + submodules/AccountContext/Sources/AccountContext.swift \ + submodules/TelegramUI/Sources/SharedAccountContext.swift \ + # ... (all touched files) + +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 39 + +Migrate AccountContext.makePeerInfoController's peer: parameter from +raw Peer to EnginePeer. Body-shadow pattern preserves downstream +peerInfoControllerImpl as a raw-Peer consumer (out of scope). + +73 consumer call sites across ~50 files: 58 Shape-A _asPeer() drops, +3 Shape-A-variant guard-statement drops, 12 Shape-C EnginePeer(...) +wraps. Net -49 bridges. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 3: Verify commit** + +```bash +git log --oneline -n 3 +git show --stat HEAD +``` + +--- + +### Task 9: Update memory + log + CLAUDE.md + +- [ ] **Step 1: Append outcome entry to `docs/superpowers/postbox-refactor-log.md`** + +Append a "Wave 39 outcome" section with: commit SHA, file count, line-change count, Shape-A/Shape-A-variant/Shape-C tallies, build iteration count, any surprises, any lesson worth adding to wave-selection guidance. + +- [ ] **Step 2: Update `project_postbox_refactor_next_wave.md` memory** + +Rewrite "Latest commits" and "Wave 39/40 candidates" sections. Promote `makeChatQrCodeScreen` + `makeChatRecentActionsController` to wave 40 (the trivial follow-up). Keep `RenderedPeer → EngineRenderedPeer`, accountManager-side engine path, Shape-C resourceData, and cached-rep triple on the shortlist. + +- [ ] **Step 3: Update `CLAUDE.md` wave tally** + +The "Waves landed so far" line on CLAUDE.md:44 currently says "36 waves plus standalone cleanups" (stale — waves 37 and 38 landed 2026-04-24 but CLAUDE.md wasn't updated). Bump the tally to "39 waves plus standalone cleanups". + +- [ ] **Step 4: Commit the docs update** + +```bash +git add docs/superpowers/postbox-refactor-log.md CLAUDE.md +git commit -m "$(cat <<'EOF' +docs: log wave 39 outcome + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +Memory file is in `~/.claude/projects/.../memory/` — saved via Write, not git-committed. + +--- + +## Self-review checklist (run before handoff) + +- [x] Every numbered Shape-A site in the spec has a corresponding Step in Task 5 (or Task 2/3 for in-signature-file sites). +- [x] Every Shape-C site in the spec table has a corresponding Step in Task 4. +- [x] Protocol + impl signatures shown verbatim. +- [x] Build command includes `source ~/.zshrc 2>/dev/null;` prefix and `--continueOnError`. +- [x] Abandonment criteria inherited from spec. +- [x] No placeholders, TBD, or "implement later". +- [x] Commit message matches the project's wave-NN style (see `git log` output for prior waves). diff --git a/docs/superpowers/plans/2026-04-24-peerinfoscreen-helpers-engine-peer-migration.md b/docs/superpowers/plans/2026-04-24-peerinfoscreen-helpers-engine-peer-migration.md new file mode 100644 index 0000000000..560e79b10c --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-peerinfoscreen-helpers-engine-peer-migration.md @@ -0,0 +1,658 @@ +# Wave 43 plan: PeerInfoScreen helpers `peer: Peer?` → `peer: EnginePeer?` + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate six PeerInfoScreen module helpers (`canEditPeerInfo`, `availableActionsForMemberOfPeer`, `peerInfoHeaderActionButtons`, `peerInfoHeaderButtons`, `peerInfoCanEdit`, `peerInfoIsChatMuted`) from `peer: Peer?` to `peer: EnginePeer?`, rewriting internal `as?`/`is` against concrete `TelegramX` subclasses to `case let .x` / `case .x` enum patterns on `EnginePeer`, and updating all 21 call sites to drop wave-42-installed `._asPeer()` / `?._asPeer()` bridges or add `.flatMap(EnginePeer.init)` / `EnginePeer(...)` wraps as appropriate. + +**Architecture:** In-place signature migration following wave-42 precedent — no new typealiases, no engine wrapper structs, no TelegramCore changes. All edits within `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/` (10 files). Single atomic commit. + +**Tech Stack:** Swift, Bazel build, `EnginePeer` enum cases (`.user(TelegramUser)`, `.legacyGroup(TelegramGroup)`, `.channel(TelegramChannel)`, `.secretChat(TelegramSecretChat)`). + +**Preceding waves:** 42 (`PeerInfoScreenData.peer: Peer? → EnginePeer?`) on 2026-04-24. This wave drops ~7 `?._asPeer()` / `._asPeer()` bridges installed then. + +**Expected net wrap change:** ~7 DROPs vs ~12 ADDs → net roughly 0. The headline win is helper-signature migration, not wrap count. Follow-up waves migrating `PeerInfoHeaderEditingContentNode.update`, `PeerInfoEditingAvatarNode.update`, `PeerInfoEditingAvatarOverlayNode.update`, `PeerInfoHeaderNode.update`, `PeerInfoScreenMemberItem.enclosingPeer`, `PeerInfoMembersPane` enclosingPeer param will drop the ADDs introduced here. + +**Build expectation:** 2 iterations likely (per wave-41 lesson — foundational-type migrations rarely first-pass-clean). Hedge for iteration-3 if unexpected property accesses surface. + +--- + +## Pre-flight facts (verified from repo 2026-04-24) + +### EnginePeer cases (from `submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift:177`) + +``` +case user(TelegramUser) +case legacyGroup(TelegramGroup) +case channel(TelegramChannel) +case secretChat(TelegramSecretChat) +``` + +Forwarded property subset on `EnginePeer` includes `id`, `addressName`, `usernames`, `indexName`, `debugDisplayTitle`, `displayLetters`, `profileImageRepresentations`, `smallProfileImage`, `largeProfileImage`, `isDeleted`, `isScam`, `isFake`, `isVerified`, `isPremium`, `isSubscription`, `isService`, `nameColor`, `verificationIconFileId`, `profileColor`, `effectiveProfileColor`, `emojiStatus`, `backgroundEmojiId`, `profileBackgroundEmojiId`, and (via `LocalizedPeerData/Sources/PeerTitle.swift`) `compactDisplayTitle`, `displayTitle(strings:displayOrder:)`. **Not** forwarded: Peer-specific members like `isCopyProtectionEnabled`, `hasPermission(_:)`, `hasBannedPermission(_:)`, `isDeleted` on user (WAIT: yes forwarded). **Internal helper bodies do not access any non-forwarded Peer members** — all their concrete-type work happens via `as? TelegramX`, which is enum-rewrite territory. Confirmed by reading helper bodies (PeerInfoData.swift:2255–2670). + +### Helper call sites inventory (21 sites across 10 files) + +Running command: + +```bash +grep -rn "\bcanEditPeerInfo\b\|\bavailableActionsForMemberOfPeer\b\|\bpeerInfoHeaderActionButtons\b\|\bpeerInfoHeaderButtons\b\|\bpeerInfoCanEdit\b\|\bpeerInfoIsChatMuted\b" submodules/ --include="*.swift" | grep -v PeerInfoData.swift +``` + +All 21 call sites: + +| # | File | Line | Current arg | Peer var origin | Action | +|---|------|------|-------------|-----------------|--------| +| 1 | `PeerInfoHeaderNode.swift` | 548 | `peer: peer` | method param `peer: Peer?` | ADD-WRAP: `peer: peer.flatMap(EnginePeer.init)` | +| 2 | `PeerInfoHeaderNode.swift` | 549 | `peer: peer` | same | ADD-WRAP | +| 3 | `PeerInfoHeaderNode.swift` | 2361 | `peer: peer` | same | ADD-WRAP | +| 4 | `PeerInfoEditingAvatarNode.swift` | 66 | `peer: peer` | method param `peer: Peer?` (unwrapped via `guard let peer = peer`) | ADD-WRAP: `peer: EnginePeer(peer)` (peer is non-optional `Peer` here) | +| 5 | `PeerInfoEditingAvatarOverlayNode.swift` | 85 | `peer: peer` | method param `peer: Peer?` (unwrapped via `guard let peer = peer`) | ADD-WRAP: `peer: EnginePeer(peer)` | +| 6 | `PeerInfoHeaderEditingContentNode.swift` | 59 | `peer: peer` | method param `peer: Peer?` | ADD-WRAP: `peer: peer.flatMap(EnginePeer.init)` | +| 7 | `PeerInfoHeaderEditingContentNode.swift` | 88 | `peer: peer` | same | ADD-WRAP | +| 8 | `PeerInfoHeaderEditingContentNode.swift` | 93 | `peer: peer` | same | ADD-WRAP | +| 9 | `PeerInfoHeaderEditingContentNode.swift` | 159 | `peer: peer` | same | ADD-WRAP | +| 10 | `PeerInfoHeaderEditingContentNode.swift` | 162 | `peer: peer` | same | ADD-WRAP | +| 11 | `PeerInfoScreenAvatarSetup.swift` | 435 | `peer: peer._asPeer()` | `peer = data.peer` (EnginePeer unwrapped) | DROP: `peer: peer` | +| 12 | `PeerInfoScreenPerformButtonAction.swift` | 62 | `peer: self.data?.peer?._asPeer()` | `self.data?.peer` is `EnginePeer?` | DROP: `peer: self.data?.peer` | +| 13 | `PeerInfoScreenPerformButtonAction.swift` | 397 | `peer: peer._asPeer()` | `peer = data.peer` unwrapped at line 381 | DROP: `peer: peer` | +| 14 | `PeerInfoScreenPerformButtonAction.swift` | 398 | `peer: peer._asPeer()` | same | DROP: `peer: peer` | +| 15 | `PeerInfoScreenOpenMember.swift` | 19 | `peer: enclosingPeer._asPeer()` | `enclosingPeer = self.data?.peer` unwrapped at line 14 | DROP: `peer: enclosingPeer` | +| 16 | `PeerInfoScreen.swift` | 1905 | `peer: group` | `group: TelegramGroup` from `if case let .legacyGroup(group) = data.peer` | CONVERT: `peer: data.peer` | +| 17 | `PeerInfoScreen.swift` | 1961 | `peer: channel` | `channel: TelegramChannel` from `if case let .channel(channel) = data.peer` | CONVERT: `peer: data.peer` | +| 18 | `PeerInfoScreen.swift` | 5857 | `peer: self.data?.peer?._asPeer()` | `self.data?.peer` is `EnginePeer?` | DROP: `peer: self.data?.peer` | +| 19 | `PeerInfoProfileItems.swift` | 853 | `peer: peer._asPeer()` | `peer = data.peer` unwrapped at line 821 | DROP: `peer: peer` | +| 20 | `PeerInfoScreenMemberItem.swift` | 178 | `peer: item.enclosingPeer` | `item.enclosingPeer: Peer` (stored raw) | ADD-WRAP: `peer: EnginePeer(item.enclosingPeer)` | +| 21 | `PeerInfoMembersPane.swift` | 139 | `peer: enclosingPeer` | `enclosingPeer: Peer` (local raw) | ADD-WRAP: `peer: EnginePeer(enclosingPeer)` | + +**Summary:** 7 DROPs (sites 11–15, 18, 19), 10 ADD-WRAPs (1–10, 20, 21 = 12 total ADDs), 2 CONVERTs (16, 17 — from concrete-type arg to whole-EnginePeer; no wrap delta but simpler/safer). + +### `is TelegramX` scan on helper bodies + +Only `peerInfoIsChatMuted` has them (PeerInfoData.swift:2641, 2643). Rewrite pattern: `if peer is TelegramUser` → `if case .user = peer`, `else if peer is TelegramGroup` → `else if case .legacyGroup = peer`. + +No `is TelegramX` checks exist at call sites for these specific helpers (wave-42 would have caught them since call-site `data.peer` is already `EnginePeer?`). + +--- + +## File Structure + +All edits within `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/`: + +**Modify (helper definitions):** +- `PeerInfoData.swift:2265–2670` — 6 helper signatures + bodies + +**Modify (call sites):** +- `PeerInfoScreen.swift` (3 sites: 1905, 1961, 5857) +- `PeerInfoHeaderNode.swift` (3 sites: 548, 549, 2361) +- `PeerInfoEditingAvatarNode.swift` (1 site: 66) +- `PeerInfoEditingAvatarOverlayNode.swift` (1 site: 85) +- `PeerInfoHeaderEditingContentNode.swift` (5 sites: 59, 88, 93, 159, 162) +- `PeerInfoScreenAvatarSetup.swift` (1 site: 435) +- `PeerInfoScreenPerformButtonAction.swift` (3 sites: 62, 397, 398) +- `PeerInfoScreenOpenMember.swift` (1 site: 19) +- `PeerInfoProfileItems.swift` (1 site: 853) +- `ListItems/PeerInfoScreenMemberItem.swift` (1 site: 178) +- `Panes/PeerInfoMembersPane.swift` (1 site: 139) + +Total: 10 files modified (PeerInfoData.swift counts once). + +--- + +### Task 1: Migrate the six helper signatures and bodies in `PeerInfoData.swift` + +**Files:** +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift:2265–2670` + +- [ ] **Step 1: Migrate `canEditPeerInfo` (line 2265).** Signature: `peer: Peer?` → `peer: EnginePeer?`. Body rewrites: + +```swift +// before (line 2269-2287) +if let user = peer as? TelegramUser, let botInfo = user.botInfo { + return botInfo.flags.contains(.canEdit) +} else if let channel = peer as? TelegramChannel { + if let threadData = threadData { + if chatLocation.threadId == 1 { + return false + } + if channel.hasPermission(.manageTopics) { + return true + } + if threadData.author == context.account.peerId { + return true + } + } else { + if channel.hasPermission(.changeInfo) { + return true + } + } +} else if let group = peer as? TelegramGroup { + switch group.role { + case .admin, .creator: + return true + case .member: + break + } + if !group.hasBannedPermission(.banChangeInfo) { + return true + } +} + +// after +if case let .user(user) = peer, let botInfo = user.botInfo { + return botInfo.flags.contains(.canEdit) +} else if case let .channel(channel) = peer { + if let threadData = threadData { + if chatLocation.threadId == 1 { + return false + } + if channel.hasPermission(.manageTopics) { + return true + } + if threadData.author == context.account.peerId { + return true + } + } else { + if channel.hasPermission(.changeInfo) { + return true + } + } +} else if case let .legacyGroup(group) = peer { + switch group.role { + case .admin, .creator: + return true + case .member: + break + } + if !group.hasBannedPermission(.banChangeInfo) { + return true + } +} +``` + +The `if context.account.peerId == peer?.id` line (2266) stays identical (`.id` is forwarded by `EnginePeer`). + +- [ ] **Step 2: Migrate `availableActionsForMemberOfPeer` (line 2314).** Signature: `peer: Peer?` → `peer: EnginePeer?`. Body rewrites — four `peer as? TelegramChannel/TelegramGroup` sites become `case let .channel/.legacyGroup` patterns: + +```swift +// Line 2320: if let channel = peer as? TelegramChannel +// → : if case let .channel(channel) = peer + +// Line 2324: } else if let group = peer as? TelegramGroup { +// → : } else if case let .legacyGroup(group) = peer { + +// Line 2330: if let channel = peer as? TelegramChannel +// → : if case let .channel(channel) = peer + +// Line 2374: } else if let group = peer as? TelegramGroup { +// → : } else if case let .legacyGroup(group) = peer { +``` + +The `if peer == nil` check (line 2317) stays identical (Optional == nil works on EnginePeer? too). + +- [ ] **Step 3: Migrate `peerInfoHeaderActionButtons` (line 2434).** Signature: `peer: Peer?` → `peer: EnginePeer?`. Single body rewrite at line 2436: + +```swift +// before +if !isContact && !isSecretChat, let user = peer as? TelegramUser, user.botInfo == nil { + +// after +if !isContact && !isSecretChat, case let .user(user) = peer, user.botInfo == nil { +``` + +- [ ] **Step 4: Migrate `peerInfoHeaderButtons` (line 2447).** Signature: `peer: Peer?` → `peer: EnginePeer?`. Three body rewrites: + +```swift +// Line 2449: if let user = peer as? TelegramUser { +// → : if case let .user(user) = peer { + +// Line 2483: } else if let channel = peer as? TelegramChannel { +// → : } else if case let .channel(channel) = peer { + +// Line 2558: } else if let group = peer as? TelegramGroup { +// → : } else if case let .legacyGroup(group) = peer { +``` + +- [ ] **Step 5: Migrate `peerInfoCanEdit` (line 2585).** Signature: `peer: Peer?` → `peer: EnginePeer?`. Three body rewrites. Note: original shadows `peer` inside each branch (`let peer = peer as? TelegramX`). Rewrite preserves the shadowing via `case let`: + +```swift +// Line 2586: if let user = peer as? TelegramUser { +// → : if case let .user(user) = peer { + +// Line 2597: } else if let peer = peer as? TelegramChannel { +// → : } else if case let .channel(peer) = peer { +// (intentional shadow of outer `peer` with inner `peer: TelegramChannel` — preserved) + +// Line 2618: } else if let peer = peer as? TelegramGroup { +// → : } else if case let .legacyGroup(peer) = peer { +``` + +- [ ] **Step 6: Migrate `peerInfoIsChatMuted` (line 2633).** Outer signature: `peer: Peer?` → `peer: EnginePeer?`. Inner function signature (line 2634) also migrates: `func isPeerMuted(peer: Peer?, ...)` → `func isPeerMuted(peer: EnginePeer?, ...)`. Body rewrites inside the inner function (line 2641–2651): + +```swift +// before (line 2641) +if peer is TelegramUser { + peerIsMuted = !globalNotificationSettings.privateChats.enabled +} else if peer is TelegramGroup { + peerIsMuted = !globalNotificationSettings.groupChats.enabled +} else if let channel = peer as? TelegramChannel { + switch channel.info { + case .group: + peerIsMuted = !globalNotificationSettings.groupChats.enabled + case .broadcast: + peerIsMuted = !globalNotificationSettings.channels.enabled + } +} + +// after +if case .user = peer { + peerIsMuted = !globalNotificationSettings.privateChats.enabled +} else if case .legacyGroup = peer { + peerIsMuted = !globalNotificationSettings.groupChats.enabled +} else if case let .channel(channel) = peer { + switch channel.info { + case .group: + peerIsMuted = !globalNotificationSettings.groupChats.enabled + case .broadcast: + peerIsMuted = !globalNotificationSettings.channels.enabled + } +} +``` + +The outer `if let peer = peer` (line 2640) stays unchanged (Optional binding works on EnginePeer?). + +The inner `peerInfoIsChatMuted` body (line 2659–2669) calls `isPeerMuted(peer: peer, ...)` with the outer `peer` (now EnginePeer?) — works without change because inner signature now matches. + +- [ ] **Step 7: Re-read PeerInfoData.swift lines 2265–2670 and visually verify no `as? TelegramX` or `is TelegramX` patterns remain.** + +Run: `grep -n "as? TelegramUser\|as? TelegramChannel\|as? TelegramGroup\|is TelegramUser\|is TelegramChannel\|is TelegramGroup" submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift | awk -F: '$2 >= 2265 && $2 <= 2670'` +Expected: empty output. + +--- + +### Task 2: Update call sites — DROPs (7 sites) + +**Files:** +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift:435` +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift:62,397,398` +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMember.swift:19` +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift:5857` +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift:853` + +- [ ] **Step 1: DROP at `PeerInfoScreenAvatarSetup.swift:435`.** + +```swift +// before +guard let data = self.controllerNode.data, let peer = data.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer._asPeer(), chatLocation: self.chatLocation, threadData: data.threadData) else { + +// after +guard let data = self.controllerNode.data, let peer = data.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: data.threadData) else { +``` + +- [ ] **Step 2: DROP at `PeerInfoScreenPerformButtonAction.swift:62`.** + +```swift +// before +let chatIsMuted = peerInfoIsChatMuted(peer: self.data?.peer?._asPeer(), peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings) + +// after +let chatIsMuted = peerInfoIsChatMuted(peer: self.data?.peer, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings) +``` + +- [ ] **Step 3: DROP at `PeerInfoScreenPerformButtonAction.swift:397 and :398` (peerInfoHeaderButtons, two lines, same pattern `peer: peer._asPeer()` → `peer: peer`).** + +Use Edit with `replace_all=true` on the substring `peer: peer._asPeer(), cachedData: data.cachedData` — this exact form appears exactly twice in the file (lines 397, 398), both targets. + +```swift +// before (at both 397 and 398) +peerInfoHeaderButtons(peer: peer._asPeer(), cachedData: data.cachedData, isOpenedFromChat: ... + +// after +peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: ... +``` + +Verification after edit: + +```bash +grep -n "_asPeer()" submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift +``` +Expected: empty (all three DROPs done). + +- [ ] **Step 4: DROP at `PeerInfoScreenOpenMember.swift:19`.** + +```swift +// before +let actions = availableActionsForMemberOfPeer(accountPeerId: self.context.account.peerId, peer: enclosingPeer._asPeer(), member: member) + +// after +let actions = availableActionsForMemberOfPeer(accountPeerId: self.context.account.peerId, peer: enclosingPeer, member: member) +``` + +- [ ] **Step 5: DROP at `PeerInfoScreen.swift:5857`.** + +```swift +// before +} else if peerInfoCanEdit(peer: self.data?.peer?._asPeer(), chatLocation: self.chatLocation, threadData: self.data?.threadData, cachedData: self.data?.cachedData, isContact: self.data?.isContact) { + +// after +} else if peerInfoCanEdit(peer: self.data?.peer, chatLocation: self.chatLocation, threadData: self.data?.threadData, cachedData: self.data?.cachedData, isContact: self.data?.isContact) { +``` + +- [ ] **Step 6: DROP at `PeerInfoProfileItems.swift:853`.** + +Only the `availableActionsForMemberOfPeer` call — the sibling `enclosingPeer: peer._asPeer()` at line 852 is NOT a helper-migration target (it's `PeerInfoScreenMemberItem.enclosingPeer: Peer`, unchanged in this wave). + +```swift +// before (line 853) +let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: peer._asPeer(), member: member) + +// after +let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: peer, member: member) +``` + +--- + +### Task 3: Update call sites — CONVERTs (2 sites in PeerInfoScreen.swift) + +**Files:** +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift:1905,1961` + +At these sites the helper arg is currently a concrete `TelegramGroup` / `TelegramChannel` extracted via case pattern. After migration the helper takes `EnginePeer?`, so pass `data.peer` directly — the helper re-does the pattern match internally, semantics preserved. + +- [ ] **Step 1: CONVERT at `PeerInfoScreen.swift:1905`.** + +```swift +// before +} else if case let .legacyGroup(group) = data.peer, canEditPeerInfo(context: strongSelf.context, peer: group, chatLocation: chatLocation, threadData: data.threadData) { + +// after +} else if case let .legacyGroup(group) = data.peer, canEditPeerInfo(context: strongSelf.context, peer: data.peer, chatLocation: chatLocation, threadData: data.threadData) { +``` + +`group` stays bound because the body below still uses it. Only the helper arg changes. + +- [ ] **Step 2: CONVERT at `PeerInfoScreen.swift:1961`.** + +```swift +// before +} else if case let .channel(channel) = data.peer, canEditPeerInfo(context: strongSelf.context, peer: channel, chatLocation: strongSelf.chatLocation, threadData: data.threadData) { + +// after +} else if case let .channel(channel) = data.peer, canEditPeerInfo(context: strongSelf.context, peer: data.peer, chatLocation: strongSelf.chatLocation, threadData: data.threadData) { +``` + +--- + +### Task 4: Update call sites — ADD-WRAPs in internal-update methods (10 sites in 4 files) + +These files' internal `.update(peer: Peer?, ...)` methods are NOT migrated in this wave (scope: helpers only). Each helper call inside bridges `peer` (raw `Peer?`) to `EnginePeer?` via `.flatMap(EnginePeer.init)`, or — where `peer` has already been unwrapped to non-optional `Peer` — via `EnginePeer(peer)`. + +**Files:** +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift:548,549,2361` +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift:66` +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift:85` +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift:59,88,93,159,162` + +- [ ] **Step 1: ADD-WRAPs at `PeerInfoHeaderNode.swift:548,549,2361`.** At lines 548, 549, the local `peer` is the raw `Peer?` method parameter (line 496). At line 2361 likewise. + +```swift +// before (line 548) +let actionButtonKeys: [PeerInfoHeaderButtonKey] = (self.isSettings || self.isMyProfile) ? [] : peerInfoHeaderActionButtons(peer: peer, isSecretChat: isSecretChat, isContact: isContact) + +// after +let actionButtonKeys: [PeerInfoHeaderButtonKey] = (self.isSettings || self.isMyProfile) ? [] : peerInfoHeaderActionButtons(peer: peer.flatMap(EnginePeer.init), isSecretChat: isSecretChat, isContact: isContact) +``` + +```swift +// before (line 549) +let buttonKeys: [PeerInfoHeaderButtonKey] = (self.isSettings || self.isMyProfile) ? [] : peerInfoHeaderButtons(peer: peer, cachedData: cachedData, ...) + +// after +let buttonKeys: [PeerInfoHeaderButtonKey] = (self.isSettings || self.isMyProfile) ? [] : peerInfoHeaderButtons(peer: peer.flatMap(EnginePeer.init), cachedData: cachedData, ...) +``` + +```swift +// before (line 2361) +let chatIsMuted = peerInfoIsChatMuted(peer: peer, peerNotificationSettings: peerNotificationSettings, threadNotificationSettings: threadNotificationSettings, globalNotificationSettings: globalNotificationSettings) + +// after +let chatIsMuted = peerInfoIsChatMuted(peer: peer.flatMap(EnginePeer.init), peerNotificationSettings: peerNotificationSettings, threadNotificationSettings: threadNotificationSettings, globalNotificationSettings: globalNotificationSettings) +``` + +- [ ] **Step 2: ADD-WRAP at `PeerInfoEditingAvatarNode.swift:66`.** Here `peer` is non-optional `Peer` (unwrapped at line 62: `guard let peer = peer else { return }`). Use `EnginePeer(peer)`. + +```swift +// before +let canEdit = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) + +// after +let canEdit = canEditPeerInfo(context: self.context, peer: EnginePeer(peer), chatLocation: chatLocation, threadData: threadData) +``` + +- [ ] **Step 3: ADD-WRAP at `PeerInfoEditingAvatarOverlayNode.swift:85`.** Same shape — `peer` is non-optional `Peer` (unwrapped at line 64). + +```swift +// before +if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) + +// after +if canEditPeerInfo(context: self.context, peer: EnginePeer(peer), chatLocation: chatLocation, threadData: threadData) +``` + +- [ ] **Step 4: ADD-WRAPs at `PeerInfoHeaderEditingContentNode.swift:59,88,93,159,162`.** Here `peer` is the method's `peer: Peer?` parameter (line 52). Five identical bridge forms. + +For each of lines 59, 88, 93, 159, 162, replace `peer: peer` (inside `canEditPeerInfo(... peer: peer, ...)`) with `peer: peer.flatMap(EnginePeer.init)`. + +The simplest approach: issue five separate Edit calls, each scoped to a unique surrounding substring. Example: + +```swift +// before (line 59) +if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) { + +// after +if canEditPeerInfo(context: self.context, peer: peer.flatMap(EnginePeer.init), chatLocation: chatLocation, threadData: threadData) { +``` + +Note line 59's trailing double-space before `{` in the original — preserve it. + +Lines 88, 93, 159 share an identical surrounding substring `if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) {` (no trailing double-space, no `|| isEditableBot`). To avoid collision with line 59, use `replace_all=true` on THIS exact string (matches 88, 93 — wait, 159 uses `isEnabled = canEditPeerInfo(...)`, different prefix). Safer plan: one Edit per line, each with enough surrounding context to be unique. Verify uniqueness after each edit with grep. + +Line 88's surrounding context: inside `if let _ = peer as? TelegramGroup {` branch — preceded by `fieldKeys.append(.title)`. + +Line 93's surrounding context: inside `if let _ = peer as? TelegramChannel {` branch — preceded by `fieldKeys.append(.title)`. Same inner phrase as 88 — so `fieldKeys.append(.title)\n if canEditPeerInfo...` appears twice. Use line-specific context (preceding `else if let _ = peer as?` token). + +Line 159: `isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData)` (no trailing text). + +Line 162: `isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) || isEditableBot`. Unique — contains ` || isEditableBot`. + +Recommended: five sequential Edits with explicit line disambiguation via surrounding context. Do not bulk-replace-all — the identical `peer: peer, chatLocation: chatLocation, threadData: threadData)` substring appears at all five sites but their line-specific surroundings differ. + +Verification after all five edits: + +```bash +grep -c "peer: peer," submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift +``` +Expected: 0 (no unmigrated call sites remain; other `peer:` occurrences in the file are either type annotations or at the method signature, which uses `peer: Peer?` not `peer: peer`). + +--- + +### Task 5: Update call sites — ADD-WRAPs at raw-`Peer` member-item sites (2 sites) + +**Files:** +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift:178` +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift:139` + +At these sites `enclosingPeer` is non-optional `Peer` (raw, stored on the item / local). Wrap with `EnginePeer(...)`. + +- [ ] **Step 1: ADD-WRAP at `PeerInfoScreenMemberItem.swift:178`.** + +```swift +// before +let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: item.enclosingPeer, member: item.member) + +// after +let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: EnginePeer(item.enclosingPeer), member: item.member) +``` + +- [ ] **Step 2: ADD-WRAP at `PeerInfoMembersPane.swift:139`.** + +```swift +// before +let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: enclosingPeer, member: member) + +// after +let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: EnginePeer(enclosingPeer), member: member) +``` + +--- + +### Task 6: Build and iterate + +- [ ] **Step 1: Full project build with `--continueOnError` to surface all errors at once.** + +```bash +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 \ + --configuration=debug_sim_arm64 --continueOnError +``` + +Expected: likely 2-iteration convergence. Budget up to iteration 3. + +Likely categories of residual errors: + +1. **Missed call sites** — grep-miss from planning. Remediate by adding `.flatMap(EnginePeer.init)` or `EnginePeer(...)` as appropriate. +2. **Missed `as? TelegramX` / `is TelegramX` inside helper bodies** — Swift compiler error "cannot convert value of type 'EnginePeer?' to expected argument type 'Peer?'" or warning "'is' test is always false". Fix with `case` pattern. +3. **Optional-lifting edge cases** — `if case let .user(user) = peer` may fail if Swift interprets `peer` as non-optional. If so, rewrite as `if let peer, case let .user(user) = peer`. +4. **Unused binding warnings** — e.g. `if case let .user(user) = peer` where `user` isn't used inside that branch. Swift's `-warnings-as-errors` (658/665 submodule BUILDs) promotes these. Rewrite as `if case .user = peer`. +5. **Unused variable `peer` or `group`/`channel` at CONVERT sites 16, 17** — lines 1905/1961 bind `group`/`channel` in the `case let` pattern; if the body body doesn't use it, Swift emits "value 'group' was never used" which `-warnings-as-errors` promotes to error. Since the body below DOES use them (updatePeerTitle(peerId: group.id, ...)` etc.), this should not trigger — but verify. + +- [ ] **Step 2: For each error category above, apply the correct fix in-place and rebuild. Iterate until green.** + +- [ ] **Step 3: After build is green, run the post-migration grep audit:** + +```bash +# Should be empty — no _asPeer() bridges at helper call sites +grep -rn "canEditPeerInfo(.*_asPeer\|peerInfoIsChatMuted(.*_asPeer\|peerInfoHeaderButtons(.*_asPeer\|peerInfoHeaderActionButtons(.*_asPeer\|peerInfoCanEdit(.*_asPeer\|availableActionsForMemberOfPeer(.*_asPeer" submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ + +# Should be empty — no concrete-type casts against peer param in helper bodies +grep -nE "as\?\s+TelegramUser|as\?\s+TelegramChannel|as\?\s+TelegramGroup|\bis\s+TelegramUser\b|\bis\s+TelegramChannel\b|\bis\s+TelegramGroup\b" submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift | awk -F: '$2 >= 2265 && $2 <= 2670' +``` + +Expected: both empty. + +--- + +### Task 7: Commit + +- [ ] **Step 1: Verify working tree only contains wave-43 edits + pre-existing WIP.** + +```bash +git status --short +``` + +Expected (pre-existing WIP, NOT to be staged): + +``` + m build-system/bazel-rules/sourcekit-bazel-bsp + M submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +?? build-system/tulsi/ +?? submodules/TgVoip/ +?? third-party/libx264/ +``` + +Plus wave-43 edits (all under `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/`): + +``` + M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift + M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift + M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift + M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift + M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift + M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift + M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift + M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift + M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMember.swift + M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift + M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift + M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift +``` + +- [ ] **Step 2: Explicitly stage only the wave-43 files (not the WIP).** + +```bash +git add \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMember.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift \ + docs/superpowers/plans/2026-04-24-peerinfoscreen-helpers-engine-peer-migration.md +``` + +- [ ] **Step 3: Commit.** + +Use a HEREDOC for the message: + +```bash +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 43 + +Migrate six PeerInfoScreen helpers (canEditPeerInfo, +availableActionsForMemberOfPeer, peerInfoHeaderActionButtons, +peerInfoHeaderButtons, peerInfoCanEdit, peerInfoIsChatMuted) from +`peer: Peer?` to `peer: EnginePeer?`. Internal `as? TelegramX` / +`is TelegramX` patterns rewritten to `case let .x` / `case .x` on +EnginePeer enum. All 21 call sites updated in the same commit: 7 +`._asPeer()` bridges installed by wave 42 dropped; 12 +`.flatMap(EnginePeer.init)` / `EnginePeer(...)` wraps added at sites +whose enclosing methods still take raw Peer?; 2 concrete-type args +converted to pass the whole EnginePeer value. + +All edits within submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/. +No new engine typealiases. No TelegramCore changes. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: Verify commit.** + +```bash +git log --oneline -1 +git show --stat HEAD +``` + +Expected: one commit, ~10 files changed, clean diff. + +--- + +## Self-review checklist (run before handoff) + +**Spec coverage:** +- All 6 helper signatures migrated (Task 1 steps 1–6). ✓ +- All 21 call sites touched (Tasks 2–5). ✓ +- Build iteration explicit (Task 6). ✓ +- Commit explicit (Task 7). ✓ + +**Type consistency:** +- Helper signatures all `peer: EnginePeer?` (consistent). ✓ +- Call-site transforms: DROP/ADD/CONVERT actions match the inventory table. ✓ +- `EnginePeer.init` constructor used both as `.flatMap(EnginePeer.init)` (Peer? → EnginePeer?) and `EnginePeer(...)` (Peer → EnginePeer) — both are valid (construction overloaded on EnginePeer extension at `TelegramCore/TelegramEngine/Peers/Peer.swift:564`). ✓ + +**Placeholder scan:** +- No "TBD" / "handle appropriately" / "similar to Task N" language — every step has its concrete code. ✓ + +**Risks flagged:** +- Wave-41 lesson: foundational-type migrations rarely first-pass-clean. Budget 2 iterations. ✓ +- Wave-41 lesson: `-warnings-as-errors` promotes always-false `is` checks and unused bindings to build errors. Task 6 step 1 calls these out explicitly. ✓ +- Wave-42 lesson: `EnginePeer` doesn't forward every Peer property. Helper bodies were verified to access only `.id`, which IS forwarded; other property accesses were on concrete types (`TelegramChannel.hasPermission(...)` etc.) which remain on concrete types post-migration. No forwarding-gap remediation expected in helpers. ✓ diff --git a/docs/superpowers/plans/2026-04-24-peerinfoscreendata-peer-engine-peer-migration.md b/docs/superpowers/plans/2026-04-24-peerinfoscreendata-peer-engine-peer-migration.md new file mode 100644 index 0000000000..a3fc76f355 --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-peerinfoscreendata-peer-engine-peer-migration.md @@ -0,0 +1,164 @@ +# Wave 42 plan: `PeerInfoScreenData.peer: Peer? → EnginePeer?` + +Date: 2026-04-24 +Preceding waves: 41 (`RenderedChannelParticipant.peer`), 40 (`makeChatQrCodeScreen`/`makeChatRecentActionsController`), 39 (`makePeerInfoController`) +Scope (confirmed with user): only `PeerInfoScreenData.peer`. Sibling fields (`chatPeer`, `savedMessagesPeer`, `linkedDiscussionPeer`, `linkedMonoforumPeer`) are follow-up-wave candidates. + +## Change target + +File: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift` + +- L386: `let peer: Peer?` → `let peer: EnginePeer?` +- L442: `peer: Peer?,` → `peer: EnginePeer?,` +- Store unchanged (`self.peer = peer`) + +## Construction sites (5, all in PeerInfoData.swift) + +| Line | Current `peer:` arg | Rewrite | +|------|---------------------|---------| +| 1027 | `peer: peer` (local, `Peer?` from `peerView.peers[peerId]`) | `peer: peer.flatMap(EnginePeer.init)` | +| 1100 | `peer: nil` | unchanged | +| 1620 | `peer: peer` (local, `Peer?` from `peerView.peers[userPeerId]`) | `peer: peer.flatMap(EnginePeer.init)` | +| 1867 | `peer: peerView.peers[peerId]` | `peer: peerView.peers[peerId].flatMap(EnginePeer.init)` | +| 2205 | `peer: peerView.peers[groupId]` | `peer: peerView.peers[groupId].flatMap(EnginePeer.init)` | + +## Consumer migration patterns (across 18 files, ~114 `data.peer` accesses) + +### Pattern A — as-cast → enum pattern match (~20 sites) + +```swift +// before +if let user = data.peer as? TelegramUser, user.botInfo == nil { ... } + +// after +if case let .user(user) = data.peer, user.botInfo == nil { ... } +``` + +Scope both sides consistently. A cast inside a larger `guard let ..., let user = ... as? TelegramUser else { return }` becomes `guard ..., case let .user(user) = data.peer else { return }`. + +### Pattern B — `is TelegramXxx` check → enum case pattern (~5 sites) + +The wave-41 lesson: `-warnings-as-errors` catches always-false `is` checks. + +```swift +// before +if let peer = self.data?.peer, peer is TelegramChannel { ... } +if peer is TelegramGroup { ... } + +// after +if case .channel = self.data?.peer { ... } +if case .legacyGroup = peer { ... } +``` + +`TelegramGroup` maps to `.legacyGroup`. `TelegramChannel` maps to `.channel`. `TelegramUser` maps to `.user`. `TelegramSecretChat` maps to `.secretChat`. + +Known sites in PeerInfoScreen.swift (inventory): L3981, L4133, L4192, L4194 (and L7421 for `chatPeer`-bound — chatPeer stays raw, so L7421 is out of scope). Use repo grep on `PeerInfoScreen/Sources` with token `is Telegram(Channel|User|Group|SecretChat)` to catch other sites. + +### Pattern C — existing `EnginePeer(peer)` wraps where `peer` was bound from `data.peer` — DROP (15+ sites) + +```swift +// before +if let peer = self.data?.peer { + self.joinChannel(peer: EnginePeer(peer)) // wave-40 wrap +} + +// after +if let peer = self.data?.peer { + self.joinChannel(peer: peer) // peer is now EnginePeer already +} +``` + +Care needed: only drop the wrap where the bound `peer` variable comes from `data.peer`. Wraps on `chatPeer`, `currentPeer`, `user` (bound via `as? TelegramUser`), `groupPeer`, or PeerView lookups stay. The lexical scope makes this judgeable. + +Known drop sites (PeerInfoScreen.swift): 1331, 1339, 1346, 1561, 2353, 2405, 3409, 3459, 3624, 3747, 4306, 4573 (inner — review scope), 4623. PeerInfoHeaderNode.swift: 571, 1218, 2054 (if bound from data.peer). PeerInfoScreenOpenChat.swift: 25, 40, 51, 57, 80, 89, 115. Verify each by backtracking the `if let peer = ...` binding. + +### Pattern D — helper call sites still taking `Peer?` (ADD-WRAP, ~10 sites) + +`canEditPeerInfo`, `peerInfoIsChatMuted`, `peerInfoHeaderButtons`, `peerInfoHeaderActionButtons`, `peerInfoCanEdit`, `availableActionsForMemberOfPeer` all keep `peer: Peer?` in this wave. Call sites must bridge: + +```swift +// before +peerInfoIsChatMuted(peer: self.data?.peer, ...) + +// after +peerInfoIsChatMuted(peer: self.data?.peer?._asPeer(), ...) +``` + +Site count (from grep): PeerInfoHeaderNode.swift:548/549/2361, PeerInfoScreenAvatarSetup.swift:435, PeerInfoScreenPerformButtonAction.swift:62/397/398, PeerInfoEditingAvatarNode.swift:66, PeerInfoScreen.swift:1905/1961/5857, PeerInfoHeaderEditingContentNode.swift:59/88/93/159/162, PeerInfoEditingAvatarOverlayNode.swift:85. But the local `peer` at some of these is already narrowed via `as? TelegramUser` (now `case let .user(user)`); in that case the helper gets `user` (still `Peer`-conforming), no bridge needed. Bridge only where the raw `data.peer` flows into the helper. + +These ADD-WRAP markers become ratchet-drops for a follow-up wave that migrates the helper signatures. + +### Pattern E — `EnginePeer?` passed as `EnginePeer?` directly (DROP wraps on callback args) + +Where `data.peer` feeds `makePeerInfoController(peer: EnginePeer)` / `chatInterfaceInteraction.openPeer(_ peer: EnginePeer, ...)` / `.peer(EnginePeer)` ChatLocation / `AvatarGalleryController(peer: EnginePeer)` / `makeChatQrCodeScreen(peer: EnginePeer)` / `makeChatRecentActionsController(peer: EnginePeer)` — drop the `EnginePeer(...)` wrap; pass directly. + +### Pattern F — `EnginePeer(peer).displayTitle(...)` / `.compactDisplayTitle` usage (DROP wrap) + +```swift +// before +EnginePeer(peer).displayTitle(strings: ..., displayOrder: ...) + +// after (peer is now EnginePeer already) +peer.displayTitle(strings: ..., displayOrder: ...) +``` + +### Pattern G — `.isPremium` on `peer?` inside construction site (L1060, L1626, L1902, L2242) + +`peerView.peers[peerId]?.isPremium` — `Peer` protocol exposes `isPremium`. But the construction site receives raw `Peer?` and then we wrap via `flatMap(EnginePeer.init)`. The `peer?.isPremium` in the same construction scope still refers to the *local* raw peer variable (type unchanged), not `self.peer`. **No change needed at construction sites for `.isPremium` accesses on the local raw `peer`.** Only change `.isPremium` accesses on `data.peer` (which is now `EnginePeer?`) — `EnginePeer.isPremium` exists. + +## File-by-file plan + +1. **PeerInfoData.swift** — declaration + init + 5 constructions. Also review L1529 (`peerView.peers[peerView.peerId] is TelegramUser`) — OUT OF SCOPE (not `data.peer`); don't touch. Helper functions L2265/2314/2434/2447/2585/2633 stay `peer: Peer?` — DO NOT TOUCH. + +2. **PeerInfoScreen.swift** — largest consumer, ~70+ sites. Walk every `data.peer` / `data?.peer` / `self.data?.peer` / `self.data.peer`. Apply A/B/C/E/F patterns. For `if let peer = data.peer` bindings, subsequent uses of `peer` now have type `EnginePeer` — drop wraps on those uses. + +3. **PeerInfoScreenOpenChat.swift, PeerInfoScreenOpenBio.swift, PeerInfoScreenOpenMember.swift, PeerInfoScreenOpenPeerInfoContextMenu.swift, PeerInfoScreenOpenUsername.swift, PeerInfoScreenCallActions.swift, PeerInfoScreenMessageActions.swift, PeerInfoScreenPerformButtonAction.swift, PeerInfoScreenAvatarSetup.swift, PeerInfoScreenSettingsActions.swift, PeerInfoScreenDisplayGiftsContextMenu.swift, PeerInfoScreenDisplayMediaGalleryContextMenu.swift** — various `data?.peer as? TelegramXxx` (A), helper bridges (D), wrap drops (C/E). + +4. **PeerInfoPaneContainerNode.swift** — L1252 `as? TelegramChannel` (A). + +5. **PeerInfoProfileItems.swift, PeerInfoSettingsItems.swift, ListItems/PeerInfoScreenPersonalChannelItem.swift** — `data.peer as? TelegramUser` style consumers (A). + +6. **PeerInfoHeaderNode.swift, PeerInfoEditingAvatarNode.swift, PeerInfoEditingAvatarOverlayNode.swift, PeerInfoHeaderEditingContentNode.swift** — these files receive `peer` as a parameter (not directly `data.peer`). Only touch if a parameter type declared as `Peer?` is the field from `data.peer` being passed in; otherwise leave. + +## Replace_all guidance (wave-41 lesson) + +Several wraps repeat identically. Where a file has multiple identical `EnginePeer(peer)` expressions in scopes where `peer` is now `EnginePeer`, use `replace_all=true` on the unique full expression. BUT verify each such file has no same-pattern wrap where `peer` is still raw (chatPeer-bound, currentPeer-bound, etc.) — such wraps must survive. + +Safer alternative: edit each site individually. + +## Out of scope (enumerated) + +- `PeerInfoScreenData.chatPeer`, `.savedMessagesPeer`, `.linkedDiscussionPeer`, `.linkedMonoforumPeer` — stay `Peer?`. +- Internal helpers `canEditPeerInfo` / `peerInfoIsChatMuted` / etc. — stay `peer: Peer?`. +- `peerView.peers[...]` access inside PeerInfoData.swift — stays raw `Peer?`. +- Any `is TelegramXxx` check on a non-`data.peer`-derived variable. + +## Build methodology + +1. Apply declaration + init + construction edits. +2. Apply consumer edits file by file. +3. `source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError` +4. Iterate on errors. Budget: 2–4 iterations (wave-41 lesson: foundational-type property-access migrations are not first-pass-clean). + +## Expected ratchet math + +- Drops: 15+ wave-40 wraps, ~20 as-cast patterns collapsed, ~5 is-checks rewritten, several `EnginePeer(peer).displayTitle` wraps dropped. +- Adds: ~10 `?._asPeer()` helper bridges, 4 `flatMap(EnginePeer.init)` at construction. +- Net: ~15–25 bridges dropped. + +## WIP interference check + +Pre-existing WIP in tree (per memory): +- `submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift` — modified, unrelated animation WIP. DO NOT TOUCH. +- `submodules/TgVoip/`, `third-party/libx264/`, `build-system/tulsi/` — untracked, unrelated. +- `build-system/bazel-rules/sourcekit-bazel-bsp` — submodule marker, unrelated. + +Wave-42 files are all in `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/` — no overlap with WIP. Commit with explicit file list (wave-39/41 lesson). + +## Post-commit followups + +- Update `docs/superpowers/postbox-refactor-log.md` with "Wave 42 outcome". +- Update `memory/project_postbox_refactor_next_wave.md` with wave 43 candidates: + - Wave 42.x sibling: `PeerInfoScreenData.chatPeer` / `.savedMessagesPeer` / `.linkedDiscussionPeer` / `.linkedMonoforumPeer` as a bundle (same file, narrow blast radius). + - Wave 42.y: PeerInfo-internal helper signatures (drops the ~10 ADD-WRAP markers). + - Option 2 from wave-42 shortlist: `RenderedChannelParticipant.peers` dict. diff --git a/docs/superpowers/plans/2026-04-24-peertokentitle-engine-peer-migration.md b/docs/superpowers/plans/2026-04-24-peertokentitle-engine-peer-migration.md new file mode 100644 index 0000000000..d8386bf99d --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-peertokentitle-engine-peer-migration.md @@ -0,0 +1,395 @@ +# Postbox → TelegramEngine wave 37: `peerTokenTitle` peer parameter Peer → EnginePeer + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate the private free function `peerTokenTitle(accountPeerId: PeerId, peer: Peer, strings:, nameDisplayOrder:)` in `submodules/TelegramUI/Sources/ContactMultiselectionController.swift` so `peer` is `EnginePeer`, dropping 5 `._asPeer()` bridges at call sites in the same file. + +**Architecture:** Single-file, atomic, private-function refactor. No public API change, no BUILD-file touch, no cross-module effects. Function body simplifies `EnginePeer(peer).displayTitle(...)` → `peer.displayTitle(...)`. + +**Tech Stack:** Swift, Bazel via `Make.py` wrapper, Telegram-iOS project conventions (see CLAUDE.md). + +**Reference:** Spec `docs/superpowers/specs/2026-04-24-peertokentitle-engine-peer-migration-design.md`. + +--- + +## File Structure + +Only one file is touched: + +- **Modify:** `submodules/TelegramUI/Sources/ContactMultiselectionController.swift` + - L21 — signature change (`peer: Peer` → `peer: EnginePeer`) + - L27 — body simplification (drop redundant `EnginePeer(...)` wrap) + - L171, L201, L386, L403, L748 — call-site bridge drops (`peer: peer._asPeer()` → `peer: peer`) + +No files created. No files deleted. No BUILD files touched. + +--- + +## Task 1: Pre-flight inventory verification + +**Files:** None (grep-only). + +- [ ] **Step 1: Confirm the function is private and single-file** + +Run: + +```bash +grep -rn "peerTokenTitle" submodules/ Telegram/ third-party/ --include="*.swift" +``` + +Expected: exactly 6 matches, all in `submodules/TelegramUI/Sources/ContactMultiselectionController.swift` — 1 definition at L21 and 5 call sites at L171, L201, L386, L403, L748. + +If any match appears outside this file, **stop and re-evaluate scope**: the function may not actually be private or another file has copy-pasted the name. + +- [ ] **Step 2: Confirm all 5 call sites currently use `._asPeer()`** + +Run: + +```bash +grep -n "peerTokenTitle(.*_asPeer())" submodules/TelegramUI/Sources/ContactMultiselectionController.swift +``` + +Expected: 5 matches, line numbers 171, 201, 386, 403, 748. + +If the count is not 5, **stop and re-inventory** — a prior change may have shifted line numbers or altered a call site. + +- [ ] **Step 3: Confirm no other `peerTokenTitle` overload exists** + +Run: + +```bash +grep -n "func peerTokenTitle" submodules/TelegramUI/Sources/ContactMultiselectionController.swift +``` + +Expected: exactly 1 match at line 21 (`private func peerTokenTitle(...)`). + +- [ ] **Step 4: Confirm `EnginePeer.displayTitle(strings:displayOrder:)` exists** + +Run: + +```bash +grep -rn "func displayTitle(strings:" submodules/TelegramCore/Sources/TelegramEngine/ submodules/TelegramCore/Sources/SyncCore/ +``` + +Expected: a match on `EnginePeer` extension exposing `displayTitle(strings: PresentationStrings, displayOrder: PresentationPersonNameOrder)`. (This is the method already called as `EnginePeer(peer).displayTitle(...)` at L27, so its existence is certain — this step just makes the dependency explicit.) + +--- + +## Task 2: Edit the function signature and body + +**Files:** +- Modify: `submodules/TelegramUI/Sources/ContactMultiselectionController.swift:21-29` + +- [ ] **Step 1: Read the current function definition** + +Read the file, lines 21–29. Current state: + +```swift +private func peerTokenTitle(accountPeerId: PeerId, peer: Peer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) -> String { + if peer.id == accountPeerId { + return strings.DialogList_SavedMessages + } else if peer.id.isReplies { + return strings.DialogList_Replies + } else { + return EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder) + } +} +``` + +- [ ] **Step 2: Apply the signature change** + +Use Edit with: + +- `old_string`: + ``` + private func peerTokenTitle(accountPeerId: PeerId, peer: Peer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) -> String { + if peer.id == accountPeerId { + return strings.DialogList_SavedMessages + } else if peer.id.isReplies { + return strings.DialogList_Replies + } else { + return EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder) + } + } + ``` +- `new_string`: + ``` + private func peerTokenTitle(accountPeerId: PeerId, peer: EnginePeer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) -> String { + if peer.id == accountPeerId { + return strings.DialogList_SavedMessages + } else if peer.id.isReplies { + return strings.DialogList_Replies + } else { + return peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder) + } + } + ``` + +Note: `accountPeerId: PeerId` stays as-is — `PeerId` is already the typealias for `EnginePeer.Id`. `peer.id.isReplies` works unchanged because `EnginePeer.Id` exposes `isReplies`. + +--- + +## Task 3: Drop `._asPeer()` bridges at all 5 call sites + +**Files:** +- Modify: `submodules/TelegramUI/Sources/ContactMultiselectionController.swift` (L171, L201, L386, L403, L748) + +All 5 call sites have an identical argument fragment: + +``` +peer: peer._asPeer(), +``` + +…which must become: + +``` +peer: peer, +``` + +The surrounding context differs per site (two distinct `strings/nameDisplayOrder` chains, see below), so we handle the substitution in two batches. + +- [ ] **Step 1: Replace sites L171, L201, L748 (use `strongSelf.presentationData.strings` / `strongSelf.presentationData.nameDisplayOrder` or `self.presentationData.strings` / `self.presentationData.nameDisplayOrder`)** + +Three call sites share identical code but with different leading `accountPeerId` expressions. Apply them individually. + +**L171 and L201 are identical** — both read: + +```swift +return EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: params.context.account.peerId, peer: peer._asPeer(), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)) +``` + +Use Edit with `replace_all=true`: + +- `old_string`: + ``` + return EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: params.context.account.peerId, peer: peer._asPeer(), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)) + ``` +- `new_string`: + ``` + return EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: params.context.account.peerId, peer: peer, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)) + ``` + +**L748** reads: + +```swift +tokens.append(EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: self.context.account.peerId, peer: peer._asPeer(), strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer))) +``` + +Use Edit (no `replace_all` — this line is unique): + +- `old_string`: + ``` + tokens.append(EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: self.context.account.peerId, peer: peer._asPeer(), strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer))) + ``` +- `new_string`: + ``` + tokens.append(EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: self.context.account.peerId, peer: peer, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer))) + ``` + +- [ ] **Step 2: Replace sites L386 and L403 (use `accountPeerId` local)** + +**L386 and L403 are identical** — both read: + +```swift +addedToken = EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: accountPeerId, peer: peer._asPeer(), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)) +``` + +Use Edit with `replace_all=true`: + +- `old_string`: + ``` + addedToken = EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: accountPeerId, peer: peer._asPeer(), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)) + ``` +- `new_string`: + ``` + addedToken = EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: accountPeerId, peer: peer, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)) + ``` + +- [ ] **Step 3: Grep to confirm zero remaining bridge sites** + +Run: + +```bash +grep -n "peerTokenTitle(.*_asPeer())" submodules/TelegramUI/Sources/ContactMultiselectionController.swift +``` + +Expected: **0 matches**. + +If any match remains, the previous edits missed a line variant — re-read the file around each missed line and apply a targeted Edit for that variant. + +- [ ] **Step 4: Confirm the 5 expected `peer: peer,` call sites now appear** + +Run: + +```bash +grep -n "peerTokenTitle(.*peer: peer," submodules/TelegramUI/Sources/ContactMultiselectionController.swift +``` + +Expected: 5 matches, line numbers approximately 171, 201, 386, 403, 748 (exact numbers unchanged — the edits don't shift line counts). + +--- + +## Task 4: Build verification + +**Files:** None edited in this task. + +- [ ] **Step 1: Run the full project build with --continueOnError** + +Run: + +```bash +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 \ + --continueOnError +``` + +Expected: build succeeds with exit code 0 and no compilation errors. + +**If the build fails:** + +1. Inspect the error output. Three failure modes are anticipated (all should be rare given the scope): + - **Missing `displayTitle` on `EnginePeer`:** unlikely, since L27 was calling it pre-migration. If it happens, verify the `EnginePeer` import chain — but do not add new imports; this file already imports `TelegramCore`. + - **A 6th call site exists** that the pre-flight grep missed (e.g., one using a different string pattern like `peer:peer` with no space, or a multi-line call). Locate it with `grep -n "peerTokenTitle" submodules/TelegramUI/Sources/ContactMultiselectionController.swift` and apply the bridge drop manually. + - **Unrelated type-inference cascade**, e.g., some `peer` local was previously inferred as `Peer` via the callback chain and now can't be. Read the error line and assess: if it's inside the function body or call site, adjust; if it's elsewhere in the file, it was pre-existing and unrelated — still, don't touch it mid-wave. Abandon per wave-rule 5 if scope creep is required. +2. Re-run the build after the fix. + +- [ ] **Step 2: Confirm the post-migration grep is clean** + +Run (after successful build): + +```bash +grep -n "peerTokenTitle(.*_asPeer())" submodules/TelegramUI/Sources/ContactMultiselectionController.swift +``` + +Expected: **0 matches**. + +--- + +## Task 5: Commit + +**Files:** +- `submodules/TelegramUI/Sources/ContactMultiselectionController.swift` + +- [ ] **Step 1: Stage the one file** + +Run: + +```bash +git add submodules/TelegramUI/Sources/ContactMultiselectionController.swift +``` + +- [ ] **Step 2: Verify the staged diff** + +Run: + +```bash +git diff --cached --stat +``` + +Expected: `1 file changed, 6 insertions(+), 6 deletions(-)` (or thereabouts — 1 line's worth of signature change, 1 body-line change, 5 identical call-site changes; each is a 1-line replacement, net zero line-count delta). + +Also run: + +```bash +git diff --cached +``` + +Inspect manually to confirm: (a) the function signature changed `peer: Peer` → `peer: EnginePeer`; (b) the body `EnginePeer(peer).displayTitle(...)` → `peer.displayTitle(...)`; (c) 5 call sites lost `._asPeer()`. No other edits. + +- [ ] **Step 3: Commit** + +Run: + +```bash +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 37 + +peerTokenTitle: peer parameter Peer -> EnginePeer. + +Drops 5 _asPeer() bridges in ContactMultiselectionController.swift +(L171, L201, L386, L403, L748) - bridges installed by prior waves. + +Private free function, single-file change. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: Confirm commit** + +Run: + +```bash +git log --oneline -3 +``` + +Expected: the new wave-37 commit at the top. + +--- + +## Task 6: Update memory / log + +**Files:** +- Modify: `/Users/isaac/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md` +- Modify: `docs/superpowers/postbox-refactor-log.md` + +- [ ] **Step 1: Read the current memory file for the refactor** + +Read `/Users/isaac/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`. + +- [ ] **Step 2: Update frontmatter + add wave-37 entry** + +Update the `description:` frontmatter field to reference wave 37 outcome (number of bridges dropped, build-iteration count, first-pass-clean-or-not). Add a bullet to "Latest commits" section with the new SHA and a one-line summary. Remove the "peerTokenTitle parameter migration" bullet from the "Wave 37 candidates" section (it's now landed). Update "Recommended wave 37" section to "Recommended wave 38" with a fresh recommendation from the remaining candidates. + +- [ ] **Step 3: Read the refactor log** + +Read `docs/superpowers/postbox-refactor-log.md`, locate the "Wave 36 outcome" section. + +- [ ] **Step 4: Append wave-37 outcome** + +Under the "Wave N outcomes" section, append a "Wave 37 outcome" subsection with: + +- Commit SHA (from `git log --oneline -1`) +- File touched (1: ContactMultiselectionController.swift) +- Lines changed (6 deletions, 6 insertions) +- Bridges dropped (5) +- Build iterations to converge (should be 1) +- Any lessons observed (likely none — this wave is mechanical) + +- [ ] **Step 5: Commit memory + log update** + +Run: + +```bash +git add docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: log wave 37 outcome + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +(Memory file under `~/.claude/` is not in the repo — save it separately via the Write tool; do not try to `git add` it.) + +--- + +## Self-review results + +**Spec coverage:** Every scope item in the spec maps to a task: +- Spec L21 signature change → Task 2 Step 2 +- Spec L27 body simplification → Task 2 Step 2 +- Spec L171/201/386/403/748 bridge drops → Task 3 Steps 1–2 +- Spec verification (grep + build + post-grep) → Task 1 + Task 4 +- Spec commit message → Task 5 Step 3 + +Out-of-scope items (L459, `import Postbox`, `accountPeerId: PeerId`) remain explicitly untouched — no task edits them. + +**Placeholder scan:** No TBD, TODO, placeholder phrases, or "handle edge cases"-style hand-waves. Every step has a concrete command or code block. + +**Type consistency:** `peer: EnginePeer`, `EnginePeer.Id` (= `PeerId` typealias), and `EnginePeer.displayTitle(strings:displayOrder:)` are all consistent across tasks. diff --git a/docs/superpowers/plans/2026-04-24-rcp-peers-engine-migration.md b/docs/superpowers/plans/2026-04-24-rcp-peers-engine-migration.md new file mode 100644 index 0000000000..9e29acda0c --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-rcp-peers-engine-migration.md @@ -0,0 +1,666 @@ +# Wave 44 — RenderedChannelParticipant.peers Engine-Peer Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate `RenderedChannelParticipant.peers: [PeerId: Peer]` to `[EnginePeer.Id: EnginePeer]`. Closes the wave-41 ratchet — the public struct no longer leaks raw Postbox `Peer` in any field. + +**Architecture:** Single atomic commit. Declaration in TelegramCore changes; 8 TelegramCore producer functions wrap raw `Peer` values at their local-dict insertion points (inside transactions that already read from Postbox); 11 consumer-surface bridges drop (6 `EnginePeer(peer)` read-wraps + 5 `.mapValues({ $0._asPeer() })` constructor-unwrap transforms); 1 consumer-surface unwrap is added where an extracted `EnginePeer` value flows into a `SimpleDictionary`. + +**Tech Stack:** Swift, Bazel (via `python3 build-system/Make/Make.py`), Postbox, TelegramCore, TelegramEngine. No unit tests — full-build verification only. + +**Spec:** `docs/superpowers/specs/2026-04-24-rcp-peers-engine-migration-design.md` + +--- + +## File Structure + +All edits happen in existing files — no new files created. Touched files: + +**TelegramCore (declaration + producers, 9 files):** +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift` (declaration) +- `submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift` +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift` +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift` +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift` +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift` +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift` +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift` +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift` + +**Consumers (drops + 1 add, 5 files):** +- `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift` +- `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift` +- `submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift` +- `submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift` +- `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift` + +**Total:** 14 files, ~30 edits, one atomic commit. + +--- + +## Task 1: Pre-flight re-verification + +**Purpose:** Confirm the grep surface matches the spec before editing anything. If any site count diverges, stop and update the spec. + +**Files:** None modified. + +- [ ] **Step 1.1: Verify 7 `participant.peers[...]` consumer read sites** + +Run: +```bash +grep -rnE "participant\.peers\[|rcp\.peers\[|renderedParticipant\.peers\[" --include="*.swift" submodules/ 2>/dev/null +``` + +Expected output — exactly 6 bracketed-indexing sites (the 7th site, iteration without bracket-indexing, is checked in Step 1.2): +- `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift:293` +- `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift:835` +- `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift:869` +- `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift:1087` +- `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift:1121` +- `submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift:164` + +If any line numbers differ by more than ±3 lines, re-read surrounding context to confirm identity. If a NEW site appears that isn't in the spec, STOP and update the spec before proceeding. + +- [ ] **Step 1.2: Verify the iteration site is still at the expected line** + +Run: +```bash +grep -nE "for \(.*,.* peer\) in participant\.peers" submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +``` + +Expected: `672: for (_, peer) in participant.peers {` + +- [ ] **Step 1.3: Verify all 8 TelegramCore producers still build `var peers: [PeerId: Peer] = [:]` locally** + +Run: +```bash +grep -rnE "^[[:space:]]+var peers: \[PeerId: Peer\] = \[:\]" submodules/TelegramCore/Sources/TelegramEngine/ 2>/dev/null +``` + +Expected 8 matches, one per producer file: +- `Messages/RequestStartBot.swift:61` +- `Peers/ChannelOwnershipTransfer.swift:170` +- `Peers/JoinChannel.swift:59` +- `Peers/AddPeerMember.swift:242` +- `Peers/PeerAdmins.swift:251` +- `Peers/ChannelBlacklist.swift:128` +- `Peers/Ranks.swift:60` +- `Peers/ChannelMembers.swift:102` + +If a producer is missing from this grep, check whether it now receives `peers` as a parameter rather than building locally — if so, STOP and update the spec (chain-migration needed). + +- [ ] **Step 1.4: Verify no `as?` / `is TelegramX` casts exist on extracted dict values** + +Run: +```bash +grep -rnE "peer = participant\.peers" --include="*.swift" -A 4 submodules/ 2>/dev/null | grep -E "as\?|is Telegram" +``` + +Expected output: empty. If this returns non-empty, STOP and update the spec. + +- [ ] **Step 1.5: Verify no one is assigning into `participant.peers` (writes would break the migration)** + +Run: +```bash +grep -rnE "participant\.peers\[[^]]+\][[:space:]]*=" --include="*.swift" submodules/ 2>/dev/null +``` + +Expected output: empty (`.peers` is a `let`; no writes possible anyway, but double-check). + +--- + +## Task 2: Migrate declaration in ChannelParticipants.swift + +**Purpose:** Change the struct field type and init default. + +**Files:** +- Modify: `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift:11, 14` + +- [ ] **Step 2.1: Change field declaration** + +In `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift`, line 11: + +```swift +// before + public let peers: [PeerId: Peer] + +// after + public let peers: [EnginePeer.Id: EnginePeer] +``` + +- [ ] **Step 2.2: Change init default** + +Same file, line 14: + +```swift +// before + public init(participant: ChannelParticipant, peer: EnginePeer, peers: [PeerId: Peer] = [:], presences: [PeerId: PeerPresence] = [:]) { + +// after + public init(participant: ChannelParticipant, peer: EnginePeer, peers: [EnginePeer.Id: EnginePeer] = [:], presences: [PeerId: PeerPresence] = [:]) { +``` + +Do NOT commit yet — this leaves the repo in a broken state until producers and consumers are updated. + +--- + +## Task 3: Migrate TelegramCore producers (8 files) + +**Purpose:** Each of the 8 TelegramCore producers builds a local `peers: [PeerId: Peer] = [:]` dict from raw Postbox peers inside a transaction. Migrate each local dict to `[EnginePeer.Id: EnginePeer] = [:]` and wrap every insertion value with `EnginePeer(...)`. + +**Pattern (applies to every sub-step):** +```swift +// before +var peers: [PeerId: Peer] = [:] +peers[X.id] = X + +// after +var peers: [EnginePeer.Id: EnginePeer] = [:] +peers[X.id] = EnginePeer(X) +``` + +The surrounding `presences: [PeerId: PeerPresence]` dict and the `RCP(..., peer: EnginePeer(X), ...)` wrap on the primary `peer` field both stay unchanged. + +- [ ] **Step 3.1: Migrate `RequestStartBot.swift`** + +File: `submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift` + +Line 61: `var peers: [PeerId: Peer] = [:]` → `var peers: [EnginePeer.Id: EnginePeer] = [:]` +Line 64: `peers[peer.id] = peer` → `peers[peer.id] = EnginePeer(peer)` + +- [ ] **Step 3.2: Migrate `ChannelOwnershipTransfer.swift`** + +File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift` + +Line 170: `var peers: [PeerId: Peer] = [:]` → `var peers: [EnginePeer.Id: EnginePeer] = [:]` +Line 172: `peers[accountUser.id] = accountUser` → `peers[accountUser.id] = EnginePeer(accountUser)` +Line 176: `peers[user.id] = user` → `peers[user.id] = EnginePeer(user)` + +Line 180 is a double-RCP-construction; `peers:` reuses the same local — no change at line 180. + +- [ ] **Step 3.3: Migrate `JoinChannel.swift`** + +File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift` + +Line 59: `var peers: [PeerId: Peer] = [:]` → `var peers: [EnginePeer.Id: EnginePeer] = [:]` +Line 64: `peers[account.peerId] = peer` → `peers[account.peerId] = EnginePeer(peer)` +Line 77: `peers[peer.id] = peer` → `peers[peer.id] = EnginePeer(peer)` + +- [ ] **Step 3.4: Migrate `AddPeerMember.swift`** + +File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift` + +Line 242: `var peers: [PeerId: Peer] = [:]` → `var peers: [EnginePeer.Id: EnginePeer] = [:]` +Line 244: `peers[memberPeer.id] = memberPeer` → `peers[memberPeer.id] = EnginePeer(memberPeer)` +Line 251: `peers[peer.id] = peer` → `peers[peer.id] = EnginePeer(peer)` + +- [ ] **Step 3.5: Migrate `PeerAdmins.swift`** + +File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift` + +Line 251: `var peers: [PeerId: Peer] = [:]` → `var peers: [EnginePeer.Id: EnginePeer] = [:]` +Line 253: `peers[adminPeer.id] = adminPeer` → `peers[adminPeer.id] = EnginePeer(adminPeer)` +Line 259: `peers[peer.id] = peer` → `peers[peer.id] = EnginePeer(peer)` + +- [ ] **Step 3.6: Migrate `ChannelBlacklist.swift`** + +File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift` + +Line 128: `var peers: [PeerId: Peer] = [:]` → `var peers: [EnginePeer.Id: EnginePeer] = [:]` +Line 130: `peers[memberPeer.id] = memberPeer` → `peers[memberPeer.id] = EnginePeer(memberPeer)` +Line 136: `peers[peer.id] = peer` → `peers[peer.id] = EnginePeer(peer)` + +- [ ] **Step 3.7: Migrate `Ranks.swift`** + +File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift` + +Line 60: `var peers: [PeerId: Peer] = [:]` → `var peers: [EnginePeer.Id: EnginePeer] = [:]` +Line 62: `peers[user.id] = user` → `peers[user.id] = EnginePeer(user)` +Line 68: `peers[peer.id] = peer` → `peers[peer.id] = EnginePeer(peer)` + +- [ ] **Step 3.8: Migrate `ChannelMembers.swift`** + +File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift` + +Line 102: `var peers: [PeerId: Peer] = [:]` → `var peers: [EnginePeer.Id: EnginePeer] = [:]` +Line 105: `peers[peer.id] = peer` → `peers[peer.id] = EnginePeer(peer)` + +- [ ] **Step 3.9: Post-producer verification** + +Run: +```bash +grep -rnE "^[[:space:]]+var peers: \[PeerId: Peer\] = \[:\]" submodules/TelegramCore/Sources/TelegramEngine/ 2>/dev/null +``` + +Expected: no output (all 8 have been converted). + +Run: +```bash +grep -rnE "^[[:space:]]+var peers: \[EnginePeer\.Id: EnginePeer\] = \[:\]" submodules/TelegramCore/Sources/TelegramEngine/ 2>/dev/null | wc -l +``` + +Expected: `8` (or ` 8`). + +--- + +## Task 4: Drop 5 consumer `.mapValues({ $0._asPeer() })` transforms + +**Purpose:** These consumer-side constructors build a `[EnginePeer.Id: EnginePeer]` source dict locally and currently unwrap to `[PeerId: Peer]` via `.mapValues({ $0._asPeer() })` to feed the old constructor signature. After Task 2, the constructor expects engine values directly — the transform becomes a no-op and is removed. + +**Pattern (applies to every sub-step):** +```swift +// before +peers: peers.mapValues({ $0._asPeer() }) + +// after +peers: peers +``` + +- [ ] **Step 4.1: `ChannelAdminsController.swift:926`** + +File: `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift` + +Line 926 (long line): locate the substring `peers: peers.mapValues({ $0._asPeer() })` and replace with `peers: peers`. + +- [ ] **Step 4.2: `ChannelMembersSearchContainerNode.swift:994`** + +File: `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift` + +Line 994: replace `peers: peers.mapValues({ $0._asPeer() })` → `peers: peers`. + +- [ ] **Step 4.3: `ChannelMembersSearchContainerNode.swift:998`** + +Same file, line 998: replace `peers: peers.mapValues({ $0._asPeer() })` → `peers: peers`. + +- [ ] **Step 4.4: `ChannelMembersSearchControllerNode.swift:409`** + +File: `submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift` + +Line 409: replace `peers: peers.mapValues({ $0._asPeer() })` → `peers: peers`. + +- [ ] **Step 4.5: `ChannelMembersSearchControllerNode.swift:413`** + +Same file, line 413: replace `peers: peers.mapValues({ $0._asPeer() })` → `peers: peers`. + +- [ ] **Step 4.6: Post-Task-4 verification** + +Run: +```bash +grep -rnE "peers\.mapValues\(\{ \$0\._asPeer\(\) \}\)" --include="*.swift" submodules/ 2>/dev/null +``` + +Expected: no output (all 5 drops applied). If any remain, locate and drop. + +--- + +## Task 5: Drop 6 consumer `EnginePeer(peer).displayTitle(...)` read-wraps + +**Purpose:** Each site extracts `peer` from `participant.peers[X]`, wraps with `EnginePeer(peer)` to call `.displayTitle(...)`. After Task 2 the extracted `peer` is already `EnginePeer` — drop the wrap. + +**Pattern (applies to every sub-step):** +```swift +// before +EnginePeer(peer).displayTitle(strings: ..., displayOrder: ...) + +// after +peer.displayTitle(strings: ..., displayOrder: ...) +``` + +- [ ] **Step 5.1: `ChannelAdminsController.swift:297`** + +File: `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift`, line 297. + +Replace: +```swift +peerText = strings.Channel_Management_PromotedBy(EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string +``` +with: +```swift +peerText = strings.Channel_Management_PromotedBy(peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string +``` + +The adjacent `peer.id == participant.peer.id` comparison at line 294 stays unchanged (both are `EnginePeer.Id`). + +- [ ] **Step 5.2: `ChannelMembersSearchContainerNode.swift:839`** + +File: `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift`, line 839. + +Replace: +```swift +label = presentationData.strings.Channel_Management_PromotedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string +``` +with: +```swift +label = presentationData.strings.Channel_Management_PromotedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string +``` + +- [ ] **Step 5.3: `ChannelMembersSearchContainerNode.swift:870`** + +Same file, line 870. + +Replace: +```swift +label = presentationData.strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string +``` +with: +```swift +label = presentationData.strings.Channel_Management_RemovedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string +``` + +- [ ] **Step 5.4: `ChannelMembersSearchContainerNode.swift:1091`** + +Same file, line 1091. + +Replace: +```swift +label = presentationData.strings.Channel_Management_PromotedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string +``` +with: +```swift +label = presentationData.strings.Channel_Management_PromotedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string +``` + +- [ ] **Step 5.5: `ChannelMembersSearchContainerNode.swift:1122`** + +Same file, line 1122. + +Replace: +```swift +label = presentationData.strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string +``` +with: +```swift +label = presentationData.strings.Channel_Management_RemovedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string +``` + +- [ ] **Step 5.6: `ChannelBlacklistController.swift:165`** + +File: `submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift`, line 165. + +Replace: +```swift +text = .text(strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string, .secondary) +``` +with: +```swift +text = .text(strings.Channel_Management_RemovedBy(peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string, .secondary) +``` + +- [ ] **Step 5.7: Post-Task-5 verification** + +Run: +```bash +grep -rnE "EnginePeer\(peer\)\.displayTitle" --include="*.swift" submodules/PeerInfoUI/ 2>/dev/null +``` + +Expected: no output within PeerInfoUI. (Other modules may still have unrelated `EnginePeer(peer).displayTitle` usages on non-RCP-peers peers — those are out of scope.) + +Run specifically for the 6 migrated sites: +```bash +grep -n "EnginePeer(peer)\.displayTitle" submodules/PeerInfoUI/Sources/ChannelAdminsController.swift submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift 2>/dev/null +``` + +Expected: no output. + +--- + +## Task 6: Add 1 consumer unwrap at ChatRecentActionsHistoryTransition + +**Purpose:** The one site that iterates `participant.peers` and inserts values into a `SimpleDictionary` container. After Task 2, the iterated `peer` is `EnginePeer`; the outer container still expects raw `Peer`. Unwrap at the insertion site. + +**Files:** +- Modify: `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift:673` + +- [ ] **Step 6.1: Replace insertion line** + +In `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift`: + +Context (lines 672–674, unchanged outside line 673): +```swift +for (_, peer) in participant.peers { + peers[peer.id] = peer +} +``` + +After edit: +```swift +for (_, peer) in participant.peers { + peers[peer.id] = peer._asPeer() +} +``` + +- [ ] **Step 6.2: Spot-check nearby wave-41 unwrap (reference, no change)** + +Line 675 in the same function is `peers[participant.peer.id] = participant.peer._asPeer()` — a wave-41 artifact, unrelated to this wave. Leave unchanged. + +--- + +## Task 7: Full build verification + +**Purpose:** Verify the atomic change set compiles. Produces the ONLY real test signal for this wave. + +**Files:** None modified; this is a build run. + +- [ ] **Step 7.1: Run the full build** + +Run: +```bash +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 \ + --continueOnError +``` + +Expected: build succeeds. Look for `INFO: Build completed successfully` near the end. + +- [ ] **Step 7.2: If build fails — triage** + +Expected failure patterns (from wave-41 lesson, budget 2–3 iterations): + +1. **Missing producer wrap** — compiler error `cannot assign value of type 'Peer' to subscript of type 'EnginePeer'` (or similar) at a TelegramCore producer file → check that file's `var peers:` decl was converted AND all insertion RHS values are wrapped. +2. **Missed consumer site** — compiler error at a `.displayTitle` call on a raw Peer → find `EnginePeer(peer).displayTitle` site that Task 5 missed; drop the wrap. +3. **Mismatched mapValues drop** — `cannot convert value of type '[EnginePeer.Id: EnginePeer]' to expected argument type '[PeerId: Peer]'` → the spec's risk #3 triggered (a `.mapValues` site had a raw-Peer source after all); replace the drop with `peers.mapValues(EnginePeer.init)` at that site instead. +4. **New grep surface** — compiler complains about a site not in this plan → add it to the commit's scope; log it to the outcome doc. + +Apply fixes, re-run Step 7.1. Repeat up to 3 iterations before re-evaluating scope. + +- [ ] **Step 7.3: Post-build final grep audit** + +Run: +```bash +grep -rnE "participant\.peers\[[^]]+\]" --include="*.swift" submodules/ 2>/dev/null +``` + +Expected: the same 6 read sites as Step 1.1 (now without `EnginePeer(peer)` wraps). + +Run: +```bash +grep -rnE "peers\.mapValues\(\{ \$0\._asPeer\(\) \}\)" --include="*.swift" submodules/ 2>/dev/null +``` + +Expected: no output. + +Run: +```bash +grep -n "public let peers: \[" submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift +``` + +Expected: `11: public let peers: [EnginePeer.Id: EnginePeer]`. + +--- + +## Task 8: Atomic commit + +**Purpose:** Land all wave-44 edits in ONE commit. Explicitly enumerate files in `git add` (wave-39 lesson — re-confirmed in waves 41, 42, 43) to avoid pulling in the pre-existing working-tree WIP listed in the spec's risk section (`ListView.swift`, `ChatMessageTransitionNode.swift`, tulsi/, TgVoip/, libx264/). + +**Files:** Commits all 14 wave-44 files. + +- [ ] **Step 8.1: Confirm working-tree state** + +Run: +```bash +git status --short +``` + +Expected (pre-existing WIP, unchanged): +- ` m build-system/bazel-rules/sourcekit-bazel-bsp` +- ` M submodules/Display/Source/ListView.swift` (do NOT include) +- ` M submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift` (do NOT include) +- `?? build-system/tulsi/` (do NOT include) +- `?? submodules/TgVoip/` (do NOT include) +- `?? third-party/libx264/` (do NOT include) + +Plus the wave-44 modified files: +- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift` +- ` M submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift` +- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift` +- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift` +- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift` +- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift` +- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift` +- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift` +- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift` +- ` M submodules/PeerInfoUI/Sources/ChannelAdminsController.swift` +- ` M submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift` +- ` M submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift` +- ` M submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift` +- ` M submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift` + +If the set of wave-44-modified files doesn't match exactly (extra or missing), STOP and investigate before committing. + +- [ ] **Step 8.2: Stage only wave-44 files** + +Run: +```bash +git add \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift \ + submodules/PeerInfoUI/Sources/ChannelAdminsController.swift \ + submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift \ + submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift \ + submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +``` + +- [ ] **Step 8.3: Verify staged set matches expected** + +Run: +```bash +git diff --cached --stat +``` + +Expected: exactly 14 files staged, all from the wave-44 list. If `ListView.swift`, `ChatMessageTransitionNode.swift`, `bazel-rules/sourcekit-bazel-bsp`, `tulsi/`, `TgVoip/`, or `libx264/` appear here, unstage them. + +- [ ] **Step 8.4: Commit** + +Run: +```bash +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 44 + +Migrate RenderedChannelParticipant.peers from [PeerId: Peer] to +[EnginePeer.Id: EnginePeer]. Closes the wave-41 ratchet — the public +struct no longer leaks raw Peer types in any field (presences stays +Postbox-typed; separate migration). + +Consumer-surface: -10 bridges. Dropped 6 EnginePeer(peer) read-wraps +at participant.peers[...] extraction sites across +ChannelAdminsController, ChannelMembersSearchContainerNode, +ChannelBlacklistController. Dropped 5 .mapValues({ $0._asPeer() }) +constructor-unwrap transforms in ChannelAdminsController, +ChannelMembersSearchContainerNode, ChannelMembersSearchControllerNode. +Added 1 ._asPeer() at ChatRecentActionsHistoryTransition.swift:673 +where the iterated value is inserted into a raw-Peer SimpleDictionary. + +TelegramCore producers: 8 files build the local peers dict inside +postbox.transaction and wrap at the insertion point. ChannelMembers, +RequestStartBot, ChannelOwnershipTransfer, JoinChannel, AddPeerMember, +PeerAdmins, ChannelBlacklist, Ranks. + +No unit tests in this project; full Telegram/Telegram build verified +under configuration=debug_sim_arm64. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 8.5: Verify commit** + +Run: +```bash +git log -1 --stat +``` + +Expected: commit with 14 files changed, message starting with `Postbox -> TelegramEngine wave 44`. + +Run: +```bash +git status --short +``` + +Expected: no M- or A-flagged wave-44 files (all committed); only the pre-existing WIP (`ListView.swift`, `ChatMessageTransitionNode.swift`, etc.) remains. + +--- + +## Rollback + +If the wave cannot be completed (e.g., build fails after 4+ iterations and the scope balloons beyond plan): + +```bash +git restore --staged \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift \ + submodules/PeerInfoUI/Sources/ChannelAdminsController.swift \ + submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift \ + submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift \ + submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift + +git checkout -- \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift \ + submodules/PeerInfoUI/Sources/ChannelAdminsController.swift \ + submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift \ + submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift \ + submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +``` + +Then document what was learned in an outcome doc and update `project_postbox_refactor_next_wave.md`. + +--- + +## Success criteria (from spec) + +1. ✅ `ChannelParticipants.swift` has `peers: [EnginePeer.Id: EnginePeer]` declaration (Task 2). +2. ✅ All 8 TelegramCore producers compile with wrapped inserts (Task 3). +3. ✅ All 5 consumer `.mapValues({ $0._asPeer() })` transforms are removed (Task 4). +4. ✅ All 6 consumer `EnginePeer(peer).displayTitle(...)` wraps on extracted dict values are removed (Task 5). +5. ✅ `ChatRecentActionsHistoryTransition.swift:673` uses `peer._asPeer()` for the SimpleDictionary insertion value (Task 6). +6. ✅ Full `Telegram/Telegram` build (`configuration=debug_sim_arm64`) is clean — **one** atomic commit (Tasks 7, 8). +7. ✅ Grep post-migration: `participant.peers[` returns only engine-typed call sites; no residual `EnginePeer(peer)` on `.peers[...]` extractions (Steps 5.7, 7.3). diff --git a/docs/superpowers/plans/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration.md b/docs/superpowers/plans/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration.md new file mode 100644 index 0000000000..9ac710ea08 --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration.md @@ -0,0 +1,860 @@ +# Wave 41 — `RenderedChannelParticipant.peer → EnginePeer` Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate `TelegramCore.RenderedChannelParticipant.peer` from Postbox `Peer` to TelegramCore `EnginePeer`. Drop ~37 bridges (net ~−14 after adds) and eliminate 2 Shape-C ratchet wraps installed by wave 39. + +**Architecture:** Single atomic commit. One TelegramCore struct field change + 16 TelegramCore internal construction sites wrapped with `EnginePeer(peer)` + 17 consumer files updated: ZERO sites untouched (~160), ~32 DROP sites unwrapped, 9 CAST sites rewritten to pattern-match, 3 ADD-ASPEER sites append `._asPeer()`, 7 ADD-WRAP consumer constructors wrap raw `Peer` with `EnginePeer`. + +**Tech Stack:** Swift, Bazel (`Make.py` wrapper), TelegramCore, Postbox → TelegramEngine refactor conventions per `CLAUDE.md`. + +**Build command:** +```sh +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError +``` + +--- + +## File Structure + +**Created:** none. + +**Modified (27 files):** + +TelegramCore (10 files): +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift` — struct field type + init param + Equatable impl +- `submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift` — 1 constructor wrap +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift` — 1 constructor wrap +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift` — 7 constructor wraps +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift` — 1 constructor wrap +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift` — 1 constructor wrap +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift` — 2 constructor wraps +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift` — 1 constructor wrap +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift` — 1 constructor wrap +- `submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift` — 1 constructor wrap + +PeerInfoUI (6 files): +- `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift` +- `submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift` +- `submodules/PeerInfoUI/Sources/ChannelMembersController.swift` +- `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift` +- `submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift` +- `submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift` + +Other consumers (11 files): +- `submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift` +- `submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift` +- `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift` +- `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift` +- `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift` +- `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift` +- `submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift` +- `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift` +- `submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift` +- `submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift` *(no `participant.peer` edits needed — all ZERO; file touched only if build surfaces type issues)* +- `submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift` *(no edits expected — only `item.peer.id` reference is ZERO)* + +--- + +## Task 1: Migrate the struct definition + +**File:** `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift` + +- [ ] **Step 1.1: Edit struct field, init param, and Equatable impl** + +Replace the entire struct body: + +```swift +public struct RenderedChannelParticipant: Equatable { + public let participant: ChannelParticipant + public let peer: EnginePeer + public let peers: [PeerId: Peer] + public let presences: [PeerId: PeerPresence] + + public init(participant: ChannelParticipant, peer: EnginePeer, peers: [PeerId: Peer] = [:], presences: [PeerId: PeerPresence] = [:]) { + self.participant = participant + self.peer = peer + self.peers = peers + self.presences = presences + } + + public static func ==(lhs: RenderedChannelParticipant, rhs: RenderedChannelParticipant) -> Bool { + return lhs.participant == rhs.participant && lhs.peer == rhs.peer + } +} +``` + +Note: the file already imports both `Postbox` (for `Peer`/`PeerId`/`PeerPresence`) and TelegramCore internal symbols (`EnginePeer` visible from within the same module). No import changes needed. + +--- + +## Task 2: Wrap TelegramCore-internal constructor sites + +Each site receives a raw `Peer` and must now wrap it with `EnginePeer(peer)`. All edits are identical in shape. + +- [ ] **Step 2.1:** `submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift:65` + +Before: +```swift +return .channelParticipant(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: presences)) +``` +After: +```swift +return .channelParticipant(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer), peers: peers, presences: presences)) +``` + +- [ ] **Step 2.2:** `submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift:255` + +Before: +```swift +return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: memberPeer, peers: peers, presences: presences)) +``` +After: +```swift +return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(memberPeer), peers: peers, presences: presences)) +``` + +- [ ] **Step 2.3:** `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift` — 7 constructor wraps + +Line 271: +```swift +action = .participantInvite(RenderedChannelParticipant(participant: participant, peer: peer)) +// becomes: +action = .participantInvite(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer))) +``` + +Line 279 (two constructors on one line): +```swift +action = .participantToggleBan(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer)) +// becomes: +action = .participantToggleBan(prev: RenderedChannelParticipant(participant: prevParticipant, peer: EnginePeer(prevPeer)), new: RenderedChannelParticipant(participant: newParticipant, peer: EnginePeer(newPeer))) +``` + +Line 287 (two constructors on one line): +```swift +action = .participantToggleAdmin(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer)) +// becomes: +action = .participantToggleAdmin(prev: RenderedChannelParticipant(participant: prevParticipant, peer: EnginePeer(prevPeer)), new: RenderedChannelParticipant(participant: newParticipant, peer: EnginePeer(newPeer))) +``` + +Line 483 (two constructors on one line): +```swift +action = .participantSubscriptionExtended(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer)) +// becomes: +action = .participantSubscriptionExtended(prev: RenderedChannelParticipant(participant: prevParticipant, peer: EnginePeer(prevPeer)), new: RenderedChannelParticipant(participant: newParticipant, peer: EnginePeer(newPeer))) +``` + +- [ ] **Step 2.4:** `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift:140` + +Before: +```swift +return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: memberPeer, peers: peers, presences: presences), isMember) +``` +After: +```swift +return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(memberPeer), peers: peers, presences: presences), isMember) +``` + +- [ ] **Step 2.5:** `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift:115` + +Before: +```swift +items.append(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: renderedPresences)) +``` +After: +```swift +items.append(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer), peers: peers, presences: renderedPresences)) +``` + +- [ ] **Step 2.6:** `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift:180` + +Before: +```swift +return [(currentCreator, RenderedChannelParticipant(participant: updatedPreviousCreator, peer: accountUser, peers: peers, presences: presences)), (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: user, peers: peers, presences: presences))] +``` +After: +```swift +return [(currentCreator, RenderedChannelParticipant(participant: updatedPreviousCreator, peer: EnginePeer(accountUser), peers: peers, presences: presences)), (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(user), peers: peers, presences: presences))] +``` + +- [ ] **Step 2.7:** `submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift:82` + +Before: +```swift +return RenderedChannelParticipant(participant: updatedParticipant, peer: peer, peers: peers, presences: presences) +``` +After: +```swift +return RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(peer), peers: peers, presences: presences) +``` + +- [ ] **Step 2.8:** `submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift:262` + +Before: +```swift +return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: adminPeer, peers: peers, presences: presences)) +``` +After: +```swift +return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(adminPeer), peers: peers, presences: presences)) +``` + +- [ ] **Step 2.9:** `submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift:95` + +Before: +```swift +return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: user, peers: peers, presences: presences)) +``` +After: +```swift +return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(user), peers: peers, presences: presences)) +``` + +--- + +## Task 3: Consumer — PeerInfoUI/ChannelAdminsController.swift + +**File:** `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift` + +- [ ] **Step 3.1:** Line 326 — DROP `EnginePeer(participant.peer)` wrap. + +Before: +```swift +return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(participant.peer), presence: participant.presences[participant.peer.id].flatMap { EnginePeer.Presence($0) }, text: peerText.isEmpty ? .presence : .text(peerText, .secondary), label: label, editing: editing, revealOptions: revealOptions, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: action, setPeerIdWithRevealedOptions: { previousId, id in +``` +After: replace `peer: EnginePeer(participant.peer)` → `peer: participant.peer` (leave the rest of the line intact). + +- [ ] **Step 3.2:** Line 921 — DROP `._asPeer()` in constructor. + +Before: +```swift +result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: rank), peer: peer._asPeer(), presences: presences)) +``` +After: +```swift +result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: rank), peer: peer, presences: presences)) +``` +(`peer` here is already `EnginePeer` — confirmed by surrounding code where `creatorPeer: EnginePeer?` is assigned from this same loop variable.) + +- [ ] **Step 3.3:** Line 926 — DROP `._asPeer()` in constructor. + +Before: +```swift +result.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: .internal_groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: rank, subscriptionUntilDate: nil), peer: peer._asPeer(), peers: peers.mapValues({ $0._asPeer() }), presences: presences)) +``` +After: change `peer: peer._asPeer()` → `peer: peer`. Leave `peers.mapValues({ $0._asPeer() })` intact — `peers` field is unchanged. + +--- + +## Task 4: Consumer — PeerInfoUI/ChannelBlacklistController.swift + +**File:** `submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift` + +- [ ] **Step 4.1:** Line 170 (or 381 — the site installed by wave 39; the file has one site `EnginePeer(participant.peer)`) + +Before: +```swift +peer: EnginePeer(participant.peer) +``` +After: +```swift +peer: participant.peer +``` + +Note: the file may have a single such site; use: +``` +grep -n 'EnginePeer(participant\.peer)' submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift +``` +and DROP every match. + +--- + +## Task 5: Consumer — PeerInfoUI/ChannelMembersController.swift + +**File:** `submodules/PeerInfoUI/Sources/ChannelMembersController.swift` + +- [ ] **Step 5.1:** Line 305 — CAST rewrite. + +Before: +```swift +if let user = participant.peer as? TelegramUser, let _ = user.botInfo { +``` +After: +```swift +if case let .user(user) = participant.peer, let _ = user.botInfo { +``` + +- [ ] **Step 5.2:** Line 334 — DROP wrap. + +Before: +```swift +peer: EnginePeer(participant.peer) +``` +After: +```swift +peer: participant.peer +``` + +- [ ] **Step 5.3:** Line 707 — DROP wrap (the wave-39-installed Shape-C wrap). + +Before: +```swift +peer: EnginePeer(participant.peer) +``` +After: +```swift +peer: participant.peer +``` + +--- + +## Task 6: Consumer — PeerInfoUI/ChannelMembersSearchContainerNode.swift + +**File:** `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift` + +This file has the most sites (4 CAST, 3 DROP pairs, 3 ADD-WRAP constructor sites). + +- [ ] **Step 6.1:** Line 212 — DROP two wraps on one line. + +Before: +```swift +peer: .peer(peer: EnginePeer(participant.peer), chatPeer: EnginePeer(participant.peer)), +``` +After: +```swift +peer: .peer(peer: participant.peer, chatPeer: participant.peer), +``` + +- [ ] **Step 6.2:** Line 223 — DROP wrap. + +Before: +```swift +interaction.peerSelected(EnginePeer(participant.peer), participant) +``` +After: +```swift +interaction.peerSelected(participant.peer, participant) +``` + +- [ ] **Step 6.3:** Line 752 — CAST rewrite. + +Before: +```swift +if excludeBots, let user = participant.peer as? TelegramUser, user.botInfo != nil { +``` +After: +```swift +if excludeBots, case let .user(user) = participant.peer, user.botInfo != nil { +``` + +- [ ] **Step 6.4:** Line 884 — CAST rewrite. Same pattern as 6.3. + +- [ ] **Step 6.5:** Line 987 — ADD-WRAP constructor. + +Before: +```swift +renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer) +``` +After: +```swift +renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: EnginePeer(peer)) +``` +(`peer` here is raw `Peer` from `peerView.peers[participant.peerId]` — confirmed by surrounding iteration code.) + +- [ ] **Step 6.6:** Line 994 — ADD-WRAP constructor. + +Change `peer: peer` to `peer: EnginePeer(peer)`. Full site for reference: +```swift +renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: .legacyGroup(group))), promotedBy: creatorPeer?.id ?? context.account.peerId, canBeEditedByAccountPeer: creatorPeer?.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() })) +``` +Change only `peer: peer,` → `peer: EnginePeer(peer),`. + +- [ ] **Step 6.7:** Line 998 — ADD-WRAP constructor. + +```swift +renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() })) +``` +Change only `peer: peer,` → `peer: EnginePeer(peer),`. + +- [ ] **Step 6.8:** Line 1052 — CAST rewrite. Same pattern as 6.3. + +- [ ] **Step 6.9:** Line 1136 — CAST rewrite. Same pattern as 6.3. + +--- + +## Task 7: Consumer — PeerInfoUI/ChannelMembersSearchControllerNode.swift + +**File:** `submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift` + +- [ ] **Step 7.1:** Line 148 — DROP wrap. + +Before: +```swift +peer: EnginePeer(participant.peer) +``` +After: +```swift +peer: participant.peer +``` +(The line has the wrap appearing twice — search the file for `EnginePeer(participant.peer)` and drop each occurrence. Use Edit with `replace_all` if unambiguous.) + +- [ ] **Step 7.2:** Line 404 — ADD-WRAP constructor. + +Before: +```swift +renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer, presences: peerView.peerPresences) +``` +After: +```swift +renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: EnginePeer(peer), presences: peerView.peerPresences) +``` + +- [ ] **Step 7.3:** Line 409 — ADD-WRAP constructor. + +Change `peer: peer,` → `peer: EnginePeer(peer),` in the full line: +```swift +renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: EnginePeer(mainPeer))), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences) +``` + +- [ ] **Step 7.4:** Line 413 — ADD-WRAP constructor. Same `peer: peer,` → `peer: EnginePeer(peer),`. + +- [ ] **Step 7.5:** Line 516 — CAST rewrite. + +Before: +```swift +if let user = participant.peer as? TelegramUser, user.botInfo != nil { +``` +After: +```swift +if case let .user(user) = participant.peer, user.botInfo != nil { +``` + +- [ ] **Step 7.6:** Line 558 — CAST rewrite. Same pattern as 7.5. + +--- + +## Task 8: Consumer — PeerInfoUI/ChannelPermissionsController.swift + +**File:** `submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift` + +- [ ] **Step 8.1:** Lines 480 and 483 — DROP wraps. + +Both lines contain `EnginePeer(participant.peer)`. Change each to `participant.peer`. + +If the two occurrences are unambiguous, use Edit with `replace_all=true` on `EnginePeer(participant.peer)` → `participant.peer`. + +--- + +## Task 9: Consumer — SearchPeerMembers/SearchPeerMembers.swift + +**File:** `submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift` + +- [ ] **Step 9.1:** Lines 30, 36, 61, 76 — DROP wraps. + +All four sites are `EnginePeer(participant.peer)`. Use Edit with `replace_all=true`: +- old: `EnginePeer(participant.peer)` +- new: `participant.peer` + +Verify with `grep -n 'EnginePeer(participant\.peer)' submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift` → should return empty after edit. + +--- + +## Task 10: Consumer — ChatRecentActionsController.swift + +**File:** `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift` + +- [ ] **Step 10.1:** Line 359 — DROP wrap. + +Before: +```swift +EnginePeer(participant.peer) +``` +After: +```swift +participant.peer +``` + +--- + +## Task 11: Consumer — ChatRecentActionsFilterController.swift + +**File:** `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift` + +- [ ] **Step 11.1:** Line 217 — DROP wrap. + +Change `EnginePeer(participant.peer)` → `participant.peer` on line 217. + +- [ ] **Step 11.2:** Line 445 — ADD-WRAP constructor rewrite. + +Before: +```swift +if let peer = peer, case let .user(user) = peer { + return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: user) +} +``` +After: +```swift +if let peer = peer, case let .user(user) = peer { + return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: .user(user)) +} +``` +(`.user(user)` is the enum case `EnginePeer.user(TelegramUser)`. Alternative: `peer: EnginePeer(user)` or `peer: peer` — but `peer: peer` reuses the already-unwrapped EnginePeer and is the cleanest. Use `peer: peer`.) + +Preferred after: +```swift +if let peer = peer, case let .user(user) = peer { + return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer) +} +``` + +--- + +## Task 12: Consumer — ChatRecentActionsHistoryTransition.swift + +**File:** `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift` + +This is the highest-volume consumer file (12 `EnginePeer(new.peer)` sites + 2 ADD-ASPEER sites). + +- [ ] **Step 12.1:** DROP all `EnginePeer(new.peer)` wraps. + +Use Edit with `replace_all=true`: +- old: `EnginePeer(new.peer)` +- new: `new.peer` + +After: grep `EnginePeer(new\.peer)` should return empty. + +- [ ] **Step 12.2:** Line 675 — ADD-ASPEER. + +Before: +```swift +peers[participant.peer.id] = participant.peer +``` +After: +```swift +peers[participant.peer.id] = participant.peer._asPeer() +``` +(Target dict is `SimpleDictionary`; the value side needs raw Peer.) + +- [ ] **Step 12.3:** Line 2275 — ADD-ASPEER. + +Before: +```swift +peers[new.peer.id] = new.peer +``` +After: +```swift +peers[new.peer.id] = new.peer._asPeer() +``` + +--- + +## Task 13: Consumer — PeerInfoMembers.swift + +**File:** `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift` + +- [ ] **Step 13.1:** Line 33 — ADD-ASPEER. + +Before: +```swift +var peer: Peer { + switch self { + case let .channelMember(participant, _): + return participant.peer +``` +After: +```swift +var peer: Peer { + switch self { + case let .channelMember(participant, _): + return participant.peer._asPeer() +``` + +No other edits in this file. The `participant.peer.id` accesses at lines 22, 44 are ZERO; `item.peer.id` at line 171 is ZERO. + +--- + +## Task 14: Consumer — ShareWithPeersScreenState.swift + +**File:** `submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift` + +- [ ] **Step 14.1:** Line 558 — DROP wrap. + +Before: +```swift +peers.append(EnginePeer(participant.peer)) +``` +After: +```swift +peers.append(participant.peer) +``` + +- [ ] **Step 14.2:** Line 566 — CAST rewrite. + +Before: +```swift +if let user = participant.peer as? TelegramUser, user.botInfo != nil { +``` +After: +```swift +if case let .user(user) = participant.peer, user.botInfo != nil { +``` + +- [ ] **Step 14.3:** Line 576 — DROP wrap. + +Before: +```swift +peers.append(EnginePeer(participant.peer)) +``` +After: +```swift +peers.append(participant.peer) +``` + +--- + +## Task 15: Consumer — AdminUserActionsSheet.swift + +**File:** `submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift` + +This file has ~6 `EnginePeer(peer.peer)` / `EnginePeer(component.peers[0].peer)` wraps and many ZERO sites. + +- [ ] **Step 15.1:** Use Edit with `replace_all=true`: +- old: `EnginePeer(peer.peer)` +- new: `peer.peer` + +This covers lines 284, 522, 523. + +- [ ] **Step 15.2:** Edit the `EnginePeer(component.peers[0].peer)` sites at lines 404, 416, 417. + +Use Edit with `replace_all=true`: +- old: `EnginePeer(component.peers[0].peer)` +- new: `component.peers[0].peer` + +- [ ] **Step 15.3:** Verify no other `EnginePeer(` wraps around `.peer` accesses remain on `RenderedChannelParticipant`. Run: +``` +grep -n 'EnginePeer(.*\.peer)' submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift +``` +Confirm remaining matches are on non-RCP types (e.g., some other context-derived peer). + +--- + +## Task 16: Consumer — StoryContentLiveChatComponent.swift + +**File:** `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift` + +- [ ] **Step 16.1:** Line 370 — DROP `._asPeer()` in constructor. + +Before: +```swift +peer: author._asPeer() +``` +After: +```swift +peer: author +``` +(`author` is `EnginePeer` — confirmed by the surrounding code that uses `author.id` and by the `chatPeer` signal's return type.) + +--- + +## Task 17: Consumer — ChatControllerAdminBanUsers.swift + +**File:** `submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift` + +- [ ] **Step 17.1:** Line 226 — ADD-WRAP constructor. + +Before: +```swift +let peer = author +renderedParticipants.append(RenderedChannelParticipant( + participant: participant, + peer: peer +)) +``` +After: +```swift +let peer = author +renderedParticipants.append(RenderedChannelParticipant( + participant: participant, + peer: EnginePeer(peer) +)) +``` +(Confirmed `author` is raw `Peer` via `presentMultiBanMessageOptions(... authors: [Peer], ...)` signature on line 45.) + +- [ ] **Step 17.2:** Line 372 — DROP `._asPeer()` in constructor. + +Before: +```swift +peer: authorPeer._asPeer() +``` +After: +```swift +peer: authorPeer +``` +(Confirmed `authorPeer` is `EnginePeer?` at line 327 via `engine.data.get(Peer.Peer(id:))` signal; already guard-unwrapped.) + +- [ ] **Step 17.3:** Line 757 — DROP `._asPeer()` in constructor. + +Same edit pattern as 17.2: `peer: authorPeer._asPeer()` → `peer: authorPeer`. + +--- + +## Task 18: Full build verification + +- [ ] **Step 18.1:** Run the full build with `--continueOnError`. + +```sh +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError +``` + +Expected: build success. First-pass-clean is the goal (wave-39 pattern applies — classification is exact, migration is mechanical, no inference-bearing return types). + +If the build fails, expect errors only in files in this plan. Any error outside the plan's file list is either: +- a pre-existing unrelated WIP (e.g., `ChatMessageTransitionNode.swift`) — not a wave-41 issue +- a genuine miss in pre-flight classification — record which file, update the plan, and re-run + +For each error in wave-41 files: +1. Read the error +2. Classify: is it a shape we mis-identified (ZERO that's not actually transparent) or a new shape (dict subscript, function arg to a `Peer`-typed param, etc.)? +3. Apply the appropriate fix (`._asPeer()` if raw Peer needed; unwrap the wrap if EnginePeer needed) +4. Re-run the build + +Budget: 1–3 build iterations. + +- [ ] **Step 18.2:** Post-build grep verification. + +Run these greps and confirm they return only the expected residual matches: + +```sh +grep -rn 'EnginePeer(participant\.peer)' submodules/ --include='*.swift' | grep -v submodules/TelegramCore/ | grep -v submodules/Postbox/ +``` +Expected: empty. + +```sh +grep -rn 'EnginePeer(new\.peer)' submodules/ --include='*.swift' | grep -v submodules/TelegramCore/ +``` +Expected: empty. + +```sh +grep -rn 'participant\.peer as\? TelegramUser' submodules/ --include='*.swift' +``` +Expected: empty. + +```sh +grep -n 'public let peer:' submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift +``` +Expected: `public let peer: EnginePeer`. + +--- + +## Task 19: Commit + +- [ ] **Step 19.1:** Stage only wave-41 files (explicitly enumerate — wave-39 lesson). + +```sh +git status --short +``` + +Inspect the output. Only wave-41 files should appear as modified. If pre-existing WIP (e.g., `submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift`) is also modified, do NOT include it in the commit. + +```sh +git add \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift \ + submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift \ + submodules/PeerInfoUI/Sources/ChannelAdminsController.swift \ + submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift \ + submodules/PeerInfoUI/Sources/ChannelMembersController.swift \ + submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift \ + submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift \ + submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift \ + submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift \ + submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift \ + submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift \ + submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift \ + submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift \ + docs/superpowers/specs/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration-design.md \ + docs/superpowers/plans/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration.md +``` + +(Add any additional files the build iterations surfaced.) + +Run `git status --short` and confirm only staged wave-41 files are green, and any unrelated WIP is still marked as unstaged. + +- [ ] **Step 19.2:** Commit. + +```sh +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 41 + +Migrate RenderedChannelParticipant.peer from Postbox `Peer` to +TelegramCore `EnginePeer`. 27 files touched: 10 TelegramCore +(1 struct + 9 files with constructor wraps) + 17 consumer files. + +Drops the 2 Shape-C wraps installed by wave 39 (ChannelMembersController +and ChannelBlacklistController) plus ~37 additional EnginePeer(...) / +._asPeer() bridges across the consumer surface. Net ~-14 bridges +after the 16 TelegramCore-internal EnginePeer(peer) wraps and the 7 +consumer ADD-WRAP constructor sites. RCP.peers and RCP.presences +dictionaries remain Postbox-typed (deferred). +EOF +)" +``` + +- [ ] **Step 19.3:** Confirm commit landed and working tree is clean except for pre-existing WIP. + +```sh +git status --short +git log -1 --oneline +``` + +--- + +## Task 20: Log the wave outcome + +- [ ] **Step 20.1:** Append wave 41 entry to `docs/superpowers/postbox-refactor-log.md`. + +Format (matching prior wave entries): + +```markdown +## Wave 41 outcome — RenderedChannelParticipant.peer: Peer → EnginePeer (2026-04-24) + +Landed as commit ``. 27 files / ~45 site edits / net ~-14 bridges. + +**Shape distribution:** +- TelegramCore: 16 constructor sites wrapped with `EnginePeer(peer)` across 9 files + struct field migrated in ChannelParticipants.swift +- Consumers: ~32 DROP (EnginePeer/._asPeer unwraps), 9 CAST (as? TelegramUser → if case let .user), 3 ADD-ASPEER, 7 ADD-WRAP constructor sites + +**First-pass-clean:** . Extends wave-39 lesson: first-pass-clean +is achievable when classification is exact and all patterns are mechanical. + +**Ratchet economics:** drops 2 wave-39 Shape-C wraps +(ChannelMembersController:707, ChannelBlacklistController:381) and installs 7 ADD-WRAP +consumer constructor sites as ratchet markers for a future +`RenderedChannelParticipant.peers: [PeerId: Peer] → [EnginePeer.Id: EnginePeer]` wave. + +**Spec:** `docs/superpowers/specs/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration-design.md`. +**Plan:** `docs/superpowers/plans/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration.md`. +``` + +- [ ] **Step 20.2:** Update the `project_postbox_refactor_next_wave.md` memory file with the wave 41 outcome and the wave 42 candidate (likely `PeerInfoScreenData.peer → EnginePeer`). + +- [ ] **Step 20.3:** Commit docs updates. + +```sh +git add docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: log wave 41 outcome +EOF +)" +``` diff --git a/docs/superpowers/plans/2026-04-24-sendaspeer-engine-peer-migration.md b/docs/superpowers/plans/2026-04-24-sendaspeer-engine-peer-migration.md new file mode 100644 index 0000000000..56c804e215 --- /dev/null +++ b/docs/superpowers/plans/2026-04-24-sendaspeer-engine-peer-migration.md @@ -0,0 +1,666 @@ +# Wave 35: `SendAsPeer.peer: Peer → EnginePeer` Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate the public field `SendAsPeer.peer` from the Postbox `Peer` protocol to the TelegramCore `EnginePeer` enum in a single atomic commit. Drops 3 `._asPeer()` bridges at construction sites, collapses 6 redundant `EnginePeer(peer.peer)` wraps, rewrites 1 `peer.peer as? TelegramChannel` downcast to an enum pattern, and adds `EnginePeer(channel)` wraps at 2 raw-`TelegramChannel` construction sites. No outflow `._asPeer()` bridges need to be added for this wave (unlike wave 34's `ContactListPeer.peer(peer:)` bridge). + +**Architecture:** One atomic commit. The field-type change is necessarily atomic (half-migrated SendAsPeer doesn't compile), so all edits land together. TelegramCore's `_internal_*SendAsAvailablePeers` functions keep `import Postbox` — only `SendAsPeer`'s public surface changes. No new wrappers, no new typealiases. The manual `==` body is replaced with synthesized Equatable (EnginePeer is Equatable). + +**Tech Stack:** Swift, Bazel build via Make.py wrapper. No tests — verification is build success + targeted grep checks. + +**Spec:** `docs/superpowers/specs/2026-04-24-sendaspeer-engine-peer-migration-design.md` + +--- + +## File Structure + +**Modified files (7 expected — 1 TelegramCore + 6 consumer. Plus 2 "verify no-edit" files.)** + +| File | Edit count | Category | +|---|---|---| +| `submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift` | ~7 spot edits (struct change + 4 constructor wraps + drop manual `==`) | α | +| `submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift` | ~5 (1 cast rewrite + 4 wrap drops) | γ | +| `submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift` | 3 (1 bridge-drop + 2 EnginePeer wraps on raw channel) | δ | +| `submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift` | 1 (bridge-drop) | δ | +| `submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift` | 1 (wrap collapse) | δ | +| `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift` | ~4 (1 bridge-drop + 1 flatMap simplify + 1 map simplify) | δ | + +**Verify-only (no edits expected):** +| File | Reason | +|---|---| +| `submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift` | Holds `[SendAsPeer]?` at collection level, no `.peer` access. | +| `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift` | Passes `currentSendAsPeer` through to `ChatSendAsPeerListContextItem` which keeps taking `[SendAsPeer]`. | + +**EnginePeer enum case mapping (used in cast rewrite):** + +| Postbox concrete | EnginePeer case | +|---|---| +| `TelegramChannel` | `.channel(TelegramChannel)` | +| `TelegramGroup` | `.legacyGroup(TelegramGroup)` | +| `TelegramUser` | `.user(TelegramUser)` | + +--- + +## Task 1: Edit `SendAsPeers.swift` — struct definition + constructor wraps + +**Files:** +- Modify: `submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift` + +Foundational change. Without it, none of the consumer edits compile. + +- [ ] **Step 1.1: Update the SendAsPeer struct field, init parameter, and drop manual `==`** + +Edit: + +```swift +// OLD +public struct SendAsPeer: Equatable { + public let peer: Peer + public let subscribers: Int32? + public let isPremiumRequired: Bool + + public init(peer: Peer, subscribers: Int32?, isPremiumRequired: Bool) { + self.peer = peer + self.subscribers = subscribers + self.isPremiumRequired = isPremiumRequired + } + + public static func ==(lhs: SendAsPeer, rhs: SendAsPeer) -> Bool { + return lhs.peer.isEqual(rhs.peer) && lhs.subscribers == rhs.subscribers && lhs.isPremiumRequired == rhs.isPremiumRequired + } +} +``` + +```swift +// NEW +public struct SendAsPeer: Equatable { + public let peer: EnginePeer + public let subscribers: Int32? + public let isPremiumRequired: Bool + + public init(peer: EnginePeer, subscribers: Int32?, isPremiumRequired: Bool) { + self.peer = peer + self.subscribers = subscribers + self.isPremiumRequired = isPremiumRequired + } +} +``` + +Use the Edit tool with the OLD block as `old_string` and the NEW block as `new_string`. Swift synthesizes Equatable for structs where every stored property is Equatable: `EnginePeer` is Equatable, `Int32?` is Equatable, `Bool` is Equatable — so the manual `==` is no longer needed. + +- [ ] **Step 1.2: Wrap raw Postbox `Peer` values at the four constructor sites** + +Sites at lines 64, 170, 236, 330. Each binds a raw Postbox `Peer` (from `transaction.getPeer(peerId)` or `peers.map { ... }`) and passes it to the `SendAsPeer(peer: ...)` init. Wrap each with `EnginePeer(...)`. + +Edit (line 64, inside `_internal_cachedPeerSendAsAvailablePeers`, cache-hit branch): + +```swift +// OLD + peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id))) +``` + +```swift +// NEW + peers.append(SendAsPeer(peer: EnginePeer(peer), subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id))) +``` + +Edit (line 170, inside `_internal_peerSendAsAvailablePeers`, network-response map): + +```swift +// OLD + return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) } +``` + +```swift +// NEW + return peers.map { SendAsPeer(peer: EnginePeer($0), subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) } +``` + +Edit (line 236, inside `_internal_cachedLiveStorySendAsAvailablePeers`, cache-hit branch): + +```swift +// OLD + peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id))) +``` + +```swift +// NEW + peers.append(SendAsPeer(peer: EnginePeer(peer), subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id))) +``` + +Note: lines 64 and 236 have identical text. If you prefer `replace_all=true`, do a grep first to confirm the count is exactly 2, then apply once. + +Edit (line 330, inside `_internal_liveStorySendAsAvailablePeers`, network-response map): + +```swift +// OLD + return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) } +``` + +```swift +// NEW + return peers.map { SendAsPeer(peer: EnginePeer($0), subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) } +``` + +Same remark as above: lines 170 and 330 are identical — one `replace_all=true` covers both if the count is exactly 2. + +- [ ] **Step 1.3: Verify** — read the updated file and confirm: + - The struct's `peer` field is now `EnginePeer` + - The init parameter is `peer: EnginePeer` + - Manual `==` has been removed + - All 4 constructor sites wrap with `EnginePeer(...)` + - `peer.peer.id` accesses inside the caching loops (lines 87, 90, 259, 262) remain unchanged (`EnginePeer.id` typealias to `PeerId` keeps them valid) + +Do not commit yet. + +--- + +## Task 2: Edit `ChatSendAsPeerListContextItem.swift` — cast rewrite + wrap collapse + +**Files:** +- Modify: `submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift` + +1 Postbox-concrete downcast rewrite + 4 `EnginePeer(peer.peer)` wrap drops. + +- [ ] **Step 2.1: Rewrite the `peer.peer as? TelegramChannel` downcast at line 73** + +Edit: + +```swift +// OLD + } else if let subscribers = peer.subscribers { + if let peer = peer.peer as? TelegramChannel { + if case .broadcast = peer.info { +``` + +```swift +// NEW + } else if let subscribers = peer.subscribers { + if case let .channel(channel) = peer.peer { + if case .broadcast = channel.info { +``` + +Note: the original `if let peer = peer.peer as? TelegramChannel` shadows the outer `peer: SendAsPeer` loop variable. The rewrite uses `channel` to avoid shadowing. Any subsequent uses of `peer.info`, `peer.flags`, etc. inside the inner `if let peer = ...` block must be renamed to `channel.*`. + +Read lines 70–90 before editing to see the full extent of the shadowed-`peer` scope, and ensure every reference to `peer.info` (and any sibling field access like `peer.flags`, `peer.username`, etc.) within the inner block is rewritten to `channel.*`. The snippet above captures the only `peer.info` site from the inventory. + +- [ ] **Step 2.2: Drop `EnginePeer(peer.peer)` wraps at lines 89, 110, 116, 121** + +The field `peer.peer` is now `EnginePeer`, so `EnginePeer(peer.peer)` becomes a type error. Drop the wrap. + +Read the full lines first to confirm each site's shape. Expected patterns (edit one at a time with enough surrounding context to make each unique — the four sites likely differ in surrounding tokens): + +For each of the four sites, the pattern to eliminate is `EnginePeer(peer.peer)` → `peer.peer`. Example: + +```swift +// OLD + let title = EnginePeer(peer.peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder) +``` + +```swift +// NEW + let title = peer.peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder) +``` + +Identify each of the four sites (lines 89, 110, 116, 121) by reading the file, then apply one Edit per site using enough surrounding context (usually 1–2 tokens before/after the `EnginePeer(peer.peer)` subexpression) to make the `old_string` unique. + +If all four lines reduce to the same substring pattern (e.g., `EnginePeer(peer.peer)` as a standalone subexpression), `replace_all=true` on the substring `EnginePeer(peer.peer)` → `peer.peer` is safe — but **first** grep to confirm the count is exactly 4 and no other meaning is captured. + +Run before: `grep -cE "EnginePeer\(peer\.peer\)" submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift` + +Expected: 4. + +- [ ] **Step 2.3: Verify** — grep: + +Run: `grep -nE "peer\.peer\s+(as\?|is)\s+Telegram|EnginePeer\(peer\.peer\)" submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift` + +Expected: zero matches. + +--- + +## Task 3: Edit `ChatControllerLoadDisplayNode.swift` — bridge-drop + raw-channel wraps + +**Files:** +- Modify: `submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift` + +1 `._asPeer()` bridge-drop at line 772 + 2 `EnginePeer(channel)` wraps for raw `TelegramChannel` at lines 805 and 823. + +- [ ] **Step 3.1: Bridge-drop at line 772** + +Edit: + +```swift +// OLD + return SendAsPeer(peer: peer._asPeer(), subscribers: nil, isPremiumRequired: false) +``` + +```swift +// NEW + return SendAsPeer(peer: peer, subscribers: nil, isPremiumRequired: false) +``` + +Verification: the surrounding signal chain binds `peer` as `EnginePeer` (from `context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: ...))`). The `._asPeer()` bridge is no longer needed. + +If the line text differs from the OLD block above (e.g., different field order or trailing arguments), read the file around line 772 and adjust the `old_string` to match byte-for-byte before editing. + +- [ ] **Step 3.2: Wrap raw `TelegramChannel` at line 805** + +Read lines 800–812 to see the bound `channel` variable. The construction site should be `SendAsPeer(peer: channel, ...)` where `channel: TelegramChannel` is raw Postbox. + +Edit: + +```swift +// OLD + SendAsPeer(peer: channel, subscribers: subscribers, isPremiumRequired: isPremiumRequired) +``` + +```swift +// NEW + SendAsPeer(peer: EnginePeer(channel), subscribers: subscribers, isPremiumRequired: isPremiumRequired) +``` + +If the surrounding context differs (different field values), match the actual line text when writing `old_string`. + +- [ ] **Step 3.3: Wrap raw `TelegramChannel` at line 823** + +Same pattern as Step 3.2. Read lines 818–830 first, identify the `SendAsPeer(peer: channel, ...)` construction site, and wrap `channel` with `EnginePeer(...)`. + +If the line text at 805 and 823 is identical, `replace_all=true` on the substring `SendAsPeer(peer: channel,` → `SendAsPeer(peer: EnginePeer(channel),` covers both. **First** grep to confirm the count: + +Run before: `grep -cE "SendAsPeer\(peer: channel," submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift` + +Expected: 2. + +- [ ] **Step 3.4: Verify** — grep: + +Run: `grep -nE "SendAsPeer\(peer:\s+\w+\._asPeer\(\)|SendAsPeer\(peer:\s+channel," submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift` + +Expected: zero matches. Lines 792, 826, 835, 844 retaining `.peer.id` accesses are expected and correct. + +--- + +## Task 4: Edit `ChatTextInputPanelComponent.swift` — bridge-drop + +**Files:** +- Modify: `submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift` + +1 `._asPeer()` bridge-drop. + +- [ ] **Step 4.1: Bridge-drop at line 847** + +Read lines 843–853 to confirm the surrounding signal chain and the type of `sendAsConfiguration.currentPeer` (expected: `EnginePeer`). + +Edit: + +```swift +// OLD + let sendAsPeers = [SendAsPeer(peer: sendAsConfiguration.currentPeer._asPeer(), subscribers: nil, isPremiumRequired: false)] +``` + +```swift +// NEW + let sendAsPeers = [SendAsPeer(peer: sendAsConfiguration.currentPeer, subscribers: nil, isPremiumRequired: false)] +``` + +If the actual line text wraps across multiple lines or uses different field values, match the real text byte-for-byte when writing `old_string`. + +- [ ] **Step 4.2: Verify** — grep: + +Run: `grep -nE "SendAsPeer\(peer:.*\._asPeer\(\)" submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift` + +Expected: zero matches. + +--- + +## Task 5: Edit `ChatTextInputPanelNode.swift` — wrap collapse + +**Files:** +- Modify: `submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift` + +1 `EnginePeer(peer)` wrap collapse at line 1625. + +- [ ] **Step 5.1: Collapse `EnginePeer(peer)` wrap** + +Read lines 1615–1630 to see the full context. `peer` is bound from a preceding `var currentPeer = sendAsPeers.first(where: { $0.peer.id == ... })?.peer` (lines 1620–1622). After migration, `.peer` returns `EnginePeer`, so `EnginePeer(peer)` on an `EnginePeer` is a type error. + +Exact edit depends on the actual line text. Example shape: + +```swift +// OLD (at or near line 1625) + let enginePeer = EnginePeer(peer) +``` + +```swift +// NEW + let enginePeer = peer +``` + +Read lines 1623–1628 first and write the Edit with byte-accurate `old_string`. If the bound variable is then used as `enginePeer.displayTitle(...)`, consider whether the rename can be eliminated entirely (e.g., rename `peer` uses downstream), but prefer the minimal edit for commit clarity. + +Lines 1616, 1620, 1622, 2948, 5370 should remain unchanged — they perform `.peer.id` comparisons or `.first(where:)` lookups that work identically on `[SendAsPeer]` with `EnginePeer`-typed `.peer`. + +- [ ] **Step 5.2: Verify** — grep: + +Run: `grep -nE "EnginePeer\(peer\)" submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift` + +Expected: zero matches. If any remain, inspect each — they may be unrelated wraps on non-SendAsPeer-sourced `peer` variables (in which case they must stay). + +--- + +## Task 6: Edit `StoryItemSetContainerViewSendMessage.swift` — multi-site cleanup + +**Files:** +- Modify: `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift` + +1 bridge-drop + 1 flatMap simplify + 1 map simplify. Many other `.peer.id` / `.peer` accesses remain unchanged. + +- [ ] **Step 6.1: Bridge-drop at line 249** + +Read lines 244–254 to confirm `accountPeer` is typed as `EnginePeer` upstream. + +Edit: + +```swift +// OLD + availablePeers.append(SendAsPeer( + peer: accountPeer._asPeer(), + subscribers: nil, + isPremiumRequired: false + )) +``` + +```swift +// NEW + availablePeers.append(SendAsPeer( + peer: accountPeer, + subscribers: nil, + isPremiumRequired: false + )) +``` + +If the actual layout (whitespace, line breaks) differs from the OLD block, match the real text byte-for-byte when writing `old_string`. + +- [ ] **Step 6.2: Simplify flatMap at line 4080** + +`EnginePeer.init` as a function reference expects a raw `Peer` and returns `EnginePeer`. After migration, `sendAsPeer?.peer` is already `EnginePeer?`, so `.flatMap(EnginePeer.init)` is both unnecessary and a type error. + +Edit: + +```swift +// OLD + myPeer: (sendAsPeer?.peer).flatMap(EnginePeer.init), +``` + +```swift +// NEW + myPeer: sendAsPeer?.peer, +``` + +Read lines 4078–4082 first to confirm the surrounding labeled-argument layout and match byte-for-byte. + +- [ ] **Step 6.3: Simplify map at line 4081** + +`.map({ EnginePeer($0.peer) })` wraps each already-`EnginePeer` value in `EnginePeer(...)` — a type error. Drop the wrap. + +Edit: + +```swift +// OLD + availableSendAsPeers: component.isEmbeddedInCamera ? [] : (self.sendAsData?.availablePeers.map({ EnginePeer($0.peer) }) ?? []), +``` + +```swift +// NEW + availableSendAsPeers: component.isEmbeddedInCamera ? [] : (self.sendAsData?.availablePeers.map({ $0.peer }) ?? []), +``` + +Read lines 4079–4083 first to confirm the exact line text. + +- [ ] **Step 6.4: Verify** — grep: + +Run: `grep -nE "SendAsPeer\(peer:.*\._asPeer\(\)|EnginePeer\(\$0\.peer\)|\(sendAsPeer\?\.peer\)\.flatMap\(EnginePeer\.init\)" submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift` + +Expected: zero matches. + +Retained-as-is accesses (inventory-verified correct after migration): `.peer.id` at lines 254, 688, 4088, 4089, 4327, 4333, 4340, 4356, 4372; optional chaining at 4050, 4068, 4069. These should NOT be edited. + +--- + +## Task 7: Verify "no-edit" consumer files + +**Files:** +- Read: `submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift` +- Read: `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift` + +Sanity-check: confirm neither file contains `.peer as?`/`is`, `EnginePeer(.peer)`, or `._asPeer()` patterns tied to SendAsPeer. If any such pattern is found, fold the fix into the relevant task above before the build pass. + +- [ ] **Step 7.1: Grep ChatPresentationInterfaceState.swift** + +Run: `grep -nE "SendAsPeer|sendAsPeers" submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift` + +Expected shape: field declaration, init param, assignment, equality comparison, `updatedSendAsPeers(_:)` method — all at the `[SendAsPeer]?` collection level. No `.peer` field access. + +- [ ] **Step 7.2: Grep StoryItemSetContainerComponent.swift** + +Run: `grep -nE "SendAsPeer|currentSendAsPeer|\.peer\b" submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift | grep -iE "sendAsPeer|\.peer"` + +Read lines 3056–3072 to confirm `sendMessageContext.currentSendAsPeer` is only passed through to `ChatSendAsPeerListContextItem` (which keeps `[SendAsPeer]`) or accessed for `.peer.id` comparisons — neither requires an edit. + +If the verification shows an edit is needed, add the edit as an additional step under the relevant Task 2–6. Do not edit here silently. + +--- + +## Task 8: Build verification (first pass) + +- [ ] **Step 8.1: Run the full build with `--continueOnError`** + +Run: + +```bash +source ~/.zshrc 2>/dev/null && python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError 2>&1 | tee /tmp/wave35-build.log +``` + +Expected outcome: ideally clean. Realistic outcome: 0–5 errors at sites the inventory missed. + +- [ ] **Step 8.2: Triage build errors** + +Likely error patterns and their fixes: + +| Error | Fix | +|---|---| +| `cannot convert value of type 'EnginePeer' to expected argument type 'Peer'` at site passing `peer.peer` | Add `._asPeer()` bridge: `peer.peer._asPeer()` | +| `cannot convert value of type 'Peer' to expected argument type 'EnginePeer'` at `SendAsPeer(peer: ...)` | Add wrap: `SendAsPeer(peer: EnginePeer(), ...)` | +| `value of type 'EnginePeer' has no member 'isEqual'` | Replace with `==` | +| `pattern of type 'TelegramChannel' cannot match values of type 'EnginePeer'` | Missed C2 — rewrite to `if case .channel(let channel) = peer.peer` form | +| `cannot invoke initializer for type 'EnginePeer' with an argument list of type '(EnginePeer)'` | Missed wrap collapse — drop `EnginePeer(...)` | +| `extraneous argument label 'peer:' in call` or similar on `SendAsPeer(...)` | Check that the construction arg is `EnginePeer`, not raw — add `EnginePeer(...)` wrap | + +For each error, identify the file:line, apply the appropriate fix, and re-run the build until clean. + +- [ ] **Step 8.3: Iterate to clean build** + +Re-run the build after each batch of fixes. The wave is complete when the build returns 0 errors for the targeted configuration. + +If 10+ unexpected errors surface, halt and reassess: the inventory was significantly incomplete and the wave may need to be split into pre-cleanup commits. Discuss with user before continuing. + +--- + +## Task 9: Post-build grep validations + +- [ ] **Step 9.1: Bridge-drop validation** + +Run: + +```bash +grep -rn "SendAsPeer(peer:.*\._asPeer()" submodules/ --include="*.swift" | grep -v "^submodules/TelegramCore/" | grep -v "^submodules/Postbox/" +``` + +Expected: zero hits. If any remain, those are missed bridge-drops — fix and re-run Task 8. + +- [ ] **Step 9.2: Wrap-collapse validation** + +Run: + +```bash +for f in submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift \ + submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift \ + submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift \ + submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift \ + submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift; do + echo "=== $f ===" + grep -nE "EnginePeer\(peer\.peer\)|EnginePeer\(\$0\.peer\)|\(sendAsPeer\?\.peer\)\.flatMap\(EnginePeer\.init\)" "$f" +done +``` + +Expected: zero hits across all 5 files. + +- [ ] **Step 9.3: C2 cast validation** + +Run: + +```bash +grep -nE "peer\.peer\s+(as\?|is)\s+Telegram" submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift +``` + +Expected: zero hits. + +- [ ] **Step 9.4: Construction-site validation** + +Ensure all `SendAsPeer(peer: ...)` construction sites outside TelegramCore provide `EnginePeer`: + +```bash +grep -rnE "SendAsPeer\(peer:" submodules/ --include="*.swift" | grep -v "^submodules/TelegramCore/" +``` + +Inspect each hit. Expected forms: `SendAsPeer(peer: , ...)` or `SendAsPeer(peer: EnginePeer(), ...)`. Anything of the form `SendAsPeer(peer: , ...)` is a miss — fix. + +If any of the validations fail, return to Task 8 to fix. + +--- + +## Task 10: Atomic commit + memory + log update + +- [ ] **Step 10.1: Stage and review** + +Run: + +```bash +git status --short +git diff --stat +``` + +Confirm exactly 6 modified Swift files (1 TelegramCore + 5 consumer — or 7 if Task 7 surfaced a needed edit). Files expected: +- `submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift` +- `submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift` +- `submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift` +- `submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift` +- `submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift` +- `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift` + +WIP from earlier (`build-system/bazel-rules/sourcekit-bazel-bsp`, `ChatListFilterPresetController.swift`, `ChatListFilterPresetListController.swift`, untracked `build-system/tulsi/` / `submodules/TgVoip/` / `third-party/libx264/`) should NOT be staged. + +The `docs/superpowers/plans/2026-04-22-claude-md-reorganization.md` untracked file should ALSO remain unstaged. + +- [ ] **Step 10.2: Stage only the wave-35 files** + +Run: + +```bash +git add submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift \ + submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift \ + submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift \ + submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift \ + submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift \ + submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +``` + +If Task 7 surfaced an additional file, append it here. + +- [ ] **Step 10.3: Commit** + +Run: + +```bash +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 35: SendAsPeer.peer Peer -> EnginePeer + +Migrates the public field `SendAsPeer.peer` from the Postbox `Peer` +protocol to the TelegramCore `EnginePeer` enum. Internal +`_internal_*SendAsAvailablePeers` bodies keep `import Postbox` (they still +call `postbox.transaction`) and wrap raw peer values with `EnginePeer(peer)` +at the SendAsPeer constructor sites. Manual `==` body dropped in favor of +synthesized Equatable. + +Consumer-side cascade in 5 files: + - 3 `._asPeer()` bridge-drops at SendAsPeer constructor sites + - 6 redundant `EnginePeer(peer.peer)` / `EnginePeer($0.peer)` wrap + drops (the field is now EnginePeer, so the wrap fails to compile) + - 1 `peer.peer as? TelegramChannel` downcast rewritten to + `if case let .channel(channel) = peer.peer` enum-pattern form + - 2 `EnginePeer(channel)` wraps added where raw `TelegramChannel` is + passed into `SendAsPeer(peer: ...)` + - 1 `(sendAsPeer?.peer).flatMap(EnginePeer.init)` simplified to + `sendAsPeer?.peer` (already `EnginePeer?`) + +Files modified: + submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift + submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift + submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift + submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift + submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift + submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift + +Plan: docs/superpowers/plans/2026-04-24-sendaspeer-engine-peer-migration.md +Spec: docs/superpowers/specs/2026-04-24-sendaspeer-engine-peer-migration-design.md + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 10.4: Update CLAUDE.md wave counter** + +Edit `CLAUDE.md` to bump the "Waves landed so far" line from "34 waves" to "35 waves" and update the "as of" date if the commit lands after 2026-04-24. + +- [ ] **Step 10.5: Append wave outcome to the postbox-refactor-log** + +Append a "Wave 35 outcome" section to `docs/superpowers/postbox-refactor-log.md` documenting: +- Actual files touched and edit counts vs. plan +- Any inventory undercounts surfaced by Task 8 +- Any lessons learned (e.g., whether the flatMap/map simplifications were actually type-required or whether they could have been left as redundant-but-compiling wraps) + +Keep concise. + +- [ ] **Step 10.6: Commit the docs update** + +Run: + +```bash +git add CLAUDE.md docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: add wave 35 outcome (SendAsPeer.peer Peer→EnginePeer) + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 10.7: Update the next-wave memory** + +Update `/Users/isaac/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`: +- Add wave 35 to the "Latest commits" section +- Move SendAsPeer migration from "Wave 34+ candidates → Downstream Peer-typed APIs" to landed +- Record the inventory undercount ratio (actual-files-touched ÷ pre-flight-file-count) for calibration of future Peer-typed-API waves +- Update the "Recommended wave 35" section to reflect the new wave 36 recommendation. Candidates to promote: `makePeerInfoController` (largest Peer-typed-API remaining), `ContactListPeer.peer(peer:)` case payload, `canSendMessagesToPeer(_:)` parameter, accountManager-side engine path, Shape-C resourceData module pick + +Use the Edit tool on the memory file. No git commit needed (memory lives outside the repo). + +--- + +## Risks and notes + +- **Inner `peer` shadowing in ChatSendAsPeerListContextItem:73.** The original `if let peer = peer.peer as? TelegramChannel` shadows the outer `peer: SendAsPeer` loop variable. The rewrite uses `channel` to avoid shadowing. Verify every reference to `peer.info` (and any sibling field access) within the old inner-if scope is updated to `channel.*` — Step 2.1's instructions cover this, but it's easy to miss a field reference. +- **`replace_all` correctness.** Whenever the plan suggests `replace_all=true`, verify the count first via grep. If the count is unexpected, revert to per-site Edits with surrounding context. +- **Inventory undercount.** Wave 34 undercounted by ~30%. The Explore agent for wave 35 explicitly included `.peer as?`/`is`/outflow-helper patterns, so the expected ratio is lower, but budget for 1–3 inventory-missed sites surfacing in Task 8. +- **Name collisions (do NOT touch).** `[EnginePeer]` arrays in `LiveStreamSettingsScreen.swift`, `ShareWithPeersScreen.swift`, and `ChatSendStarsScreen.swift` named `sendAsPeers` / `availableSendAsPeers` are unrelated. `ChatPanelInterfaceInteraction` callbacks named `openSendAsPeer` take `(ASDisplayNode, ContextGesture?)`, not `SendAsPeer`. `initialSendAsPeerId` parameters are `PeerId`-typed. If Task 8 surfaces errors in any of these files, the fix likely indicates a wrong cascade from a real SendAsPeer site — do NOT migrate those files as part of this wave. +- **WIP isolation.** Pre-existing modifications to `ChatListFilterPresetController.swift`, `ChatListFilterPresetListController.swift`, the `sourcekit-bazel-bsp` submodule marker, and untracked `build-system/tulsi/` / `submodules/TgVoip/` / `third-party/libx264/` / `docs/superpowers/plans/2026-04-22-claude-md-reorganization.md` are user WIP — do NOT stage them. Use the explicit `git add ` form in Step 10.2. diff --git a/docs/superpowers/plans/2026-04-25-clearpeerhistory-openclearhistory-engine-peer.md b/docs/superpowers/plans/2026-04-25-clearpeerhistory-openclearhistory-engine-peer.md new file mode 100644 index 0000000000..1b847bd2d5 --- /dev/null +++ b/docs/superpowers/plans/2026-04-25-clearpeerhistory-openclearhistory-engine-peer.md @@ -0,0 +1,116 @@ +# Wave 54: ClearPeerHistory.init + openClearHistory `chatPeer: Peer → EnginePeer` + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Migrate the `chatPeer:` parameter type on both `ClearPeerHistory.init` and `openClearHistory` from `Peer` to `EnginePeer`. Closes wave-53's deferred sibling. + +**Wave shape:** Bundled method-signature migration (familiar from waves 41/44/47/50/53). Mechanical `as?`/`is` cluster on a single field, with EnginePeer.init boundary lifts at each call site. + +**Tech Stack:** Swift, Bazel, Make.py. + +--- + +## Pre-Flight Inventory (validated 2026-04-25) + +**2 files modified, 16 edits.** + +### File 1: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift` (PIS) + +| Line | Current | After | Note | +|---|---|---|---| +| 3213 | `func openClearHistory(... peer: Peer, chatPeer: Peer) {` | `... chatPeer: EnginePeer)` | type-site | +| 3230 | `EnginePeer(chatPeer).compactDisplayTitle` | `chatPeer.compactDisplayTitle` | drop wrap | +| 3232 | `EnginePeer(chatPeer).compactDisplayTitle` | `chatPeer.compactDisplayTitle` | drop wrap | +| 3251 | `EnginePeer(chatPeer).compactDisplayTitle` | `chatPeer.compactDisplayTitle` | drop wrap | +| 3269 | `EnginePeer(chatPeer).compactDisplayTitle` | `chatPeer.compactDisplayTitle` | drop wrap | +| 7416 | `init(... peer: Peer, chatPeer: Peer, cachedData: ...)` | `... chatPeer: EnginePeer, cachedData: ...` | type-site | +| 7421 | `} else if chatPeer is TelegramSecretChat {` | `} else if case .secretChat = chatPeer {` | conversion | +| 7425 | `} else if let group = chatPeer as? TelegramGroup {` | `} else if case let .legacyGroup(group) = chatPeer {` | conversion | +| 7436 | `} else if let channel = chatPeer as? TelegramChannel {` | `} else if case let .channel(channel) = chatPeer {` | conversion | +| 7464 | `if let user = chatPeer as? TelegramUser, user.botInfo != nil {` | `if case let .user(user) = chatPeer, user.botInfo != nil {` | conversion | + +`peer:` parameter stays Peer-typed in both functions: `openClearHistory` doesn't reference `peer` in its body; `ClearPeerHistory.init` uses only `peer.id == context.account.peerId` (line 7417), which works on Peer (and would also work on EnginePeer, but migrating it would require 6 boundary lifts at PISPBA call sites for no internal benefit). + +### File 2: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift` (PISPBA) + +| Line | Current | After | Note | +|---|---|---|---| +| 851 | `chatPeer: chatPeer._asPeer()` | `chatPeer: chatPeer` | drop wave-53 ADD | +| 857 | `chatPeer: user` | `chatPeer: EnginePeer(user)` | boundary lift (TelegramUser) | +| 1067 | `chatPeer: channel` | `chatPeer: EnginePeer(channel)` | boundary lift (TelegramChannel) | +| 1073 | `chatPeer: channel` | `chatPeer: EnginePeer(channel)` | boundary lift (TelegramChannel) | +| 1234 | `chatPeer: group` | `chatPeer: EnginePeer(group)` | boundary lift (TelegramGroup) | +| 1240 | `chatPeer: group` | `chatPeer: EnginePeer(group)` | boundary lift (TelegramGroup) | + +### Net accounting + +- **Drops:** 5 (4 `EnginePeer(chatPeer).compactDisplayTitle` + 1 `_asPeer()` bridge from wave 53). +- **Adds:** 5 boundary lifts (5 `EnginePeer(...)` wraps at PISPBA call sites). +- **Conversions:** 4 (`is`/`as?` → `case let`). +- **Type-site:** 2 (signature changes on PIS:3213 and PIS:7416). + +Net internal-bridge progress: `5 drops − 5 adds = 0 raw count`. But ratchet kills 4 internal display-call wraps (`EnginePeer(chatPeer).compactDisplayTitle` patterns) which is the hot path; only call-site boundary lifts remain. Closes wave-53's deferred ADD at PISPBA:851. + +--- + +## Tasks + +### Task 1: PIS signature edits + body wrap drops + +- [ ] **Step 1: Edit `openClearHistory` signature at PIS:3213** + +Replace `peer: Peer, chatPeer: Peer)` with `peer: Peer, chatPeer: EnginePeer)`. + +- [ ] **Step 2: Drop 4 `EnginePeer(chatPeer).compactDisplayTitle` wraps** + +`replace_all=true` of `EnginePeer(chatPeer).compactDisplayTitle` → `chatPeer.compactDisplayTitle`. (Only 4 occurrences in the file, all in `openClearHistory` body; verified by grep.) + +- [ ] **Step 3: Edit `ClearPeerHistory.init` signature at PIS:7416** + +Replace `peer: Peer, chatPeer: Peer, cachedData:` with `peer: Peer, chatPeer: EnginePeer, cachedData:`. + +- [ ] **Step 4: Convert PIS:7421 `is TelegramSecretChat`** + +Replace `} else if chatPeer is TelegramSecretChat {` with `} else if case .secretChat = chatPeer {`. + +- [ ] **Step 5: Convert PIS:7425 `as? TelegramGroup`** + +Replace `} else if let group = chatPeer as? TelegramGroup {` with `} else if case let .legacyGroup(group) = chatPeer {`. + +- [ ] **Step 6: Convert PIS:7436 `as? TelegramChannel`** + +Replace `} else if let channel = chatPeer as? TelegramChannel {` with `} else if case let .channel(channel) = chatPeer {`. + +- [ ] **Step 7: Convert PIS:7464 `as? TelegramUser`** + +Replace `if let user = chatPeer as? TelegramUser, user.botInfo != nil {` with `if case let .user(user) = chatPeer, user.botInfo != nil {`. + +### Task 2: PISPBA call-site lifts + bridge drop + +- [ ] **Step 1: Drop wave-53 `_asPeer()` bridge at PISPBA:851** + +Replace `chatPeer: chatPeer._asPeer()` with `chatPeer: chatPeer`. + +- [ ] **Step 2: Lift PISPBA:857 `chatPeer: user`** + +Replace `peer: user, chatPeer: user)` with `peer: user, chatPeer: EnginePeer(user))`. + +- [ ] **Step 3: Lift channel call sites (PISPBA:1067 + 1073)** + +`replace_all=true` of `chatPeer: channel` → `chatPeer: EnginePeer(channel)`. Verify exactly 2 hits flipped. + +- [ ] **Step 4: Lift group call sites (PISPBA:1234 + 1240)** + +`replace_all=true` of `chatPeer: group` → `chatPeer: EnginePeer(group)`. Verify exactly 2 hits flipped. + +### Task 3: Build verification + +- [ ] **Step 1: Run full build with `--continueOnError`.** + +Forecast 1 iteration. Risk: hidden `chatPeer` access on Peer-typed shape elsewhere (none expected — body audit complete). + +### Task 4: Commit + log + +- [ ] **Step 1: Commit wave with the two file paths explicitly.** +- [ ] **Step 2: Update `docs/superpowers/postbox-refactor-log.md` and the memory file.** +- [ ] **Step 3: Commit log.** diff --git a/docs/superpowers/plans/2026-04-25-peerinfo-enclosingpeer-engine-peer.md b/docs/superpowers/plans/2026-04-25-peerinfo-enclosingpeer-engine-peer.md new file mode 100644 index 0000000000..4e3fdf4a09 --- /dev/null +++ b/docs/superpowers/plans/2026-04-25-peerinfo-enclosingpeer-engine-peer.md @@ -0,0 +1,516 @@ +# Wave 50: enclosingPeer Peer? → EnginePeer? Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate the PeerInfo members chain's `enclosingPeer` field from raw Postbox `Peer?` to `EnginePeer?` (wave 50 of the Postbox → TelegramEngine refactor). + +**Architecture:** Cross-file private struct-field migration with stored-form ratchet. Edits stay inside `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/`. Replaces `as? TelegramChannel` / `as? TelegramGroup` casts with `case let .channel(...)` / `case let .legacyGroup(...)` (wave-41/45 idiom), drops `is TelegramChannel` checks for `case .channel = ...` (wave-41 always-false-warning fix), and removes 5 internal `_asPeer()` / `EnginePeer(...)` / `flatMap(EnginePeer.init)` bridges. The engine.data subscription at PIMP:354 already returns `EnginePeer?` — this wave closes the demote-then-promote ratchet. + +**Tech Stack:** Swift, Bazel via `Make.py`, no unit tests (per `CLAUDE.md`). Verification is the full-project debug-sim-arm64 build with `--continueOnError`. + +**Iteration budget:** 1–2 (target first-pass-clean; recent first-pass-clean streak: waves 42, 43*, 45, 46, 48, 49 — *wave 43 took 2 iterations). + +**Note on TDD:** This project has no unit tests (CLAUDE.md "No tests are used at the moment"). The standard TDD test-first cycle in the skill template does not apply. Each task instead writes the edits, then verifies via Bazel build + residue grep. + +--- + +## File Structure + +| File | Role | Changes | +|---|---|---| +| `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift` (PSMI) | List-item view-model + node | Type-change stored field + init param; 4 cast/is-check rewrites; 1 `flatMap(EnginePeer.init)` simplification | +| `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift` (PIMP) | Members-pane node + helpers | 3 func sigs + 1 stored field type-change; 4 cast/is-check rewrites; 1 `EnginePeer(...)` wrap drop; 2 `_asPeer()` drops | +| `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift` (PSPB) | Profile-items builder (non-settings members section) | 1 boundary `_asPeer()` drop at the call site that constructs the migrated init | + +No public-API ripple — `PeerInfoScreenMemberItem` and `PeerInfoMembersPaneNode` are local to the PeerInfoScreen module. + +--- + +## Task 1: PSMI.swift — type changes + cast/is-check rewrites + flatMap simplification + +**Files:** +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift` + +**Edits in this task:** 7 (1 stored-field type, 1 init-param type, 2 cast→case-let, 2 is→case, 1 flatMap simplification). + +- [ ] **Step 1: Change stored field type at line 23** + +Find: + +```swift + let enclosingPeer: Peer? +``` + +Replace with: + +```swift + let enclosingPeer: EnginePeer? +``` + +- [ ] **Step 2: Change init parameter type at line 34** + +Find: + +```swift + enclosingPeer: Peer?, +``` + +Replace with: + +```swift + enclosingPeer: EnginePeer?, +``` + +- [ ] **Step 3: Rewrite cast at line 152 (TelegramChannel)** + +Find: + +```swift + if let channel = item.enclosingPeer as? TelegramChannel, channel.hasPermission(.editRank) { +``` + +Replace with: + +```swift + if case let .channel(channel) = item.enclosingPeer, channel.hasPermission(.editRank) { +``` + +- [ ] **Step 4: Rewrite cast at line 154 (TelegramGroup)** + +Find: + +```swift + } else if let group = item.enclosingPeer as? TelegramGroup, !group.hasBannedPermission(.banEditRank) { +``` + +Replace with: + +```swift + } else if case let .legacyGroup(group) = item.enclosingPeer, !group.hasBannedPermission(.banEditRank) { +``` + +- [ ] **Step 5: Simplify flatMap at line 178** + +Find: + +```swift + let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: item.enclosingPeer.flatMap(EnginePeer.init), member: item.member) +``` + +Replace with: + +```swift + let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: item.enclosingPeer, member: item.member) +``` + +- [ ] **Step 6: Rewrite is-check at line 181** + +Find: + +```swift + if actions.contains(.promote) && item.enclosingPeer is TelegramChannel { +``` + +Replace with: + +```swift + if actions.contains(.promote), case .channel = item.enclosingPeer { +``` + +- [ ] **Step 7: Rewrite is-check at line 187** + +Find: + +```swift + if item.enclosingPeer is TelegramChannel { +``` + +Replace with: + +```swift + if case .channel = item.enclosingPeer { +``` + +--- + +## Task 2: PIMP.swift — signatures + stored field + body rewrites + demotion drops + +**Files:** +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift` + +**Edits in this task:** 11 (3 func sigs + 1 stored-field type + 4 cast/is rewrites + 1 EnginePeer wrap drop + 2 `_asPeer()` drops). + +- [ ] **Step 1: Change `func item(...)` signature at line 92** + +Find: + +```swift + func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { +``` + +Replace with: + +```swift + func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: EnginePeer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { +``` + +- [ ] **Step 2: Rewrite cast at line 113 (TelegramChannel, non-optional context)** + +Find: + +```swift + if let channel = enclosingPeer as? TelegramChannel, channel.hasPermission(.editRank) { +``` + +Replace with: + +```swift + if case let .channel(channel) = enclosingPeer, channel.hasPermission(.editRank) { +``` + +- [ ] **Step 3: Rewrite cast at line 115 (TelegramGroup, non-optional context)** + +Find: + +```swift + } else if let group = enclosingPeer as? TelegramGroup, !group.hasBannedPermission(.banEditRank) { +``` + +Replace with: + +```swift + } else if case let .legacyGroup(group) = enclosingPeer, !group.hasBannedPermission(.banEditRank) { +``` + +- [ ] **Step 4: Drop the `EnginePeer(...)` wrap at line 139** + +Find: + +```swift + let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: EnginePeer(enclosingPeer), member: member) +``` + +Replace with: + +```swift + let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: enclosingPeer, member: member) +``` + +`availableActionsForMemberOfPeer` takes `peer: EnginePeer?` (PeerInfoData.swift:2314); Swift auto-wraps the non-optional `enclosingPeer: EnginePeer` to optional. + +- [ ] **Step 5: Rewrite is-check at line 142 (non-optional context)** + +Find: + +```swift + if actions.contains(.promote) && enclosingPeer is TelegramChannel { +``` + +Replace with: + +```swift + if actions.contains(.promote), case .channel = enclosingPeer { +``` + +- [ ] **Step 6: Rewrite is-check at line 148 (non-optional context)** + +Find: + +```swift + if enclosingPeer is TelegramChannel { +``` + +Replace with: + +```swift + if case .channel = enclosingPeer { +``` + +- [ ] **Step 7: Change `preparedTransition` signature at line 271** + +Find: + +```swift +private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> PeerMembersListTransaction { +``` + +Replace with: + +```swift +private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: EnginePeer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> PeerMembersListTransaction { +``` + +- [ ] **Step 8: Change stored field type at line 293** + +Find: + +```swift + private var enclosingPeer: Peer? +``` + +Replace with: + +```swift + private var enclosingPeer: EnginePeer? +``` + +- [ ] **Step 9: Drop `_asPeer()` at line 361** + +Find: + +```swift + strongSelf.enclosingPeer = enclosingPeer._asPeer() +``` + +Replace with: + +```swift + strongSelf.enclosingPeer = enclosingPeer +``` + +- [ ] **Step 10: Drop `_asPeer()` at line 363** + +Find: + +```swift + strongSelf.updateState(enclosingPeer: enclosingPeer._asPeer(), state: state, presentationData: presentationData) +``` + +Replace with: + +```swift + strongSelf.updateState(enclosingPeer: enclosingPeer, state: state, presentationData: presentationData) +``` + +- [ ] **Step 11: Change `updateState` signature at line 442** + +Find: + +```swift + private func updateState(enclosingPeer: Peer, state: PeerInfoMembersState, presentationData: PresentationData) { +``` + +Replace with: + +```swift + private func updateState(enclosingPeer: EnginePeer, state: PeerInfoMembersState, presentationData: PresentationData) { +``` + +The pass-through call sites at PIMP:275, :276, :437, :438, :451, :485 require no edit — types flow through transparently. + +--- + +## Task 3: PSPB.swift — boundary lift at members-section call site + +**Files:** +- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift` + +**Edits in this task:** 1. + +- [ ] **Step 1: Drop `_asPeer()` at line 852** + +Find: + +```swift + items[.peerMembers]!.append(PeerInfoScreenMemberItem(id: member.id, context: .account(context), enclosingPeer: peer._asPeer(), member: member, isAccount: false, action: isAccountPeer ? { _ in +``` + +Replace with: + +```swift + items[.peerMembers]!.append(PeerInfoScreenMemberItem(id: member.id, context: .account(context), enclosingPeer: peer, member: member, isAccount: false, action: isAccountPeer ? { _ in +``` + +`peer` here is the closure-bound `EnginePeer` from the `data.peer` source pipeline (`PeerInfoScreenData.peer: EnginePeer?` post-wave-42, unwrapped to non-optional `EnginePeer` and being passed to a now-`EnginePeer?` param — auto-promotes to optional). + +The other `PeerInfoScreenMemberItem(...)` construction at `PeerInfoSettingsItems.swift:132` passes `enclosingPeer: nil`, which is valid for either optional type — no edit. + +--- + +## Task 4: Full-project Bazel build + +**Files:** none (verification only). + +- [ ] **Step 1: Run the build with `--continueOnError`** + +Run: + +```sh +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent \ + --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError +``` + +Expected: clean build (`bazel build complete` or equivalent green output). + +- [ ] **Step 2: If build fails, triage iteration** + +If errors land in `PeerInfoScreenMemberItem.swift` or `PeerInfoMembersPane.swift` or `PeerInfoProfileItems.swift`: +- Read the failing line. +- Common failure modes from prior waves: + - **Always-false `is` warning under `-warnings-as-errors`**: leftover `is TelegramX` not converted in step. Re-grep `enclosingPeer is Telegram` over the 3 files. + - **Always-failing `as?` cast warning**: leftover `as? TelegramX` not converted. Re-grep `enclosingPeer.*as\?`. + - **Type mismatch on closure-capture alias**: a `strongSelf.enclosingPeer` or `self.enclosingPeer` site missed a `_asPeer()` drop. Re-grep `enclosingPeer\._asPeer\|EnginePeer\(enclosingPeer`. + - **Unused variable warning**: a binding from `case let .channel(channel)` not actually used. Re-read the body. + +Fix in place and re-run step 1. Budget: 2 iterations. + +If errors land outside those 3 files: **STOP**. The wave was supposed to be self-contained. Re-read the spec, identify the missed call site, decide whether to add it or abandon the wave. + +--- + +## Task 5: Post-edit residue grep + +**Files:** none (verification only). + +- [ ] **Step 1: Bridge residue grep** + +Run: + +```sh +grep -rnE "enclosingPeer\._asPeer|EnginePeer\(enclosingPeer\)|enclosingPeer\.flatMap\(EnginePeer" \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ +``` + +Expected: empty output. + +- [ ] **Step 2: Cast/is-check residue grep** + +Run: + +```sh +grep -rnE "enclosingPeer.*as\? TelegramChannel|enclosingPeer.*as\? TelegramGroup|enclosingPeer is TelegramChannel" \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ +``` + +Expected: empty output. + +- [ ] **Step 3: Sanity check — `enclosingPeer` references should now exclusively type-resolve to EnginePeer** + +Run: + +```sh +grep -nE ": Peer\b" submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift +``` + +Expected: no `enclosingPeer: Peer` or `enclosingPeer: Peer?` annotations remain. (Other `: Peer` annotations on unrelated symbols are fine.) + +--- + +## Task 6: Commit the wave + +**Files:** none (git only). + +- [ ] **Step 1: Stage the 3 modified files** + +```sh +git add \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift +``` + +- [ ] **Step 2: Confirm staging is clean** + +```sh +git status --short | grep -v "^??" +``` + +Expected output: only the 3 staged files (lines starting with `M ` or `A `). If other modified files appear, they predate the wave (per CLAUDE.md memory: build-system/bazel-rules/sourcekit-bazel-bsp submodule marker is pre-existing WIP). + +- [ ] **Step 3: Commit** + +```sh +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 50 + +Migrate enclosingPeer Peer? -> EnginePeer? across PeerInfoScreenMemberItem ++ PeerInfoMembersPaneNode + 1 PSPB call site. 19 edits / 3 files. + +Drops 5 internal bridges: 2 _asPeer() demotions at PIMP:361/363, 1 +EnginePeer(enclosingPeer) wrap at PIMP:139, 1 flatMap(EnginePeer.init) +at PSMI:178, 1 boundary _asPeer() lift at PSPB:852. + +Closes the wave-48-pattern internal-demotion-and-external-re-promotion +ratchet at PIMP:354-363 (engine.data subscription returns EnginePeer?, +previously demoted to Peer? at storage). + +All `as? TelegramChannel` / `as? TelegramGroup` casts converted to +`case let .channel(...)` / `case let .legacyGroup(...)` (wave-41/45 +idiom). All `is TelegramChannel` checks converted to +`case .channel = ...` (wave-41 always-false-warning fix). + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: Verify commit** + +```sh +git log --oneline -1 +``` + +Expected: shows the wave 50 commit. + +--- + +## Task 7: Update outcome log + memory + +**Files:** +- Modify: `docs/superpowers/postbox-refactor-log.md` +- Modify: `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md` + +- [ ] **Step 1: Append wave 50 outcome to refactor log** + +Add a "Wave 50 outcome" entry at the appropriate chronological position in `docs/superpowers/postbox-refactor-log.md`. Use the wave 49 outcome entry as the template. Include: +- Commit hash (from Task 6 step 4). +- Iteration count (1 if first-pass-clean; 2 if Task 4 step 2 fired once). +- Net-bridge accounting: −5 internal bridges (2 `_asPeer()` + 1 `EnginePeer(...)` wrap + 1 `flatMap(EnginePeer.init)` + 1 boundary `_asPeer()` lift). 0 ADD wraps. 0 boundary lifts net new. +- Bazel build duration (from Task 4 step 1 output). +- Any wave-specific lessons surfaced. + +- [ ] **Step 2: Update wave-50-next-wave memory** + +Edit `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`: +- Promote wave 50 outcome line into the "Latest commits" section using the format of the wave 49 entry. +- Update the top frontmatter `description` to reflect wave 50 landed and propose wave 51. +- Promote the wave-51 candidate (`PeerInfoGroupsInCommonPaneNode.PeerEntry.peer: Peer → EnginePeer`) to the top of the "Wave 51 candidates" section, replacing the now-stale "Wave 50 candidates" header. Re-run the broader grep if needed: + +```sh +grep -rnE "^\s*(let|var|public let|public var|private let|private var) [a-zA-Z_]+: Peer\??$|^\s*(let|var|public let|public var|private let|private var) [a-zA-Z_]+: Peer\? = " \ + submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ --include="*.swift" | grep -v "EnginePeer" +``` + +- [ ] **Step 3: Commit the doc update** + +```sh +git add docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: log wave 50 outcome + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +(Memory file updates are not committed — they live outside the repo.) + +--- + +## Net delta projection (from spec) + +| Category | Count | Sites | +|---|---|---| +| Internal bridge drops | −5 | PIMP:361, PIMP:363, PIMP:139, PSMI:178, PSPB:852 | +| Boundary lifts (net new) | 0 | source pipeline already EnginePeer? | +| ADD wraps | 0 | no Peer-only property accesses on bare `enclosingPeer` | +| Cast→case-let conversions | 4 | PSMI:152/154, PIMP:113/115 | +| `is`→`case` conversions | 4 | PSMI:181/187, PIMP:142/148 | +| Type annotations updated | 6 | PSMI:23/34, PIMP:92/271/293/442 | + +**Total commit footprint:** 19 line edits across 3 files, plus a docs commit for the outcome log. diff --git a/docs/superpowers/plans/2026-04-25-peerinfoscreendata-linked-peers-engine-peer.md b/docs/superpowers/plans/2026-04-25-peerinfoscreendata-linked-peers-engine-peer.md new file mode 100644 index 0000000000..8abbf566ef --- /dev/null +++ b/docs/superpowers/plans/2026-04-25-peerinfoscreendata-linked-peers-engine-peer.md @@ -0,0 +1,106 @@ +# Wave 49 — `PeerInfoScreenData.linkedDiscussionPeer` + `.linkedMonoforumPeer` `Peer? → EnginePeer?` (bundle) + +**Date:** 2026-04-25 +**Predecessor:** Wave 48 (commit `1e4c2eea33`) — savedMessagesPeer single-field migration. +**Shape:** Cross-file bundled struct-field migration (2 sibling fields, 2 files). Both fields are module-internal; no external consumer references them on `PeerInfoScreenData`. Bundled because both fields: +- Share a sibling declaration site in `PeerInfoData.swift`. +- Have parallel local-source patterns (raw `Peer?` from `peerView.peers[id]` dict lookup; **not** an engine signal as in wave 48). +- Are both consumed in `PeerInfoProfileItems.swift` only. +- Migrating one without the other adds friction at the source-construction sites where they're computed together. + +## Pre-flight inventory + +`grep -rEn "(\w+\??)\.linkedDiscussionPeer\b|(\w+\??)\.linkedMonoforumPeer\b" submodules/ Telegram/`: +- `submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift:600,651` — references are on a local `view` object (different type, NOT PeerInfoScreenData). Out of scope. +- All `data.linkedDiscussionPeer` / `data.linkedMonoforumPeer` accesses live in PIPI within the PeerInfoScreen module. + +Within scope: + +### `PeerInfoData.swift` (storage class + 2 init sites that compute the locals) + +| Site | Code | Action | +|------|------|--------| +| :396 | `let linkedDiscussionPeer: Peer?` (field decl) | Type → `EnginePeer?` | +| :397 | `let linkedMonoforumPeer: Peer?` (field decl) | Type → `EnginePeer?` | +| :453 | `linkedDiscussionPeer: Peer?,` (init param) | Type → `EnginePeer?` | +| :454 | `linkedMonoforumPeer: Peer?,` (init param) | Type → `EnginePeer?` | +| :498 | `self.linkedDiscussionPeer = linkedDiscussionPeer` | No change | +| :499 | `self.linkedMonoforumPeer = linkedMonoforumPeer` | No change | +| :1038, :1111, :1631 | `linkedDiscussionPeer: nil,` (init kwargs) | No change | +| :1039, :1112, :1632 | `linkedMonoforumPeer: nil,` (init kwargs) | No change | +| :1836 | `var discussionPeer: Peer?` (local) | Type → `EnginePeer?` | +| :1838 | `discussionPeer = peer` (where `peer = peerView.peers[linkedDiscussionPeerId]`, raw `Peer`) | Wrap → `discussionPeer = EnginePeer(peer)` | +| :1841 | `var monoforumPeer: Peer?` (local) | Type → `EnginePeer?` | +| :1843 | `monoforumPeer = peerView.peers[linkedMonoforumId]` (dict lookup, `Peer?`) | Wrap → `monoforumPeer = peerView.peers[linkedMonoforumId].flatMap(EnginePeer.init)` | +| :2131 | `var discussionPeer: Peer?` (local, parallel to :1836) | Type → `EnginePeer?` | +| :2133 | `discussionPeer = peer` (parallel to :1838) | Wrap → `discussionPeer = EnginePeer(peer)` | +| :2136 | `var monoforumPeer: Peer?` (local, parallel to :1841) | Type → `EnginePeer?` | +| :2138 | `monoforumPeer = peerView.peers[linkedMonoforumId]` (parallel to :1843) | Wrap with `.flatMap(EnginePeer.init)` | +| :1878, :1879, :2216, :2217 | init kwargs `linkedDiscussionPeer: discussionPeer,` / `linkedMonoforumPeer: monoforumPeer,` | No change (locals migrate; pass through) | + +That's **12 edits** in PID. Note the `var` declarations and assignments at :1836–:1843 and :2131–:2138 are *parallel pairs* (verified by grep). Use `replace_all=true` for the duplicate snippets. + +### `PeerInfoProfileItems.swift` (3 edits) + +| Site | Code | Action | +|------|------|--------| +| :1098 | `if let peer = data.linkedDiscussionPeer { ... }` | No change (binding works on `EnginePeer?`) | +| :1099 | `if let addressName = peer.addressName, !addressName.isEmpty {` | No change — `EnginePeer.addressName` forwarded (verified at `submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift:461`) | +| :1102 | `discussionGroupTitle = EnginePeer(peer).displayTitle(strings: ..., displayOrder: ...)` | **Drop wrap** → `peer.displayTitle(...)` | +| :1197 | `if let monoforumPeer = data.linkedMonoforumPeer as? TelegramChannel {` | **Pattern rewrite** → `if case let .channel(monoforumPeer) = data.linkedMonoforumPeer {` | +| :1198 | `monoforumPeer.sendPaidMessageStars` | No change — `sendPaidMessageStars` is a `TelegramChannel` property (`SyncCore_TelegramChannel.swift:215`); `case .channel` binds to `TelegramChannel` | +| :1404 | `if let linkedDiscussionPeer = data.linkedDiscussionPeer {` | No change (binding works) | +| :1406 | `if let addressName = linkedDiscussionPeer.addressName, !addressName.isEmpty {` | No change (forwarded) | +| :1409 | `peerTitle = EnginePeer(linkedDiscussionPeer).displayTitle(...)` | **Drop wrap** → `linkedDiscussionPeer.displayTitle(...)` | + +3 edits in PIPI. + +## EnginePeer property forwarding audit + +- `EnginePeer.addressName` — forwarded at `Peer.swift:461`. ✓ +- `EnginePeer.displayTitle(strings:displayOrder:)` — defined as `EnginePeer` instance method (used elsewhere via `EnginePeer(...).displayTitle(...)` pattern; once we have an `EnginePeer`, it's directly callable). ✓ +- `case .channel` binding payload is `TelegramChannel`. ✓ +- `TelegramChannel.sendPaidMessageStars` — exists (`SyncCore_TelegramChannel.swift:215`). ✓ + +## Net bridge count + +- **ADDs (4):** boundary lifts at PID:1838 (`EnginePeer(peer)`), PID:1843 (`.flatMap(EnginePeer.init)`), PID:2133, PID:2138. These lift the Postbox-typed `peerView.peers[...]` value to the engine type at the boundary — the correct semantic position for a Postbox→Engine refactor (mirrors wave 42 where `peer.flatMap(EnginePeer.init)` lift was added at PID:1620). +- **DROPs (2):** PIPI:1102 and :1409 lose `EnginePeer(...)` wraps around `displayTitle` calls. +- **Net text bridges:** +2. **But:** the ADDs are correct boundary lifts; the field-typed-as-`EnginePeer?` is the canonical state. The 2 displayTitle DROPs are the actual ratchet value. +- **Plus:** 1 cleaner pattern (PIPI:1197 `as?` cast → `case let .channel`), no text saving but better Swift idiom. + +## Edit list + +### `PeerInfoData.swift` (12 edits, but Edit text uses `replace_all=true` to bundle parallel pairs) + +1. Line 396: `let linkedDiscussionPeer: Peer?` → `let linkedDiscussionPeer: EnginePeer?` +2. Line 397: `let linkedMonoforumPeer: Peer?` → `let linkedMonoforumPeer: EnginePeer?` +3. Line 453: `linkedDiscussionPeer: Peer?,` → `linkedDiscussionPeer: EnginePeer?,` +4. Line 454: `linkedMonoforumPeer: Peer?,` → `linkedMonoforumPeer: EnginePeer?,` +5. Lines 1836 + 2131 (`replace_all=true` over `var discussionPeer: Peer?`): → `var discussionPeer: EnginePeer?` +6. Lines 1838 + 2133 (`replace_all=true` over `discussionPeer = peer`): → `discussionPeer = EnginePeer(peer)` +7. Lines 1841 + 2136 (`replace_all=true` over `var monoforumPeer: Peer?`): → `var monoforumPeer: EnginePeer?` +8. Lines 1843 + 2138 (`replace_all=true` over `monoforumPeer = peerView.peers[linkedMonoforumId]`): → `monoforumPeer = peerView.peers[linkedMonoforumId].flatMap(EnginePeer.init)` + +### `PeerInfoProfileItems.swift` (3 edits) + +9. Line 1102: `discussionGroupTitle = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)` → `discussionGroupTitle = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)` +10. Line 1197: `if let monoforumPeer = data.linkedMonoforumPeer as? TelegramChannel {` → `if case let .channel(monoforumPeer) = data.linkedMonoforumPeer {` +11. Line 1409: `peerTitle = EnginePeer(linkedDiscussionPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)` → `peerTitle = linkedDiscussionPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)` + +## Out of scope + +- `PeerInfoScreenData.chatPeer` — large blast radius. Defer. +- `PeerInfoScreenMemberItem.enclosingPeer`. Defer. + +## Build & verify + +Standard Bazel command. Expected 1 iteration if forwarding audit holds; 2 if a `displayTitle` overload-resolution surprise surfaces. + +## Commit + +`Postbox -> TelegramEngine wave 49`. Body: bundle + edits summary + ADD/DROP accounting. + +## Outcome capture + +Append Wave 49 entry to `docs/superpowers/postbox-refactor-log.md`; update memory file. diff --git a/docs/superpowers/plans/2026-04-25-peerinfoscreendata-savedmessagespeer-engine-peer.md b/docs/superpowers/plans/2026-04-25-peerinfoscreendata-savedmessagespeer-engine-peer.md new file mode 100644 index 0000000000..fcbab698e1 --- /dev/null +++ b/docs/superpowers/plans/2026-04-25-peerinfoscreendata-savedmessagespeer-engine-peer.md @@ -0,0 +1,70 @@ +# Wave 48 — `PeerInfoScreenData.savedMessagesPeer` `Peer? → EnginePeer?` + +**Date:** 2026-04-25 +**Predecessor:** Wave 47 (commit `d7b7536440`) — stored PHN.peer single-file private migration. +**Shape:** Cross-file struct-field migration. Storage class is internal to PeerInfoScreen module; no external consumer references PSD.savedMessagesPeer. + +## Target + +`submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift`, `PeerInfoScreenData.savedMessagesPeer: Peer?` at line 388. + +## Pre-flight inventory + +`grep -rEn "(\w+\??)\.savedMessagesPeer\b" submodules/ Telegram/` → matches only inside `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/`. No external consumer. The same field name appears in unrelated places (TelegramEngineMessages.swift, ChatListUI, etc.) but those are different declarations on different types. + +Within PeerInfoScreen module: + +| Site | Code | Action | +|------|------|--------| +| `PeerInfoData.swift:388` | `let savedMessagesPeer: Peer?` (struct field decl) | Type change → `EnginePeer?` | +| `PeerInfoData.swift:444` | `savedMessagesPeer: Peer?,` (init param) | Type change → `EnginePeer?` | +| `PeerInfoData.swift:489` | `self.savedMessagesPeer = savedMessagesPeer` (assignment) | No change (passthrough) | +| `PeerInfoData.swift:1029` | `savedMessagesPeer: nil,` (init kwarg) | No change (`nil` works for either) | +| `PeerInfoData.swift:1102` | `savedMessagesPeer: nil,` | No change | +| `PeerInfoData.swift:1313–1317` | `let savedMessagesPeer: Signal` (local) | No change — already `EnginePeer?` | +| `PeerInfoData.swift:1622` | `savedMessagesPeer: savedMessagesPeer?._asPeer(),` | **Drop bridge** → `savedMessagesPeer: savedMessagesPeer,` | +| `PeerInfoData.swift:1869` | `savedMessagesPeer: nil,` | No change | +| `PeerInfoData.swift:2207` | `savedMessagesPeer: nil,` | No change | +| `PeerInfoScreen.swift:5399` | `peer: self.data?.savedMessagesPeer.flatMap(EnginePeer.init) ?? self.data?.peer,` | **Drop bridge** → `peer: self.data?.savedMessagesPeer ?? self.data?.peer,` | +| `PeerInfoScreen.swift:5805` | same as :5399 | Same drop | + +Total edits: 5 (3 in PID, 2 in PIS). + +## EnginePeer / read-site audit + +The local signal at `PeerInfoData.swift:1313` already produces `EnginePeer?` from `engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(...))`. The `?._asPeer()` at line 1622 was an artificial demotion. Migrating the field type to `EnginePeer?` removes both the demotion at the storage site and the `flatMap(EnginePeer.init)` re-promotions at the read sites — a clean ratchet. + +PIS:5399 and :5805 use the field as input to `headerNode.update(... peer: ...)`, whose `peer` parameter has been `EnginePeer?` since wave 45. The `??` coalescing operand is `self.data?.peer` (already `EnginePeer?`). Result: drop the `.flatMap(EnginePeer.init)` and the expression compiles. + +## Edit list + +### PeerInfoData.swift (3 edits) + +1. Line 388: `let savedMessagesPeer: Peer?` → `let savedMessagesPeer: EnginePeer?` +2. Line 444: `savedMessagesPeer: Peer?,` → `savedMessagesPeer: EnginePeer?,` +3. Line 1622: `savedMessagesPeer: savedMessagesPeer?._asPeer(),` → `savedMessagesPeer: savedMessagesPeer,` + +### PeerInfoScreen.swift (2 edits, identical text) + +4. Line 5399: `peer: self.data?.savedMessagesPeer.flatMap(EnginePeer.init) ?? self.data?.peer,` → `peer: self.data?.savedMessagesPeer ?? self.data?.peer,` +5. Line 5805: same + +Use `replace_all=true` for the PIS edit since the matched text appears at both call sites verbatim. + +## Out of scope + +- `PeerInfoScreenData.chatPeer` — large blast radius (5 `as? TelegramX` checks downstream + ClearPeerHistory init parameter), defer. +- `PeerInfoScreenData.linkedDiscussionPeer`, `linkedMonoforumPeer` — both have `as? TelegramChannel` consumer sites in `PeerInfoProfileItems.swift`. Defer. +- `PeerInfoScreenMemberItem.enclosingPeer` — defer (separate target). + +## Build & verify + +Same Bazel command as wave 47. Expected 1-iteration first-pass-clean (single-pattern bridge removal, no enum-case rewrites, no Peer-only property access). + +## Commit + +`Postbox -> TelegramEngine wave 48`. Body lists the 5-edit summary and notes −3 internal bridges (1 PID + 2 PIS, identical PIS text appears twice). + +## Outcome capture + +Append a Wave 48 entry to `docs/superpowers/postbox-refactor-log.md` and update memory file `project_postbox_refactor_next_wave.md`. diff --git a/docs/superpowers/plans/2026-04-25-phn-peer-stored-field-engine-peer.md b/docs/superpowers/plans/2026-04-25-phn-peer-stored-field-engine-peer.md new file mode 100644 index 0000000000..773f2bb01a --- /dev/null +++ b/docs/superpowers/plans/2026-04-25-phn-peer-stored-field-engine-peer.md @@ -0,0 +1,62 @@ +# Wave 47 — `PeerInfoHeaderNode.peer` stored field `Peer? → EnginePeer?` + +**Date:** 2026-04-25 +**Predecessor:** Wave 46 (commit `5ca99da5a7`) — PeerInfo avatar chain. +**Shape:** Single-file stored-field type migration. No external API change (field is `private`). + +## Target + +`submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift`, stored field `private var peer: Peer?` at line 92. + +## Pre-flight inventory + +`grep -n "self\.peer\b" PeerInfoHeaderNode.swift` returns exactly 3 references: + +| Line | Code | Site type | Action | +|------|------|-----------|--------| +| 426 | `if let peer = self.peer, peer.profileImageRepresentations.isEmpty && gallery {` | Read | None — `profileImageRepresentations` is forwarded by `EnginePeer` (see `submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift:485`). Compiles unchanged. | +| 521 | `self.peer = peer?._asPeer()` | Assignment | Drop the bridge → `self.peer = peer`. The `peer` parameter is already `EnginePeer?` after wave 45. | +| 2049–2054 | `guard let self, let peer = self.peer, ...` followed by `peer: EnginePeer(peer),` | Read | Drop the wrap at line 2054 → `peer: peer,`. | + +External access check: `grep -rn "headerNode\.peer\b" submodules/ Telegram/` returns empty. The field is private; only same-file siblings touch it. + +EnginePeer forwarding (re-confirmed at plan time): +- `profileImageRepresentations` — forwarded (Peer.swift:485). ✓ +- `EnginePeer(peer)` (PHN:2054) — accepts `EnginePeer` directly when the local is already `EnginePeer`; drop the constructor. + +Field-declaration change is the only "type" change needed. The 3 callers' adjustments are mechanical bridge drops. + +## Edit list + +1. Line 92: `private var peer: Peer?` → `private var peer: EnginePeer?` +2. Line 521: `self.peer = peer?._asPeer()` → `self.peer = peer` +3. Line 2054: `peer: EnginePeer(peer),` → `peer: peer,` + +Total: 3 edits in 1 file. + +## Out of scope + +- `PeerInfoData.swift:355,487` — different classes' `self.peer` assignments (different types). Audit confirms these are `RenderedChannelParticipant.peer` and similar — already migrated in earlier waves or owned by other types. +- `PeerInfoAvatarTransformContainerNode.peer` (line 223) — already `EnginePeer?` after wave 46. + +## Build & verify + +```sh +source ~/.zshrc 2>/dev/null; \ +python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent \ + --buildNumber=1 --configuration=debug_sim_arm64 +``` + +Expected: 1-iteration first-pass-clean. Only PeerInfoScreen + TelegramUI recompile. + +## Commit + +`Postbox -> TelegramEngine wave 47`. Body lists the 3-edit summary and notes -3 internal bridges. + +## Outcome capture + +Append a Wave 47 entry to `docs/superpowers/postbox-refactor-log.md` and update memory file `project_postbox_refactor_next_wave.md`. diff --git a/docs/superpowers/plans/2026-04-26-postbox-wave-103-chat-recent-actions-controller-node.md b/docs/superpowers/plans/2026-04-26-postbox-wave-103-chat-recent-actions-controller-node.md new file mode 100644 index 0000000000..68d5d0311b --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-postbox-wave-103-chat-recent-actions-controller-node.md @@ -0,0 +1,347 @@ +# Wave 103: ChatRecentActionsControllerNode peer Peer → EnginePeer Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate `ChatRecentActionsControllerNode`'s stored `peer: Peer` field to `EnginePeer`, dropping the `_asPeer()` boundary call at the single caller site (wave 103 of the Postbox → TelegramEngine refactor). + +**Architecture:** Wave-71-shadow close. Single-file private stored-form migration plus a 1-line caller drop. The caller (`ChatRecentActionsController`) already holds `peer: EnginePeer` and demotes once before passing into the node init. The wave drops the demotion and rewrites 3 `as? TelegramChannel` downcasts inside the node body to `case let .channel(...)` (wave-41/45 idiom). All scope is within `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/`. + +**Tech Stack:** Swift, Bazel via `Make.py`, no unit tests (per `CLAUDE.md`). Verification is the full-project debug-sim-arm64 build. + +**Iteration budget:** 1 (target first-pass-clean given the 7-edit scope and validated pre-flight grep). + +**Note on TDD:** This project has no unit tests. The standard TDD test-first cycle does not apply. Each task writes the edits, then verifies via Bazel build + residue grep. + +--- + +## File Structure + +| File | Role | Changes | +|---|---|---| +| `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift` (CRACN) | Recent-actions screen controller node | Drop `import Postbox`, retype stored field + init param, rewrite 3 `as? TelegramChannel` downcasts (6 edits) | +| `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift` (CRAC) | Recent-actions screen controller (caller) | Drop `_asPeer()` at the node init (1 edit) | + +No public-API ripple — `ChatRecentActionsControllerNode` is local to the module and has a single caller verified by grep. + +--- + +## Task 1: CRACN.swift — drop `import Postbox` + type changes + downcast rewrites + +**Files:** +- Modify: `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift` + +**Edits in this task:** 6 (1 import drop, 1 stored-field retype, 1 init-param retype, 3 cast → case-let). + +- [ ] **Step 1: Drop `import Postbox` at line 5** + +Find: + +```swift +import Postbox +``` + +Replace with: (delete the line entirely) + +This file imports `TelegramCore` at line 4, which provides the `EnginePeer` type and the typealiases needed for the rest of this task. + +- [ ] **Step 2: Retype stored field at line 46** + +Find: + +```swift + private let peer: Peer +``` + +Replace with: + +```swift + private let peer: EnginePeer +``` + +- [ ] **Step 3: Retype init parameter at line 111** + +Find: + +```swift + init(context: AccountContext, controller: ChatRecentActionsController, peer: Peer, presentationData: PresentationData, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, PresentationContextType, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?) { +``` + +Replace with: + +```swift + init(context: AccountContext, controller: ChatRecentActionsController, peer: EnginePeer, presentationData: PresentationData, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, PresentationContextType, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?) { +``` + +- [ ] **Step 4: Rewrite downcast at line 899** + +Find: + +```swift + if let peer = strongSelf.peer as? TelegramChannel { +``` + +Replace with: + +```swift + if case let .channel(peer) = strongSelf.peer { +``` + +The bound name `peer` is preserved so the inner block (`switch peer.info { case .group: ... }`) ports verbatim. `case let .channel(peer)` binds `peer: TelegramChannel` directly (the associated value of `EnginePeer.channel`). + +- [ ] **Step 5: Rewrite downcast at line 948** + +Find: + +```swift + if let channel = self.peer as? TelegramChannel, case .broadcast = channel.info { +``` + +Replace with: + +```swift + if case let .channel(channel) = self.peer, case .broadcast = channel.info { +``` + +The compound condition (`, case .broadcast = channel.info`) ports verbatim because the bound `channel` is still `TelegramChannel`-typed. + +- [ ] **Step 6: Rewrite downcast at line 1088** + +Find: + +```swift + if let channel = self.peer as? TelegramChannel { +``` + +Replace with: + +```swift + if case let .channel(channel) = self.peer { +``` + +The inner block (`channel.hasPermission(.banMembers)`, `case .broadcast = channel.info`) ports verbatim. + +The `self.peer.id` accesses at lines 145, 161, 1138, 1490 require no edit — `EnginePeer.id` is a typealiased `PeerId`, identical at the call sites. + +--- + +## Task 2: CRAC.swift — drop boundary `_asPeer()` + +**Files:** +- Modify: `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift` + +**Edits in this task:** 1. + +- [ ] **Step 1: Drop `_asPeer()` at line 277** + +Find: + +```swift + self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer._asPeer(), presentationData: self.presentationData, pushController: { [weak self] c in +``` + +Replace with: + +```swift + self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer, presentationData: self.presentationData, pushController: { [weak self] c in +``` + +`ChatRecentActionsController.peer` is already declared `EnginePeer` at line 42 (`public init(context: AccountContext, peer: EnginePeer, ...)`) — the type carries through to the now-`EnginePeer`-typed init parameter. + +--- + +## Task 3: Full-project Bazel build + +**Files:** none (verification only). + +- [ ] **Step 1: Run the build** + +Run: + +```sh +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent \ + --buildNumber=1 --configuration=debug_sim_arm64 +``` + +Expected: clean build (`bazel build complete` or equivalent green output). No `--continueOnError` because the small scope makes the first error informative. + +Build cost projection: consumer-only, ~25s. If it exceeds ~60s, suspect a cascade leak. + +- [ ] **Step 2: If build fails, triage iteration** + +If errors land in `ChatRecentActionsControllerNode.swift` or `ChatRecentActionsController.swift`: +- Read the failing line. +- Common failure modes from prior waves: + - **Always-false `is` warning under `-warnings-as-errors`:** none expected here (pre-flight grep confirmed no `is TelegramChannel` checks on `self.peer`). If one surfaces anyway, convert to `case .channel = self.peer`. + - **Always-failing `as?` cast warning:** leftover `as? TelegramX` not converted in step 4/5/6. Re-grep `(self|strongSelf)\.peer as\?` over the file. + - **Type mismatch on closure-capture alias:** none expected here (pre-flight grep confirmed only `strongSelf.peer` and `self.peer` aliases, both ride the type change). + - **Type mismatch on `.id` access:** would indicate a regression in the `EnginePeer.Id` typealias — STOP and re-read CLAUDE.md, this is not a wave-103 issue. + - **Unused-variable warning under `-warnings-as-errors`:** a `case let .channel(peer)` binding not used inside the body. Re-read step 4/5/6 — if the inner block never references the bound name, switch to `case .channel = ...` and remove the binding. + +Fix in place and re-run step 1. Budget: 2 iterations. + +If errors land outside those 2 files: **STOP**. The wave was supposed to be self-contained. Re-read the spec, identify the missed call site, decide whether to add it or abandon the wave. + +--- + +## Task 4: Post-edit residue grep + +**Files:** none (verification only). + +- [ ] **Step 1: Cast residue grep** + +Run: + +```sh +grep -nE "(self|strongSelf)\.peer as\? Telegram(Channel|Group|User)" \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ +``` + +Expected: empty output. + +- [ ] **Step 2: Boundary `_asPeer()` residue grep** + +Run: + +```sh +grep -nE "self\.peer\._asPeer\(\)" \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ +``` + +Expected: empty output. + +- [ ] **Step 3: `import Postbox` residue grep** + +Run: + +```sh +grep -rn "^import Postbox$" \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ +``` + +Expected: empty output. The module is now Postbox-import-free. + +- [ ] **Step 4: Sanity check — `peer: Peer` annotations** + +Run: + +```sh +grep -nE "peer: Peer\b" \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +``` + +Expected: empty output. (The 3 `as? TelegramChannel` downcasts on `self.peer` were the only sources; both `peer: Peer` annotations on stored field and init param are now `peer: EnginePeer`.) + +--- + +## Task 5: Commit the wave + +**Files:** none (git only). + +- [ ] **Step 1: Stage the 2 modified files** + +```sh +git add \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +``` + +- [ ] **Step 2: Confirm staging is clean** + +```sh +git status --short | grep -v "^??" +``` + +Expected output: only the 2 staged files (lines starting with `M `). If other modified files appear, they predate the wave (per CLAUDE.md memory: `build-system/bazel-rules/sourcekit-bazel-bsp` submodule marker is pre-existing WIP). + +- [ ] **Step 3: Commit** + +```sh +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 103 + +Migrate ChatRecentActionsControllerNode.peer Peer -> EnginePeer. +Closes the wave-71 shadow: caller already held EnginePeer and demoted +at the boundary. 7 edits / 2 files. + +Drops 1 boundary _asPeer() at ChatRecentActionsController:277, drops +import Postbox at ChatRecentActionsControllerNode:5, rewrites 3 +`as? TelegramChannel` downcasts to `case let .channel(...)` (wave-41/45 +idiom). + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: Verify commit** + +```sh +git log --oneline -1 +``` + +Expected: shows the wave 103 commit as HEAD. + +--- + +## Task 6: Update outcome log + memory + +**Files:** +- Modify: `docs/superpowers/postbox-refactor-log.md` +- Modify: `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md` +- Modify: `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md` + +- [ ] **Step 1: Append wave 103 outcome to refactor log** + +Append a "Wave 103 outcome" entry at the chronological end of `docs/superpowers/postbox-refactor-log.md`. Use the most recent wave-outcome entry as a structural template. Include: +- Commit hash (from Task 5 step 4). +- Iteration count (1 if first-pass-clean; 2 if Task 3 step 2 fired). +- Net-bridge accounting: −1 boundary `_asPeer()` (CRAC:277), −1 `import Postbox` (CRACN:5). 0 ADD wraps. 3 cast → case-let conversions (CRACN:899/948/1088). +- Bazel build duration (from Task 3 step 1 output). +- Wave-shape note: wave-71-shadow close, single-iter target validated. + +- [ ] **Step 2: Update next-wave memory** + +Edit `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`: +- Add the wave 103 outcome line into the recent-waves section (commit hash + 7-edit / 2-file / 1-iter summary). +- Remove the now-stale `ChatRecentActionsControllerNode.peer: Peer -> EnginePeer` candidate line (currently bullet 5 in the candidates list). +- Update the top frontmatter `description` to reflect wave 103 landed and propose wave 104. +- Promote the next candidate (likely one of: `cachedResourceRepresentation` foundational facade, `RenderedPeer` cascade kickoff, `SelectivePrivacyPeer` foundational, or another Shape-C/D mini-refactor) to the top of the candidates list. + +- [ ] **Step 3: Update MEMORY.md index** + +Edit `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md`: +- Update the `[Postbox refactor next wave]` line to mention wave 103 landed and shift the "Wave 103+ Shape-C/D candidates" framing forward to "Wave 104+ candidates". + +- [ ] **Step 4: Commit the doc update** + +```sh +git add docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: log wave 103 outcome + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +(Memory file updates at `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/` are not committed — they live outside the repo.) + +--- + +## Net delta projection (from spec) + +| Category | Count | Sites | +|---|---|---| +| Internal bridge drops | −1 | CRAC:277 (`_asPeer()`) | +| `import Postbox` drops | −1 | CRACN:5 | +| ADD wraps | 0 | no Peer-only property accesses on bare `self.peer` | +| Cast → case-let conversions | 3 | CRACN:899, CRACN:948, CRACN:1088 | +| Type annotations updated | 2 | CRACN:46 (stored field), CRACN:111 (init param) | +| Postbox-free module count | +1 | `Components/Chat/ChatRecentActionsController/` joins the list | + +**Total commit footprint:** 7 line edits across 2 files, plus a docs commit for the outcome log. diff --git a/docs/superpowers/plans/2026-04-26-postbox-wave-103-retry-account-manager-store-resource-data-drain.md b/docs/superpowers/plans/2026-04-26-postbox-wave-103-retry-account-manager-store-resource-data-drain.md new file mode 100644 index 0000000000..efc202c33d --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-postbox-wave-103-retry-account-manager-store-resource-data-drain.md @@ -0,0 +1,260 @@ +# Wave 103 (retry): accountManager.mediaBox.storeResourceData drain Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Drain 5 remaining `accountManager.mediaBox.storeResourceData(...)` Shape-A sites against the wave-94 `AccountManagerResources.storeResourceData(id:data:synchronous:)` facade. Wave 103 (retry) of the Postbox → TelegramEngine refactor, after the abandonment of the original wave-103 plan. + +**Architecture:** Wave-shape-G drain. Pure call-site rewrite; no facade addition, no TelegramCore touch, no public-API change. 5 sites across 2 consumer files (`ThemeUpdateManager.swift`, `WallpaperResources.swift`) migrated via 3 `Edit` calls (1 single + 2 `replace_all=true` batches). + +**Tech Stack:** Swift, Bazel via `Make.py`, no unit tests (per `CLAUDE.md`). Verification is the full-project debug-sim-arm64 build. + +**Iteration budget:** 1 (target first-pass-clean given mechanical scope and validated facade). + +**Note on TDD:** This project has no unit tests. Each task writes the edits, then verifies via Bazel build + residue grep. + +--- + +## File Structure + +| File | Role | Changes | +|---|---|---| +| `submodules/TelegramUI/Sources/ThemeUpdateManager.swift` | Theme-update background sync | 1 site migrated | +| `submodules/WallpaperResources/Sources/WallpaperResources.swift` | Wallpaper resource pipeline | 4 sites migrated via 2 `replace_all=true` batches | + +No public-API ripple — both files are leaf consumers of the wave-94 facade. + +--- + +## Task 1: ThemeUpdateManager.swift — single-site migration + +**Files:** +- Modify: `submodules/TelegramUI/Sources/ThemeUpdateManager.swift` + +**Edits in this task:** 1. + +- [ ] **Step 1: Migrate the storeResourceData call at line 112** + +Find: + +```swift + accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true) +``` + +Replace with: + +```swift + accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: fullSizeData, synchronous: true) +``` + +`accountManager` here is closure-captured from `presentationThemeSettingsUpdated(_:)` scope, typed `AccountManager`. The facade is exposed via `public extension AccountManager { var resources: AccountManagerResources }`. + +--- + +## Task 2: WallpaperResources.swift — two batched migrations + +**Files:** +- Modify: `submodules/WallpaperResources/Sources/WallpaperResources.swift` + +**Edits in this task:** 2 (each `replace_all=true`, covering 2 sites apiece). + +- [ ] **Step 1: Migrate the `reference.resource.id` pattern (lines 973, 1214)** + +Use `Edit` with `replace_all=true`: + +Find: + +```swift +accountManager.mediaBox.storeResourceData(reference.resource.id, data: data) +``` + +Replace with: + +```swift +accountManager.resources.storeResourceData(id: EngineMediaResource.Id(reference.resource.id), data: data) +``` + +Both sites share identical text (verified by pre-flight grep). `replace_all=true` handles both atomically. + +- [ ] **Step 2: Migrate the `file.file.resource.id` pattern (lines 1260, 1523)** + +Use `Edit` with `replace_all=true`: + +Find: + +```swift +accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData) +``` + +Replace with: + +```swift +accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: fullSizeData) +``` + +Both sites share identical text. `replace_all=true` handles both atomically. + +--- + +## Task 3: Full-project Bazel build + +**Files:** none (verification only). + +- [ ] **Step 1: Run the build** + +```sh +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent \ + --buildNumber=1 --configuration=debug_sim_arm64 +``` + +Expected: clean build (`bazel build complete` / `INFO: Build completed successfully`). No `--continueOnError` because the small scope makes the first error informative. + +Build cost projection: WallpaperResources is foundational with wide rebuild fan-out; expect ~30-90s. + +- [ ] **Step 2: If build fails, triage iteration** + +Common failure modes: +- **`EngineMediaResource.Id` not in scope** — verify `import TelegramCore` is at the top of the failing file (it should be — pre-flight inventoried both files have it). If absent, add it. +- **Type mismatch on `id:` parameter** — would suggest an unexpected `MediaResourceId` subtype. STOP and re-read; the migration assumed `MediaResource.id: MediaResourceId` for both `reference.resource` and `file.file.resource`. Both should resolve to `MediaResourceId` per Postbox protocol. +- **`accountManager.resources` not in scope** — the `public extension AccountManager` exists in TelegramCore (wave 94). If unreachable, the consumer's BUILD might be missing a TelegramCore dep — but both files already use TelegramCore types, so this should not happen. STOP if it does. + +If errors land outside those 2 files: **STOP and report BLOCKED**. The wave is supposed to be self-contained. + +Fix in place and re-run step 1. Budget: 2 iterations. + +--- + +## Task 4: Post-edit residue grep + +**Files:** none (verification only). + +- [ ] **Step 1: Verify zero remaining `accountManager.mediaBox.storeResourceData` in the 2 touched files** + +Run: + +```sh +grep -rn "accountManager\.mediaBox\.storeResourceData" \ + submodules/TelegramUI/Sources/ThemeUpdateManager.swift \ + submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +Expected: empty output. + +--- + +## Task 5: Commit the wave + +**Files:** none (git only). + +- [ ] **Step 1: Stage the 2 modified files** + +```sh +git add \ + submodules/TelegramUI/Sources/ThemeUpdateManager.swift \ + submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +- [ ] **Step 2: Confirm staging is clean** + +```sh +git status --short | grep -v "^??" +``` + +Expected output: only the 2 staged files (lines starting with `M `). The line `m build-system/bazel-rules/sourcekit-bazel-bsp` is pre-existing WIP and should NOT appear in the staged list. + +- [ ] **Step 3: Commit** + +```sh +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 103 (retry) + +Drain 5 accountManager.mediaBox.storeResourceData(...) Shape-A sites +that the wave-94/95-99 sweep missed. All 5 migrated to +accountManager.resources.storeResourceData(id: EngineMediaResource.Id(...)) +against the existing wave-94 facade. + +Sites: ThemeUpdateManager:112 (with synchronous: true), +WallpaperResources:973, 1214 (reference.resource.id pattern, replace_all), +WallpaperResources:1260, 1523 (file.file.resource.id pattern, replace_all). + +5 sites / 2 files / 3 Edit calls. Consumer-only build. + +Wave-103 retry after the abandonment of ChatRecentActionsControllerNode +peer migration; see postbox-refactor-log "Wave 103 outcome" for the +forensics. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: Verify commit** + +```sh +git log --oneline -1 +``` + +Expected: shows the wave 103 (retry) commit as HEAD. + +--- + +## Task 6: Update outcome log + memory + +**Files:** +- Modify: `docs/superpowers/postbox-refactor-log.md` +- Modify: `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md` +- Modify: `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md` + +- [ ] **Step 1: Append wave 103 (retry) outcome to refactor log** + +Append a "Wave 103 (retry) outcome" entry to `docs/superpowers/postbox-refactor-log.md`. Include: +- Commit hash (from Task 5 step 4). +- Iteration count (1 if first-pass-clean; 2 if Task 3 step 2 fired). +- Bazel build duration. +- Net-delta accounting: −5 raw `mediaBox.X` accesses, +5 facade calls, +5 `EngineMediaResource.Id(...)` wraps (canonical engine-side, not Postbox bridges). +- Wave-shape note: G drain, validates the wave-94 facade across an additional 2-module footprint. + +- [ ] **Step 2: Update next-wave memory** + +Edit `project_postbox_refactor_next_wave.md`: +- Add wave 103 (retry) outcome line into the recent-waves section. +- Mark the 5 sites as drained; remove from candidate inventories (the file currently lists "Wave 95+ candidates" with stale storeResourceData entries — clean those up). +- Update the top frontmatter `description` to reflect wave 103 (retry) landed. +- Promote next candidate. Options: 7-site `resourceData(...)` drain (would need a new facade method or use existing `data(resource:)`), DirectMediaImageCache Shape-C/D, or pivot to a foundational wave. + +- [ ] **Step 3: Update MEMORY.md index** + +Edit `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md`: +- Update the `[Postbox refactor next wave]` line to mention wave 103 (retry) landed. + +- [ ] **Step 4: Commit the doc update** + +```sh +git add docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: log wave 103 (retry) outcome + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +(Memory file updates are not committed — they live outside the repo.) + +--- + +## Net delta projection + +| Category | Count | Sites | +|---|---|---| +| Raw `mediaBox.X` access drops | −5 | TUM:112 + WR:973, 1214, 1260, 1523 | +| Facade calls added | +5 | same sites, migrated form | +| `EngineMediaResource.Id(...)` wraps | +5 | canonical engine-side constructs (not Postbox bridges) | +| `import Postbox` drops | 0 | both files retain Postbox import for unrelated symbols | +| Postbox-free module count | 0 | no module dropped from the import list | + +**Total commit footprint:** 5 line edits (3 Edit calls) across 2 files, plus a docs commit for the outcome log. diff --git a/docs/superpowers/plans/2026-04-26-postbox-wave-104-account-manager-resource-data-drain-3-sites.md b/docs/superpowers/plans/2026-04-26-postbox-wave-104-account-manager-resource-data-drain-3-sites.md new file mode 100644 index 0000000000..de643a1f87 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-postbox-wave-104-account-manager-resource-data-drain-3-sites.md @@ -0,0 +1,331 @@ +# Wave 104: accountManager.mediaBox.resourceData drain (3 clean sites) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Drain 3 of 8 `accountManager.mediaBox.resourceData(...)` Shape-A sites against the existing wave-32 / wave-94 `AccountManagerResources.data(resource:)` facade. Wave 104 of the Postbox → TelegramEngine refactor. + +**Architecture:** Wave-shape-G drain with a documented consumer field rename. Single-file consumer migration in `submodules/WallpaperResources/Sources/WallpaperResources.swift`. 3 call rewrites + 3 consumer-side `.complete` → `.isComplete` renames, 6 Edit calls total. The remaining 5 of the original 8 `resourceData` candidates are deferred (2 cross a `MediaResourceData` flow-out cascade, 3 are coupled to postbox-side via `combineLatest` typed tuples). + +**Tech Stack:** Swift, Bazel via `Make.py`, no unit tests. Verification is the full-project debug-sim-arm64 build. + +**Iteration budget:** 1 (target first-pass-clean given verified pre-flight inventory). + +**Note on TDD:** This project has no unit tests. Each task writes the edits, then verifies via Bazel build + residue grep. + +--- + +## File Structure + +| File | Role | Changes | +|---|---|---| +| `submodules/WallpaperResources/Sources/WallpaperResources.swift` | Wallpaper resource pipeline | 3 call rewrites + 3 consumer renames | + +No public-API ripple — leaf-consumer migration against an existing facade. + +--- + +## Task 1: WallpaperResources.swift — call rewrites (3 edits) + +**Files:** +- Modify: `submodules/WallpaperResources/Sources/WallpaperResources.swift` + +**Edits in this task:** 3. + +- [ ] **Step 1: Migrate the call at line 957 (`reference.resource` argument)** + +Find: + +```swift + let maybeFetched = accountManager.mediaBox.resourceData(reference.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) +``` + +Replace with: + +```swift + let maybeFetched = accountManager.resources.data(resource: EngineMediaResource(reference.resource), attemptSynchronously: synchronousLoad) +``` + +Note: `waitUntilFetchStatus: false` is omitted because the facade default is `false`. The site explicitly passed `false`, so behavior is preserved. + +- [ ] **Step 2: Migrate the call at line 1164 (`fileReference.media.resource` argument)** + +Find: + +```swift + let maybeFetched = accountManager.mediaBox.resourceData(fileReference.media.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) +``` + +Replace with: + +```swift + let maybeFetched = accountManager.resources.data(resource: EngineMediaResource(fileReference.media.resource), attemptSynchronously: synchronousLoad) +``` + +Same `waitUntilFetchStatus: false` omission rationale. + +- [ ] **Step 3: Migrate the call at line 1264 (`file.file.resource` argument, no option)** + +Find: + +```swift + return accountManager.mediaBox.resourceData(file.file.resource) +``` + +Replace with: + +```swift + return accountManager.resources.data(resource: EngineMediaResource(file.file.resource)) +``` + +The original used the underlying `MediaBox.resourceData(_ resource:)` overload's defaults — facade defaults match exactly (`pathExtension: nil`, `waitUntilFetchStatus: false`, `attemptSynchronously: false`). + +--- + +## Task 2: WallpaperResources.swift — consumer-side `.complete` → `.isComplete` renames (3 edits) + +**Files:** +- Modify: `submodules/WallpaperResources/Sources/WallpaperResources.swift` + +**Edits in this task:** 3. + +`EngineMediaResource.ResourceData` exposes `.isComplete` (renamed from `MediaResourceData.complete`). All three migrated call sites have a single consumer-side `.complete` access on the migrated result that needs renaming. + +- [ ] **Step 1: Rename `maybeData.complete` at line 961 (consumer of site 957)** + +Find: + +```swift + if maybeData.complete { +``` + +Replace with: + +```swift + if maybeData.isComplete { +``` + +The leading whitespace (8 spaces) must match exactly. + +- [ ] **Step 2: Rename `maybeData.complete` at line 1168 (consumer of site 1164)** + +Find: + +```swift + if maybeData.complete && isSupportedTheme { +``` + +Replace with: + +```swift + if maybeData.isComplete && isSupportedTheme { +``` + +The leading whitespace (16 spaces) must match exactly. + +- [ ] **Step 3: Rename `data.complete` at line 1266 (consumer of site 1264)** + +Find: + +```swift + if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { +``` + +Replace with: + +```swift + if data.isComplete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { +``` + +The leading whitespace (36 spaces) must match exactly. + +The `data.path` access on the same line is unchanged — both `MediaResourceData.path` and `EngineMediaResource.ResourceData.path` are `String`. + +--- + +## Sites NOT touched (deferred) + +For the implementer's awareness — these `.complete` accesses on UNRELATED bindings stay raw and are NOT to be renamed: + +- `WallpaperResources.swift:968` — `return data.complete ? try? Data(contentsOf: URL(fileURLWithPath: data.path)) : nil` — this `data` is bound from `account.postbox.mediaBox.resourceData(...)` (postbox-side, not migrated). STAYS `.complete`. +- Other `.complete` accesses elsewhere in the file that aren't on the 3 migrated bindings — STAY. + +The 3 renames target only the 3 specific lines listed in Task 2 steps 1-3. Do NOT use `replace_all=true` for renames — bindings differ per scope. + +--- + +## Task 3: Full-project Bazel build + +**Files:** none (verification only). + +- [ ] **Step 1: Run the build** + +```sh +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent \ + --buildNumber=1 --configuration=debug_sim_arm64 +``` + +Expected: clean build (`bazel build complete` / `INFO: Build completed successfully`). No `--continueOnError`. Build cost projection: ~30-60s (consumer-only, foundational module rebuild fan-out). + +- [ ] **Step 2: If build fails, triage iteration** + +Common failure modes: +- **`EngineMediaResource` constructor not found** — verify `import TelegramCore` at the top of WallpaperResources.swift (it should already be there). If missing, add it. +- **Type mismatch on `resource:` parameter** — would suggest the argument expression isn't `MediaResource`-typed. STOP and check the actual type at the failing site. +- **Type mismatch on `.isComplete` rename** — if the closure parameter binding is somehow inferred wrong (e.g., Swift inferred the OLD `MediaResourceData` type because the call rewrite didn't take effect), the rename will fail. Re-read the diff and verify the call rewrite landed. +- **`data.path` type mismatch** — should not happen; both types expose `path: String`. If it does, STOP and re-read. + +If errors land outside WallpaperResources.swift: STOP and report BLOCKED. The wave is supposed to be self-contained. + +Iteration budget: 2. + +--- + +## Task 4: Post-edit residue grep + +**Files:** none (verification only). + +- [ ] **Step 1: Verify the 3 migrated call sites are gone** + +Run: + +```sh +grep -nE "accountManager\.mediaBox\.resourceData\(" submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +Expected: exactly 3 lines remaining (L33, L59, L401 — the deferred combineLatest sites). The migrated lines (originally 957, 1164, 1264) should NOT appear. + +- [ ] **Step 2: Verify the 3 renames are applied** + +Run: + +```sh +grep -nE "maybeData\.complete\b" submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +Expected: empty output. Both `maybeData.complete` accesses (originally L961, L1168) should be gone. + +```sh +grep -nE "if data\.complete," submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +Expected: no line at L1266 (the migrated site). Other `data.complete` accesses on postbox-side bindings (e.g., L968) may remain — those are out of scope. + +--- + +## Task 5: Commit the wave + +**Files:** none (git only). + +- [ ] **Step 1: Stage the 1 modified file** + +```sh +git add submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +- [ ] **Step 2: Confirm staging is clean** + +```sh +git status --short | grep -v "^??" +``` + +Expected: only the 1 staged file (line starting with `M `). The line `m build-system/bazel-rules/sourcekit-bazel-bsp` is pre-existing WIP and should NOT appear in the staged list. + +- [ ] **Step 3: Commit** + +```sh +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 104 + +Drain 3 accountManager.mediaBox.resourceData(...) Shape-A sites against +the existing wave-32 / wave-94 AccountManagerResources.data(resource:) +facade. Sites: WallpaperResources:957 (reference.resource), :1164 +(fileReference.media.resource), :1264 (file.file.resource). + +Migration: accountManager.mediaBox.resourceData(X, option: .complete( +waitUntilFetchStatus: false)[, attemptSynchronously: Y]) -> accountManager +.resources.data(resource: EngineMediaResource(X)[, attemptSynchronously: +Y]). Plus 3 consumer-side .complete -> .isComplete renames at L961, +L1168, L1266 to match EngineMediaResource.ResourceData field name. + +3 sites / 1 file / 6 Edit calls. Consumer-only build. + +Deferred: 2 sites in FetchCachedRepresentations.swift (482, 490) flow +data: MediaResourceData into fetchCachedScaled*Representation cascade; +3 sites in WallpaperResources (33, 59, 401) coupled to postbox-side via +combineLatest typed tuples. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: Verify commit** + +```sh +git log --oneline -1 +``` + +Expected: shows the wave 104 commit as HEAD. + +--- + +## Task 6: Update outcome log + memory + +**Files:** +- Modify: `docs/superpowers/postbox-refactor-log.md` +- Modify: `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md` +- Modify: `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md` + +- [ ] **Step 1: Append wave 104 outcome to refactor log** + +Append a "Wave 104 outcome" entry to `docs/superpowers/postbox-refactor-log.md` matching the format of "Wave 103 (retry) outcome". Include: +- Commit hash (from Task 5 step 4). +- Iteration count (1 if first-pass-clean; 2 if Task 3 step 2 fired). +- Bazel build duration (from Task 3 step 1 output). +- Net-delta accounting: −3 raw `mediaBox.X` accesses, +3 facade calls, +3 `EngineMediaResource(...)` wraps, +3 consumer field renames. +- Wave-shape note: G drain with documented consumer field rename. The pre-flight identified a `MediaResourceData`-typed-function-parameter barrier (`fetchCachedScaled*Representation` family) that forced 2 sites into the deferred bucket — illustrates the wave-71-shadow lesson applied to result-type cascades, not just peer migrations. + +- [ ] **Step 2: Update next-wave memory** + +Edit `project_postbox_refactor_next_wave.md`: +- Add wave 104 outcome line into the recent-waves section. +- Update accountManager-side facade drain status table: `resourceData` count drops from 8 → 5 (3 drained, 5 deferred). +- Add a new section (or extend an existing one) documenting the "Postbox-typed-function-parameter barrier" pattern, with `Message.peers: SimpleDictionary` (wave-103 lesson) and now `fetchCachedScaled*Representation(resourceData: MediaResourceData)` as the two known instances. + +- [ ] **Step 3: Update MEMORY.md index** + +Edit `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md`: +- Update the `[Postbox refactor next wave]` line to mention wave 104 landed. + +- [ ] **Step 4: Commit the doc update** + +```sh +git add docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: log wave 104 outcome + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +(Memory file updates are not committed — they live outside the repo.) + +--- + +## Net delta projection + +| Category | Count | Sites | +|---|---|---| +| Raw `mediaBox.X` access drops | −3 | WR:957, 1164, 1264 | +| Facade calls added | +3 | same sites, migrated form | +| `EngineMediaResource(...)` wraps | +3 | canonical engine-side, not Postbox bridges | +| Consumer field renames | +3 | WR:961 (`maybeData.complete` → `.isComplete`), WR:1168 (same), WR:1266 (`data.complete` → `.isComplete`) | +| `import Postbox` drops | 0 | WallpaperResources retains import for unrelated symbols | + +**Total commit footprint:** 6 line edits in 1 file, plus a docs commit for the outcome log. diff --git a/docs/superpowers/plans/2026-04-26-postbox-wave-105-device-contact-info-subject-engine-peer.md b/docs/superpowers/plans/2026-04-26-postbox-wave-105-device-contact-info-subject-engine-peer.md new file mode 100644 index 0000000000..a191faea48 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-postbox-wave-105-device-contact-info-subject-engine-peer.md @@ -0,0 +1,489 @@ +# Wave 105: DeviceContactInfoSubject enum payload Peer? → EnginePeer? Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Migrate `DeviceContactInfoSubject` enum's 3 case payloads + 2 callback signatures + 1 computed property from raw Postbox `Peer?` to `EnginePeer?`. Wave 105 of the Postbox → TelegramEngine refactor. + +**Architecture:** Multi-module enum-payload migration (wave-91 shape). 17 edits across 5 files. AccountContext.swift hosts the enum + property. DeviceContactInfoController.swift is the primary consumer. 4 construction sites in TelegramUI/PeerInfoUI/StoryContainerScreen/ChatController. Net wrap delta: −8 (drops 10, adds 2 at Chat-side construction barriers documented per spec). + +**Tech Stack:** Swift, Bazel via `Make.py`, no unit tests. Verification is the full-project debug-sim-arm64 build. + +**Iteration budget:** 1-3 (wave-91 precedent: 2 iter for similar shape). + +**Note on TDD:** No unit tests in this project. Each task writes the edits, then verifies via Bazel build + residue grep. + +--- + +## File Structure + +| File | Role | Edits | +|---|---|---| +| `submodules/AccountContext/Sources/AccountContext.swift` | Enum definition + computed property | 4 type-line edits | +| `submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift` | Primary consumer | 9 edits (5 `_asPeer` drops + 3 `.flatMap` simplifications + 1 downcast rewrite) | +| `submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift` | Chat-side construction (Pattern E ADD bridges) | 1 Edit (replace_all=true covers 2 sites) | +| `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift` | Story-side construction | 1 edit | +| `submodules/TelegramUI/Sources/OpenChatMessage.swift` | OpenChatMessage construction | 1 edit | + +--- + +## Task 1: AccountContext.swift — enum + computed property type changes + +**File:** `submodules/AccountContext/Sources/AccountContext.swift` + +- [ ] **Step 1: Migrate the 3 enum case payloads (single Edit covers consecutive lines)** + +Find: + +```swift +public enum DeviceContactInfoSubject { + case vcard(Peer?, DeviceContactStableId?, DeviceContactExtendedData) + case filter(peer: Peer?, contactId: DeviceContactStableId?, contactData: DeviceContactExtendedData, completion: (Peer?, DeviceContactExtendedData) -> Void) + case create(peer: Peer?, contactData: DeviceContactExtendedData, isSharing: Bool, shareViaException: Bool, completion: (Peer?, DeviceContactStableId, DeviceContactExtendedData) -> Void) + + public var peer: Peer? { +``` + +Replace with: + +```swift +public enum DeviceContactInfoSubject { + case vcard(EnginePeer?, DeviceContactStableId?, DeviceContactExtendedData) + case filter(peer: EnginePeer?, contactId: DeviceContactStableId?, contactData: DeviceContactExtendedData, completion: (EnginePeer?, DeviceContactExtendedData) -> Void) + case create(peer: EnginePeer?, contactData: DeviceContactExtendedData, isSharing: Bool, shareViaException: Bool, completion: (EnginePeer?, DeviceContactStableId, DeviceContactExtendedData) -> Void) + + public var peer: EnginePeer? { +``` + +This single Edit covers all 4 type-line changes in `AccountContext.swift`. The `contactData: DeviceContactExtendedData` computed property (lines 719-727) is unaffected. + +--- + +## Task 2: DeviceContactInfoController.swift — Pattern D downcast rewrite (1 edit) + +**File:** `submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift` + +- [ ] **Step 1: Rewrite the `as? TelegramUser` downcast at line 849** + +Find: + +```swift + if let peer = peer as? TelegramUser { +``` + +Replace with: + +```swift + if case let .user(peer) = peer { +``` + +The leading whitespace (8 spaces) must match exactly. The outer `peer: EnginePeer?` (from `case let .create(peer, ...) = subject` at L845) is shadowed inside the if-body by `peer: TelegramUser` (the `.user` case associated value). Inner body access (`peer.firstName`, `peer.lastName`, `peer.phone`) works on the rebinding. + +--- + +## Task 3: DeviceContactInfoController.swift — Pattern C `.flatMap` simplifications (3 edits) + +**File:** `submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift` + +- [ ] **Step 1: Simplify `.vcard` case body at line 942** + +Find: + +```swift + case let .vcard(peer, id, data): + contactData = .single((peer.flatMap(EnginePeer.init), id, data)) +``` + +Replace with: + +```swift + case let .vcard(peer, id, data): + contactData = .single((peer, id, data)) +``` + +- [ ] **Step 2: Simplify `.filter` case body at line 944** + +Find: + +```swift + case let .filter(peer, id, data, _): + contactData = .single((peer.flatMap(EnginePeer.init), id, data)) +``` + +Replace with: + +```swift + case let .filter(peer, id, data, _): + contactData = .single((peer, id, data)) +``` + +- [ ] **Step 3: Simplify `.create` case body at line 946** + +Find: + +```swift + case let .create(peer, data, share, shareViaExceptionValue, _): + contactData = .single((peer.flatMap(EnginePeer.init), nil, data)) +``` + +Replace with: + +```swift + case let .create(peer, data, share, shareViaExceptionValue, _): + contactData = .single((peer, nil, data)) +``` + +After Task 1's enum migration, the destructured `peer: EnginePeer?` is the target type — `.flatMap(EnginePeer.init)` becomes a redundant round-trip. + +--- + +## Task 4: DeviceContactInfoController.swift — Pattern B `_asPeer` drops at completion calls (2 edits) + +**File:** `submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift` + +- [ ] **Step 1: Drop `_asPeer()` at completion call line 1105** + +Find: + +```swift + completion(peerAndContactData.0?._asPeer(), filteredData) +``` + +Replace with: + +```swift + completion(peerAndContactData.0, filteredData) +``` + +`peerAndContactData.0` is `EnginePeer?` from the typed signal at L939. Completion's first parameter type changes from `Peer?` to `EnginePeer?` per Task 1. + +- [ ] **Step 2: Drop `_asPeer()` at completion call line 1224** + +Find: + +```swift + completion(contactIdAndData.2?._asPeer(), contactIdAndData.0, contactIdAndData.1) +``` + +Replace with: + +```swift + completion(contactIdAndData.2, contactIdAndData.0, contactIdAndData.1) +``` + +`contactIdAndData.2` is `EnginePeer?` per the typed signal `(DeviceContactStableId, DeviceContactExtendedData, EnginePeer?)?` declared at L1175. + +--- + +## Task 5: DeviceContactInfoController.swift — Pattern A `_asPeer` drops at construction (3 edits) + +**File:** `submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift` + +- [ ] **Step 1: Drop `_asPeer()` at line 1289** + +Find: + +```swift + replaceControllerImpl?(deviceContactInfoController(context: context, environment: environment, subject: .vcard(peer?._asPeer(), contactId, contactData), completed: nil, cancelled: nil)) +``` + +Replace with: + +```swift + replaceControllerImpl?(deviceContactInfoController(context: context, environment: environment, subject: .vcard(peer, contactId, contactData), completed: nil, cancelled: nil)) +``` + +- [ ] **Step 2: Drop `_asPeer()` at line 1443** + +Find: + +```swift + parentController.present(deviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer?._asPeer(), contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in +``` + +Replace with: + +```swift + parentController.present(deviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer, contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in +``` + +- [ ] **Step 3: Drop `_asPeer()` at line 1489** + +Find: + +```swift + controller?.present(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 +``` + +Replace with: + +```swift + controller?.present(context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer, contactData: contactData, isSharing: peer != nil, shareViaException: false, completion: { _, _, _ in +``` + +All 3 sites have `peer` source already typed as `EnginePeer?` per inventory. + +--- + +## Task 6: ChatControllerOpenAttachmentMenu.swift — Pattern E ADD wraps (1 Edit, 2 sites via replace_all=true) + +**File:** `submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift` + +- [ ] **Step 1: Add `.flatMap(EnginePeer.init)` wrap at lines 683 and 1850** + +Use Edit with `replace_all=true`. Find: + +```swift +subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in +``` + +Replace with: + +```swift +subject: .filter(peer: peerAndContactData.0.flatMap(EnginePeer.init), contactId: nil, contactData: contactData, completion: { peer, contactData in +``` + +`replace_all=true` is required — both sites at L683 and L1850 share identical text. The upstream signal type is `(Peer?, DeviceContactExtendedData?)` (verified at L634 and L1822); `.flatMap(EnginePeer.init)` wraps `Peer?` to `EnginePeer?` to satisfy the migrated `.filter(peer: EnginePeer?, ...)` signature. + +--- + +## Task 7: StoryItemSetContainerViewSendMessage.swift — Pattern A `_asPeer` drop (1 edit) + +**File:** `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift` + +- [ ] **Step 1: Drop `_asPeer()` at line 2132** + +Find: + +```swift + let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: component.context), environment: ShareControllerAppEnvironment(sharedContext: component.context.sharedContext), subject: .filter(peer: peerAndContactData.0?._asPeer(), contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in +``` + +Replace with: + +```swift + let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: component.context), environment: ShareControllerAppEnvironment(sharedContext: component.context.sharedContext), subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in +``` + +`peerAndContactData.0` is `EnginePeer?` from the typed signal at this site (the presence of `?._asPeer()` confirms it). + +--- + +## Task 8: OpenChatMessage.swift — Pattern A `_asPeer` drop (1 edit) + +**File:** `submodules/TelegramUI/Sources/OpenChatMessage.swift` + +- [ ] **Step 1: Drop `_asPeer()` at line 443** + +Find: + +```swift + let controller = deviceContactInfoController(context: ShareControllerAppAccountContext(context: params.context), environment: ShareControllerAppEnvironment(sharedContext: params.context.sharedContext), updatedPresentationData: params.updatedPresentationData, subject: .vcard(peer?._asPeer(), nil, contactData), completed: nil, cancelled: nil) +``` + +Replace with: + +```swift + let controller = deviceContactInfoController(context: ShareControllerAppAccountContext(context: params.context), environment: ShareControllerAppEnvironment(sharedContext: params.context.sharedContext), updatedPresentationData: params.updatedPresentationData, subject: .vcard(peer, nil, contactData), completed: nil, cancelled: nil) +``` + +`peer` source is already `EnginePeer?` (the `?._asPeer()` confirms the source type). + +--- + +## Task 9: Full-project Bazel build + +**Files:** none (verification only). + +- [ ] **Step 1: Run the build with `--continueOnError`** + +```sh +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent \ + --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError +``` + +`--continueOnError` enabled — multi-module wave; surface all errors at once if iter-1 fails. + +Expected: clean build. AccountContext is foundational; expect 60-180s build cost. + +- [ ] **Step 2: If build fails, triage iteration** + +Common failure modes (per wave-91 precedent): +- **Type mismatch on a destructured `peer`** — a destructure body may use `peer.X` where `X` is a Peer-protocol-only method not on EnginePeer. Pre-flight inventory found ZERO such sites, but verify the failing line. +- **`.id` access on EnginePeer? doesn't compile** — would indicate an EnginePeer.Id typealias regression (very unlikely; would have failed all prior waves). +- **`case let .user(peer) = peer` doesn't compile** — verify the outer `peer` is `EnginePeer?` (after migration) and not still `Peer?`. +- **A construction site missed an `_asPeer()` drop** — re-grep `_asPeer\(\)` over the 5 touched files. +- **Hidden `Peer?`-typed completion call site** — would indicate an unmigrated callback consumer. Re-grep across consumer module sources. + +If errors land outside the 5 touched files: STOP and report BLOCKED — the wave is supposed to be self-contained. + +Iteration budget: 3. + +--- + +## Task 10: Post-edit residue grep + +**Files:** none (verification only). + +- [ ] **Step 1: Construction-site `_asPeer` residue (expected empty)** + +```sh +grep -nE "subject:\s*\.(vcard|filter|create)\(.*_asPeer\(\)" \ + submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift \ + submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift \ + submodules/TelegramUI/Sources/OpenChatMessage.swift +``` + +- [ ] **Step 2: Completion `_asPeer` residue (expected empty)** + +```sh +grep -nE "completion\(.*_asPeer\(\)" submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +``` + +- [ ] **Step 3: `.flatMap(EnginePeer.init)` simplification residue (expected empty)** + +```sh +grep -nE "peer\.flatMap\(EnginePeer\.init\)" submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +``` + +- [ ] **Step 4: Downcast residue (expected empty)** + +```sh +grep -nE "peer as\? TelegramUser" submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +``` + +- [ ] **Step 5: ADD wraps applied (expected 2 lines)** + +```sh +grep -nE "peerAndContactData\.0\.flatMap\(EnginePeer\.init\)" submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +``` + +Expected: 2 lines (originally L683 and L1850, line numbers may have shifted slightly). + +--- + +## Task 11: Commit the wave + +**Files:** none (git only). + +- [ ] **Step 1: Stage the 5 modified files** + +```sh +git add \ + submodules/AccountContext/Sources/AccountContext.swift \ + submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift \ + submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift \ + submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift \ + submodules/TelegramUI/Sources/OpenChatMessage.swift +``` + +- [ ] **Step 2: Confirm staging** + +```sh +git status --short | grep -v "^??" +``` + +Expected: 5 staged files (lines starting with `M `). The pre-existing `m build-system/bazel-rules/sourcekit-bazel-bsp` WIP marker should NOT appear in staged. + +- [ ] **Step 3: Commit** + +```sh +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 105 + +Migrate DeviceContactInfoSubject enum 3 case Peer? payloads + 2 callback +(Peer?, ...) -> Void signatures + 1 computed peer: Peer? property to +EnginePeer?. Wave-91-pattern multi-module enum-payload migration. + +Drops 10 wraps: +- 5 _asPeer() at construction sites: DeviceContactInfoController:1289, + 1443, 1489 + StoryItemSetContainerViewSendMessage:2132 + + OpenChatMessage:443. +- 2 _asPeer() at completion-call sites: + DeviceContactInfoController:1105, 1224. +- 3 .flatMap(EnginePeer.init) simplifications at + DeviceContactInfoController:942, 944, 946. + +Adds 2 ADD bridges: ChatControllerOpenAttachmentMenu:683, 1850 — both +construct .filter(peer:) from peerAndContactData.0 typed (Peer?, ...); +.flatMap(EnginePeer.init) wraps to EnginePeer?. Net wrap delta: -8. + +Plus 1 downcast rewrite: DeviceContactInfoController:849 — `if let peer += peer as? TelegramUser` to `if case let .user(peer) = peer`. + +5 files / 17 edits. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: Verify commit** + +```sh +git log --oneline -1 +``` + +--- + +## Task 12: Update outcome log + memory + +**Files:** +- Modify: `docs/superpowers/postbox-refactor-log.md` +- Modify: `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md` +- Modify: `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md` + +- [ ] **Step 1: Append wave 105 outcome to refactor log** + +Include: +- Commit hash (from Task 11 step 4). +- Iteration count (1 if first-pass-clean; 2-3 if Task 9 step 2 fired). +- Bazel build duration. +- Net-delta accounting: −10 wrap drops, +2 ADD wraps, +1 downcast rewrite. Net −8 wraps. +- Wave-shape note: wave-91-pattern multi-module enum-payload migration with full pre-flight inventory clearing layers 1-4 of the wave-71-shadow checklist. Documents the value of thorough pre-flight inventory: 17 mechanical edits with 0 surprises. + +- [ ] **Step 2: Update next-wave memory** + +Edit `project_postbox_refactor_next_wave.md`: +- Add wave 105 outcome line into the recent-waves section. +- Mark `DeviceContactInfoSubject` candidate as drained (currently bullet 9 in deferred list). +- Promote next candidate. + +- [ ] **Step 3: Update MEMORY.md index** + +Update the `[Postbox refactor next wave]` line. + +- [ ] **Step 4: Commit the doc update** + +```sh +git add docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: log wave 105 outcome + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +(Memory file updates not committed — they live outside the repo.) + +--- + +## Net delta projection + +| Category | Count | Sites | +|---|---|---| +| `_asPeer()` drops at construction | −5 | DCIC:1289, 1443, 1489 + SISCVSM:2132 + OCM:443 | +| `_asPeer()` drops at completion calls | −2 | DCIC:1105, 1224 | +| `.flatMap(EnginePeer.init)` simplifications | −3 | DCIC:942, 944, 946 | +| `.flatMap(EnginePeer.init)` ADD wraps | +2 | CCOAM:683, 1850 | +| Downcast → case-let | +1 | DCIC:849 | +| Type annotations migrated | 4 | AccountContext: 3 enum cases + 1 computed property | + +**Total commit footprint:** 17 line edits across 5 files, plus a docs commit for the outcome log. + +**Net wrap delta:** **−8** (the wave's headline metric). diff --git a/docs/superpowers/plans/2026-04-26-postbox-wave-106-import-drop-sweep.md b/docs/superpowers/plans/2026-04-26-postbox-wave-106-import-drop-sweep.md new file mode 100644 index 0000000000..b36195daf1 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-postbox-wave-106-import-drop-sweep.md @@ -0,0 +1,493 @@ +# Wave 106: Speculative `import Postbox` Drop Sweep (round 2) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Drop `import Postbox` from any consumer-module Swift file in `submodules/` whose remaining content no longer references a Postbox-only symbol. Wave 106 of the Postbox → TelegramEngine refactor — round 2 of the wave-93 speculative-drop sweep. + +**Architecture:** Procedural sweep with build-feedback loop. (1) inventory candidates → (2) pre-flight regex pre-restore → (3) drop imports en masse → (4) build with `--continueOnError` → (5) restore failures → iterate → (6) final clean build → (7) optional BUILD-dep sweep → (8) single atomic commit. No code semantic changes — only `import` and BUILD `deps` lines. + +**Tech Stack:** Swift, Bazel via `Make.py`, `grep`/`sed` for inventory, no unit tests. Verification is the full-project debug-sim-arm64 build. + +**Iteration budget:** 2-5 build cycles (wave-93 precedent: 2 iter — drop, restore, clean). + +**Note on TDD:** No unit tests in this project. Each task verifies via Bazel build + diff inspection. Build feedback IS the test. + +**Spec:** `docs/superpowers/specs/2026-04-26-postbox-wave-106-import-drop-sweep-design.md`. + +--- + +## File Structure + +| Artifact | Role | +|---|---| +| `/tmp/wave106-candidates.txt` | All consumer files currently `import Postbox` | +| `/tmp/wave106-skiplist.txt` | Files that match preemptive-restore regex (keep import) | +| `/tmp/wave106-droplist.txt` | candidates − skiplist; files to edit | +| `/tmp/wave106-build-iterN.log` | Per-iteration build log | +| `/tmp/wave106-restore-iterN.txt` | Files needing restore after iter N | +| `/tmp/wave106-final-droplist.txt` | Net dropped files after all iterations | +| `submodules/**/*.swift` | Edited files (single-line Edit each) | +| `submodules/**/BUILD` | (Optional Step 7) packages with no remaining Postbox imports | + +--- + +## Task 1: Pre-flight WIP check + +**File:** none (read-only). + +- [ ] **Step 1: Verify clean working tree (modulo known-persistent state)** + +Run: + +```sh +git status --short +``` + +Expected output: + +``` + m build-system/bazel-rules/sourcekit-bazel-bsp +?? build-system/tulsi/ +?? submodules/TgVoip/ +?? third-party/libx264/ +``` + +If output contains anything else (modified `M` files, other untracked dirs), HALT — there is unrelated WIP that would get tangled with the wave commit. Resolve before proceeding. + +- [ ] **Step 2: Confirm we are on `master`** + +Run: + +```sh +git branch --show-current +``` + +Expected: `master`. If not, stop and ask. + +--- + +## Task 2: Inventory candidate files + +**File:** none (read-only). + +- [ ] **Step 1: Build the candidate list** + +Run: + +```sh +grep -rl "^import Postbox" submodules --include="*.swift" \ + | grep -v "^submodules/Postbox/" \ + | grep -v "^submodules/TelegramCore/" \ + | grep -v "^submodules/TelegramApi/" \ + | sort -u > /tmp/wave106-candidates.txt +wc -l /tmp/wave106-candidates.txt +``` + +Expected: between ~700 and ~1200 files (wave-93-era was ~1200; waves 94-105 may have peeled some). + +- [ ] **Step 2: Sanity-check the exclusion filters worked** + +Run: + +```sh +grep -E "^submodules/(Postbox|TelegramCore|TelegramApi)/" /tmp/wave106-candidates.txt | head -5 +``` + +Expected: empty output (no excluded paths leaked through). + +--- + +## Task 3: Build the skip-list via preemptive regex + +**File:** none (read-only). + +- [ ] **Step 1: Run the combined skip-regex against candidates** + +The skip-regex is the union of three tiers from the spec. Run: + +```sh +grep -El "\bPostbox\b|\bMediaBox\b|\bMediaResource\b|\bMediaResourceData\b|\bMediaResourceId\b|\bPostboxCoding\b|\bPostboxDecoder\b|\bPostboxEncoder\b|\bMemoryBuffer\b|\bTempBoxFile\b|\bValueBoxKey\b|\bPostboxView\b|\bcombinedView\b|\bPeerId\b|\bMessageId\b|\bMediaId\b|\bMessageIndex\b|\bMessageAndThreadId\b|\bPeerNameIndex\b|\bStoryId\b|\bItemCollectionId\b|\bFetchResourceSourceType\b|\bFetchResourceError\b|\bPeer\b|\bMessage\b|\bMedia\b" \ + $(cat /tmp/wave106-candidates.txt) \ + | sort -u > /tmp/wave106-skiplist.txt +wc -l /tmp/wave106-skiplist.txt +``` + +Expected: most of the candidate list (likely 600-1100 files matched) — `\bPeer\b`, `\bMessage\b`, `\bMedia\b` are deliberately broad and catch many false positives. False positives are SAFE — they just mean fewer drops, not bad drops. + +- [ ] **Step 2: Compute the drop-list** + +Run: + +```sh +comm -23 /tmp/wave106-candidates.txt /tmp/wave106-skiplist.txt > /tmp/wave106-droplist.txt +wc -l /tmp/wave106-droplist.txt +head -20 /tmp/wave106-droplist.txt +``` + +Expected: 5-50 files in the drop-list (wave 93 had 12). If 0, the regex is over-matching — halt and revisit. If >100, the regex is under-matching — halt, expand patterns, re-run. + +- [ ] **Step 3: Spot-verify 3 random drop candidates** + +Run for each of 3 files from the head of the drop-list: + +```sh +head -3 /tmp/wave106-droplist.txt | while read f; do + echo "=== $f ===" + grep -nE "Postbox|MediaBox|MediaResource|PeerId|MessageId|MediaId|MessageIndex" "$f" | head -5 +done +``` + +Expected: Only `import Postbox` line appears. If any other Postbox-token appears, the file should have been skipped — add the missing pattern to the regex in Step 1, redo Steps 1-2, and re-spot-check. + +--- + +## Task 4: Drop `import Postbox` from drop-list files + +**Files:** every path listed in `/tmp/wave106-droplist.txt`. + +- [ ] **Step 1: Read each drop-list file's import block to locate the exact `import Postbox` line** + +For each file in the drop-list, the line is `import Postbox` (exact match, no whitespace variations expected). Use a single-purpose `sed` to remove it from all drop-list files: + +```sh +while read f; do + sed -i '' '/^import Postbox$/d' "$f" +done < /tmp/wave106-droplist.txt +``` + +The `sed -i ''` syntax is BSD/macOS specific — required on Darwin. + +- [ ] **Step 2: Verify the imports were removed** + +Run: + +```sh +grep -lE "^import Postbox$" $(cat /tmp/wave106-droplist.txt) | wc -l +``` + +Expected: 0 (no file in the drop-list still contains `import Postbox`). + +- [ ] **Step 3: Verify no other lines were touched** + +Run: + +```sh +git diff --stat | tail -5 +git diff --shortstat +``` + +Expected: same number of files modified as drop-list size. Each file should show `-1` insertion (or `-1` deletion). If any file shows multiple deletions, something went wrong — `git checkout -- $(cat /tmp/wave106-droplist.txt)` and investigate. + +--- + +## Task 5: Build iteration 1 — capture failures + +**File:** none (build only). + +- [ ] **Step 1: Run the build with `--continueOnError`** + +Run: + +```sh +source ~/.zshrc 2>/dev/null && \ +python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 \ + --configuration=debug_sim_arm64 \ + --continueOnError 2>&1 | tee /tmp/wave106-build-iter1.log +``` + +Expected: Build completes (with errors). Wall-clock 30-260s depending on cache state. + +- [ ] **Step 2: Extract failing files** + +Run: + +```sh +grep -E ":[0-9]+:[0-9]+: error:" /tmp/wave106-build-iter1.log \ + | awk -F: '{print $1}' \ + | sort -u > /tmp/wave106-restore-iter1.txt +wc -l /tmp/wave106-restore-iter1.txt +cat /tmp/wave106-restore-iter1.txt +``` + +Expected: a subset of the drop-list. Wave 93 saw 5 of 12 needing restore. If the count > 50% of drop-list, the regex is missing a major pattern — HALT, analyze the failure cluster, add the missing pattern to Task 3 Step 1, restart from Task 4. + +- [ ] **Step 3: Verify no errors in TelegramCore/Postbox/TelegramApi** + +Run: + +```sh +grep -E "^submodules/(TelegramCore|Postbox|TelegramApi)/" /tmp/wave106-restore-iter1.txt +``` + +Expected: empty. If non-empty: HALT immediately, `git checkout -- submodules/`, and revert the wave — scope drift indicates the candidate filter or sed pattern is wrong. + +--- + +## Task 6: Restore failing files (iter 1) + +**Files:** every path in `/tmp/wave106-restore-iter1.txt`. + +- [ ] **Step 1: Re-add `import Postbox` to each failing file** + +Use awk uniformly (BSD `sed -i '' 'i\'` line-continuation is fragile inside shell loops). Insert `import Postbox` immediately before `import TelegramCore` if present, else immediately after the first existing `import ` line: + +```sh +while read f; do + awk ' + BEGIN { added = 0 } + !added && /^import TelegramCore$/ { print "import Postbox"; print; added = 1; next } + { print } + END { + if (!added) { + # fallback path was not used — try post-first-import injection + # (this END block is a no-op; awk cannot re-emit lines after END) + } + } + ' "$f" > "$f.tmp" + if ! grep -q "^import Postbox$" "$f.tmp"; then + # no TelegramCore anchor found — fall back to "after first import" + awk ' + BEGIN { added = 0 } + !added && /^import / { print; print "import Postbox"; added = 1; next } + { print } + ' "$f" > "$f.tmp" + fi + mv "$f.tmp" "$f" +done < /tmp/wave106-restore-iter1.txt +``` + +- [ ] **Step 2: Verify restorations** + +Run: + +```sh +grep -L "^import Postbox$" $(cat /tmp/wave106-restore-iter1.txt) +``` + +Expected: empty (every file in the restore list now contains `import Postbox` again). + +- [ ] **Step 3: Update the working drop-list** + +Run: + +```sh +comm -23 /tmp/wave106-droplist.txt /tmp/wave106-restore-iter1.txt > /tmp/wave106-final-droplist.txt +wc -l /tmp/wave106-final-droplist.txt +``` + +This is the current "successfully dropped" set. + +--- + +## Task 7: Build iteration 2 — verify clean (or iterate further) + +**File:** none (build only). + +- [ ] **Step 1: Re-run the build with `--continueOnError`** + +Run: + +```sh +source ~/.zshrc 2>/dev/null && \ +python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 \ + --configuration=debug_sim_arm64 \ + --continueOnError 2>&1 | tee /tmp/wave106-build-iter2.log +``` + +- [ ] **Step 2: Extract any new failures** + +Run: + +```sh +grep -E ":[0-9]+:[0-9]+: error:" /tmp/wave106-build-iter2.log \ + | awk -F: '{print $1}' \ + | sort -u > /tmp/wave106-restore-iter2.txt +wc -l /tmp/wave106-restore-iter2.txt +``` + +- [ ] **Step 3: If non-empty, repeat Task 6 with `iter2.txt` and run Task 7 again as iter3.** + +Stop when: +- `restore-iterN.txt` is empty → proceed to Task 8. +- `N == 5` → HALT (diminishing returns); commit what is green via Task 9. + +Each repeat: substitute `iter1` → `iter2` → `iter3` etc. throughout. Update the final-droplist after each restore: `comm -23 /tmp/wave106-final-droplist.txt /tmp/wave106-restore-iterN.txt > /tmp/wave106-final-droplist.txt.new && mv /tmp/wave106-final-droplist.txt.new /tmp/wave106-final-droplist.txt`. + +--- + +## Task 8: Final clean build (no `--continueOnError`) + +**File:** none (build only). + +- [ ] **Step 1: Run a clean build to confirm no inter-module ordering issue was masked** + +Run: + +```sh +source ~/.zshrc 2>/dev/null && \ +python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 \ + --configuration=debug_sim_arm64 2>&1 | tail -30 +``` + +Expected: build success, no `error:` lines in the tail. If failure: an inter-module visibility issue exists that `--continueOnError` masked. Restore the final-droplist file(s) implicated by the error, repeat Task 7 / Task 8. + +--- + +## Task 9 (optional): BUILD-dep sweep + +**Files:** various `submodules/*/BUILD` files. + +This step removes `//submodules/Postbox` from any Bazel package whose Swift sources no longer contain `import Postbox`. Skip this task if iteration time is constrained — the import drops alone are the core wave value; deps trim is housekeeping. + +- [ ] **Step 1: Find packages whose `BUILD` still depends on `//submodules/Postbox` but whose `Sources/**/*.swift` no longer imports it** + +Run: + +```sh +for build in $(find submodules -name BUILD -not -path "submodules/Postbox/*"); do + pkg_dir=$(dirname "$build") + if grep -q "//submodules/Postbox" "$build" 2>/dev/null; then + if ! grep -rq "^import Postbox$" "$pkg_dir" --include="*.swift" 2>/dev/null; then + echo "$build" + fi + fi +done > /tmp/wave106-build-deps-candidates.txt +wc -l /tmp/wave106-build-deps-candidates.txt +cat /tmp/wave106-build-deps-candidates.txt +``` + +Expected: 0-5 candidates. If 0, skip to Task 10. + +- [ ] **Step 2: For each candidate BUILD, locate the `//submodules/Postbox` line and remove it via Edit** + +Note that BUILD files often list deps as either `"//submodules/Postbox"` (string) or via aliases. Use Read to inspect each, then Edit to drop just the dep line. The exact string pattern varies — typically `"//submodules/Postbox",` on its own line within a `deps = [ ... ]` block. + +For each candidate file, Read the lines around the match, then Edit to remove the line preserving the surrounding bracket structure. + +- [ ] **Step 3: Re-run the clean build (no `--continueOnError`) to confirm no dep was load-bearing** + +Run the same command as Task 8 Step 1. + +If failure: a transitive dep was being satisfied through Postbox. Restore the dep line(s) implicated by the error and re-run. + +--- + +## Task 10: Commit the wave + +**File:** `git`. + +- [ ] **Step 1: Inspect final diff statistics** + +Run: + +```sh +git diff --stat +git diff --shortstat +``` + +Expected: N file modifications, all `-1` line changes (just `import Postbox` lines), possibly plus a small number of BUILD diffs from Task 9. + +- [ ] **Step 2: Confirm only allowed paths are touched** + +Run: + +```sh +git diff --name-only | grep -vE "^submodules/" | head -5 +git diff --name-only | grep -E "^submodules/(Postbox|TelegramCore|TelegramApi)/" | head -5 +``` + +Both expected: empty. If either has output: HALT — rogue changes exist; investigate before committing. + +- [ ] **Step 3: Stage only the modified Swift and BUILD files** + +Run: + +```sh +git add $(git diff --name-only) +git status --short +``` + +Expected: all changes staged with `M`. Untracked dirs (`build-system/tulsi/`, `submodules/TgVoip/`, `third-party/libx264/`) and the `m` submodule marker remain untouched. + +- [ ] **Step 4: Commit with the wave message** + +Substitute `` with the count from `/tmp/wave106-final-droplist.txt`, `` with the total restored across iterations, and `` with the BUILD deps removed (0 if Task 9 skipped). + +```sh +N=$(wc -l < /tmp/wave106-final-droplist.txt | tr -d ' ') +M=$(cat /tmp/wave106-restore-iter*.txt 2>/dev/null | sort -u | wc -l | tr -d ' ') +K=$(git diff --cached --name-only | grep -c BUILD) + +git commit -m "$(cat < TelegramEngine wave 106 (import drop sweep round 2) + +Speculative drop of \`import Postbox\` in $N files where the last +Postbox-typed symbol reference was peeled off by waves 94-105. +Methodology: pattern-based pre-flight skip + drop + build-feedback +restore loop (wave-93-validated recipe). $M files restored after build. +$K BUILD deps removed. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +git status --short +``` + +Expected: clean commit, working tree returns to the known-persistent untracked-only state. + +--- + +## Task 11: Update memory file with wave outcome + +**File:** `/Users/isaac/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`. + +- [ ] **Step 1: Read the current memory file to find the recent-commits section** + +Read the top section listing wave commits. + +- [ ] **Step 2: Insert a wave-106 line below the wave-105 entry** + +Append (substituting the actual commit hash from `git log -1 --format=%H | head -c 10`): + +```markdown +- `` — wave 106: speculative `import Postbox` drop sweep round 2. files dropped, restored after build feedback. Methodology re-run of wave 93 (`72de7c4fd5`) with expanded pre-flight regex (added bare-name escapes `\bPeer\b`/`\bMessage\b`/`\bMedia\b` per wave-93 lesson). BUILD deps removed. build cycles. +``` + +Update the memory file's `description:` frontmatter to reflect wave 106 as the latest. + +--- + +## Halt-and-revert recipe (if anything goes seriously wrong) + +If at any point the build fails in TelegramCore/Postbox/TelegramApi, or iteration count exceeds 5 with non-trivial residue, or scope drifts beyond the spec: + +```sh +git checkout -- submodules/ +git status --short # should match the pre-flight expected output +``` + +The wave is fully reversible until Task 10 commits. + +--- + +## Plan Self-Review Notes + +- **Spec coverage:** Tasks 1-11 map 1:1 to the 8-step procedure in the spec plus pre-flight (Task 1) and post-commit memory update (Task 11). Halt conditions appear in Tasks 5/7/9 and the final halt-and-revert recipe. +- **Placeholder scan:** No TBDs/TODOs. All ``/``/``/`` are explicitly substituted via shell expansion in Step 4 of Task 10. +- **Type/method consistency:** Single-purpose tasks operating on filesystem and grep — no method-name drift risk. +- **Iteration shape:** Tasks 5-7 form the iteration loop; Task 8 is the validation gate; Task 9 is optional housekeeping; Task 10 commits. diff --git a/docs/superpowers/plans/2026-04-26-postbox-wave-106-pivot-engine-data-incremental.md b/docs/superpowers/plans/2026-04-26-postbox-wave-106-pivot-engine-data-incremental.md new file mode 100644 index 0000000000..fdf4edb3b9 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-postbox-wave-106-pivot-engine-data-incremental.md @@ -0,0 +1,223 @@ +# Wave 106 Pivot: engine `data(resource:incremental:)` facade extension + 1-site drain + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Pivot wave 106 from the abandoned import-drop sweep to a small facade-extension wave. Add an `incremental: Bool = false` parameter to `TelegramEngine.Resources.data(resource:)`, drain the 1 live `account.postbox.mediaBox.resourceData(..., option: .incremental(...))` consumer site (`ChatInterfaceStateContextMenus.swift:1327`). + +**Background — wave 106 (pure sweep) abandoned 2026-04-26:** Inventory of 576 candidate files showed every one of them legitimately references at least one Postbox-tier token (Postbox/MediaBox/MediaResource/protocol-Peer/protocol-Message/protocol-Media/typealiased identifier). Wave 93's pure import-sweep pattern is exhausted at the file granularity — no single-file orphans remain. See spec `docs/superpowers/specs/2026-04-26-postbox-wave-106-import-drop-sweep-design.md` (committed) for the abandoned methodology. + +**Architecture:** Wave-shape G (facade addition + small validation drain). 2 file edits (1 in TelegramCore, 1 in TelegramUI). Single-iter expected. + +**Tech Stack:** Swift, Bazel via `Make.py`, no unit tests. Verification is the full-project debug-sim-arm64 build. + +**Iteration budget:** 1-2 (TelegramCore touch incurs ~210-260s build). + +--- + +## File Structure + +| File | Role | Edits | +|---|---|---| +| `submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift` | Add `incremental` param to `data(resource:)` facade | 1 Edit (signature + body) | +| `submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift` | Migrate L1327 call site | 1 Edit (call + `data.complete` → `data.isComplete`) | + +--- + +## Task 1: Extend the engine `data(resource:)` facade with `incremental:` parameter + +**File:** `submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift` + +- [ ] **Step 1: Edit the `data(resource:)` facade signature and body** + +Find at line 453-466: + +```swift + public func data( + resource: EngineMediaResource, + pathExtension: String? = nil, + waitUntilFetchStatus: Bool = false, + attemptSynchronously: Bool = false + ) -> Signal { + return self.account.postbox.mediaBox.resourceData( + resource._asResource(), + pathExtension: pathExtension, + option: .complete(waitUntilFetchStatus: waitUntilFetchStatus), + attemptSynchronously: attemptSynchronously + ) + |> map { EngineMediaResource.ResourceData($0) } + } +``` + +Replace with: + +```swift + public func data( + resource: EngineMediaResource, + pathExtension: String? = nil, + waitUntilFetchStatus: Bool = false, + incremental: Bool = false, + attemptSynchronously: Bool = false + ) -> Signal { + let option: MediaBoxFetchDataOption = incremental + ? .incremental(waitUntilFetchStatus: waitUntilFetchStatus) + : .complete(waitUntilFetchStatus: waitUntilFetchStatus) + return self.account.postbox.mediaBox.resourceData( + resource._asResource(), + pathExtension: pathExtension, + option: option, + attemptSynchronously: attemptSynchronously + ) + |> map { EngineMediaResource.ResourceData($0) } + } +``` + +The `incremental` parameter is inserted between `waitUntilFetchStatus` and `attemptSynchronously`. Existing call sites passing only labeled-or-trailing arguments remain compatible because Swift requires labels for these (no positional ordering issue). + +--- + +## Task 2: Migrate the consumer call site + +**File:** `submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift` + +- [ ] **Step 1: Edit L1327 — replace `account.postbox.mediaBox.resourceData(...)` with engine facade** + +Find at line 1327: + +```swift + let _ = (context.account.postbox.mediaBox.resourceData(largest.resource, option: .incremental(waitUntilFetchStatus: false)) +``` + +Replace with: + +```swift + let _ = (context.engine.resources.data(resource: EngineMediaResource(largest.resource), incremental: true) +``` + +- [ ] **Step 2: Rename downstream `data.complete` field access to `data.isComplete`** + +Find at line 1330: + +```swift + if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { +``` + +Replace with: + +```swift + if data.isComplete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { +``` + +The `.path` field name is unchanged (both `MediaResourceData` and `EngineMediaResource.ResourceData` use `path`). + +--- + +## Task 3: Verify residue and build + +- [ ] **Step 1: Residue grep for the migrated expression** + +Run: + +```sh +grep -nE "context\.account\.postbox\.mediaBox\.resourceData\(.*option: \.incremental" submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +``` + +Expected: empty. + +- [ ] **Step 2: Verify the new facade signature compiles without breaking existing callers** + +Existing call sites of `engine.resources.data(resource:)` use these forms (per wave 32+ history): +- `engine.resources.data(resource: EngineMediaResource(x))` — default args, fine +- `engine.resources.data(resource: EngineMediaResource(x), pathExtension: "ext")` — labeled, fine +- `engine.resources.data(resource: EngineMediaResource(x), waitUntilFetchStatus: true)` — labeled, fine + +Adding `incremental: Bool = false` between `waitUntilFetchStatus` and `attemptSynchronously` doesn't reorder existing call sites because all parameters use labels. Confirm with grep: + +```sh +grep -rnE "engine\.resources\.data\(resource:" submodules --include="*.swift" | wc -l +``` + +Just for visibility — number of existing call sites that should remain green. + +- [ ] **Step 3: Run the full clean build** + +Run: + +```sh +source ~/.zshrc 2>/dev/null && \ +python3 build-system/Make/Make.py --overrideXcodeVersion \ + --cacheDir ~/telegram-bazel-cache \ + build \ + --configurationPath build-system/appstore-configuration.json \ + --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \ + --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 \ + --configuration=debug_sim_arm64 2>&1 | tail -30 +``` + +Expected: build success, no errors. If failure, fix in place and re-run (single iter expected). + +--- + +## Task 4: Commit the wave + +- [ ] **Step 1: Inspect diff** + +Run: + +```sh +git diff --stat +``` + +Expected: 2 files modified. + +- [ ] **Step 2: Stage and commit** + +```sh +git add submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift \ + submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift + +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 106 (pivot: engine data() incremental facade + 1-site drain) + +Original wave 106 pure import-drop sweep abandoned: 576 candidate files +all genuinely reference Postbox-tier tokens; wave 93's pattern exhausted +at file granularity (no single-file orphans remain). + +Pivot: extend engine.resources.data(resource:) facade with +`incremental: Bool = false` parameter. Drain the 1 live consumer site +(ChatInterfaceStateContextMenus:1327) plus consumer-side +`data.complete` -> `data.isComplete` rename. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 5: Update memory file + +**File:** `/Users/isaac/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md` + +- [ ] **Step 1: Update frontmatter description and append wave 106 entries** + +Update the `description:` line to reflect wave 106 (pivot). + +Append two lines under the recent-commits section: +- One for the abandoned wave 106 import-drop sweep with the key finding (sweep pattern exhausted, save future sessions a re-attempt). +- One for the wave 106 pivot commit hash with cost/yield. + +- [ ] **Step 2: Append a "Wave 106 ABANDONED" subsection** documenting the import-sweep exhaustion finding so future sessions don't re-attempt the pure sweep shape. Note the regex set tested and the conclusion ("any consumer file with `import Postbox` legitimately needs at least one Tier-1/Tier-2 token"). + +--- + +## Halt-and-revert recipe + +If build fails for non-trivial reasons (more than 1 iter): + +```sh +git checkout -- submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift \ + submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +git status --short +``` + +Wave is reversible until Task 4 commits. diff --git a/docs/superpowers/plans/2026-04-30-typing-draft-send-delay.md b/docs/superpowers/plans/2026-04-30-typing-draft-send-delay.md new file mode 100644 index 0000000000..c2e2c5d5f3 --- /dev/null +++ b/docs/superpowers/plans/2026-04-30-typing-draft-send-delay.md @@ -0,0 +1,703 @@ +# Typing-Draft Send Delay Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Park outgoing messages in `PendingMessageManager` after their content is uploaded, releasing them only when the typing-draft for the matching `(peerId, threadId)` clears. + +**Architecture:** Add a per-account Postbox view (`.allTypingDrafts`) that exposes the live `Set` of currently-active typing drafts. Subscribe once at `PendingMessageManager.init`. Add a parked state (`.waitingForSendGate`) to `PendingMessageState` and a forward parking lot dictionary on the manager. Gate at three sites (single-send, album-send, forward-send); drain on view updates that remove keys. + +**Tech Stack:** Swift, Bazel, Postbox view system, SwiftSignalKit. + +**Spec:** `docs/superpowers/specs/2026-04-30-typing-draft-send-delay-design.md` + +**Build verification command (used by every task that compiles code):** + +```sh +source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError +``` + +This codebase has **no unit tests** (per `CLAUDE.md`). Verification is "full build green" at each step plus manual exercise after final task. + +--- + +## File Structure + +**Files created:** +- `submodules/Postbox/Sources/AllTypingDraftsView.swift` — new Postbox view tracking the set of active typing-draft keys. + +**Files modified:** +- `submodules/Postbox/Sources/Views.swift` — register the new view key. +- `submodules/TelegramCore/Sources/State/PendingMessageManager.swift` — gate insertion, drain, subscription, parked-state plumbing. + +**No BUILD edits needed:** Postbox `BUILD` uses `glob(["Sources/**/*.swift"])`, so the new file is auto-picked up. + +--- + +## Task 1: Add `MutableAllTypingDraftsView` / `AllTypingDraftsView` + +**Files:** +- Create: `submodules/Postbox/Sources/AllTypingDraftsView.swift` + +This task adds the Postbox view file. It is **not yet wired** into `PostboxViewKey` (Task 2), so this commit alone will not change behavior. + +- [ ] **Step 1: Create `AllTypingDraftsView.swift`** + +```swift +import Foundation + +final class MutableAllTypingDraftsView: MutablePostboxView { + fileprivate var keys: Set + + init(postbox: PostboxImpl) { + self.keys = Set(postbox.currentTypingDrafts.keys) + } + + func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool { + if transaction.updatedTypingDrafts.isEmpty { + return false + } + var updated = false + for (key, update) in transaction.updatedTypingDrafts { + if update.value != nil { + if self.keys.insert(key).inserted { + updated = true + } + } else { + if self.keys.remove(key) != nil { + updated = true + } + } + } + return updated + } + + func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool { + let new = Set(postbox.currentTypingDrafts.keys) + if new == self.keys { + return false + } + self.keys = new + return true + } + + func immutableView() -> PostboxView { + return AllTypingDraftsView(self) + } +} + +public final class AllTypingDraftsView: PostboxView { + public let keys: Set + + init(_ view: MutableAllTypingDraftsView) { + self.keys = view.keys + } +} +``` + +- [ ] **Step 2: Verify the build (file is unwired, so it must compile standalone)** + +Run the build command from the header. Expected: **success**. Common failure modes: `currentTypingDrafts` access scope (already `fileprivate(set)`, accessible from same-module file); `transaction.updatedTypingDrafts` shape (already `[PeerAndThreadId: PostboxImpl.TypingDraftUpdate]` per `PostboxTransaction.swift:59`). + +- [ ] **Step 3: Commit** + +```bash +git add submodules/Postbox/Sources/AllTypingDraftsView.swift +git commit -m "Postbox: add AllTypingDraftsView tracking Set of active typing drafts" +``` + +--- + +## Task 2: Register `.allTypingDrafts` in `PostboxViewKey` + +**Files:** +- Modify: `submodules/Postbox/Sources/Views.swift` + +- [ ] **Step 1: Add the case to the enum** + +In `submodules/Postbox/Sources/Views.swift` line 105, append `.allTypingDrafts` after `.typingDrafts(...)`: + +```swift + case typingDrafts(PeerAndThreadId) + case allTypingDrafts +``` + +- [ ] **Step 2: Add hash arm** + +In the `hash(into:)` switch (the new arm goes alongside the existing `case let .typingDrafts(peerId):` arm at line 232–233): + +```swift + case let .typingDrafts(peerId): + hasher.combine(peerId) + case .allTypingDrafts: + hasher.combine(22) +``` + +The constant `22` is the next free hash-tag (existing tags use 0–21). + +- [ ] **Step 3: Add `==` arm** + +In the `==(lhs:rhs:)` switch (line 551–556 area): + +```swift + case let .typingDrafts(peerId): + if case .typingDrafts(peerId) = rhs { + return true + } else { + return false + } + case .allTypingDrafts: + if case .allTypingDrafts = rhs { + return true + } else { + return false + } +``` + +- [ ] **Step 4: Wire `postboxViewForKey`** + +In the `postboxViewForKey(postbox:key:)` switch (line 684–685 area): + +```swift + case let .typingDrafts(peerId): + return MutableTypingDraftsView(postbox: postbox, peerAndThreadId: peerId) + case .allTypingDrafts: + return MutableAllTypingDraftsView(postbox: postbox) + } +} +``` + +- [ ] **Step 5: Verify the build** + +Run the build command. Expected: **success**. The view is now constructible but no consumer subscribes yet. + +- [ ] **Step 6: Commit** + +```bash +git add submodules/Postbox/Sources/Views.swift +git commit -m "Postbox: wire .allTypingDrafts view key into PostboxViewKey" +``` + +--- + +## Task 3: Add `.waitingForSendGate` state and update related switches + +**Files:** +- Modify: `submodules/TelegramCore/Sources/State/PendingMessageManager.swift` + +This task introduces the new `PendingMessageState` case and extends every switch that needs to know about it. No gate insertion happens yet — the new case is unreachable, so behavior is unchanged. + +- [ ] **Step 1: Add the case to `PendingMessageState`** + +Edit `private enum PendingMessageState` (line 28). After the `.waitingToBeSent` case, add: + +```swift + case waitingToBeSent(groupId: Int64?, content: PendingMessageUploadedContentAndReuploadInfo) + case waitingForSendGate(groupId: Int64?, content: PendingMessageUploadedContentAndReuploadInfo) +``` + +- [ ] **Step 2: Extend the `groupId` computed property** + +In the same `var groupId: Int64?` switch (line 37–54), add an arm right after the `.waitingToBeSent` arm: + +```swift + case let .waitingToBeSent(groupId, _): + return groupId + case let .waitingForSendGate(groupId, _): + return groupId +``` + +- [ ] **Step 3: Extend `dataForPendingMessageGroup` to accept parked members as ready** + +Find `private func dataForPendingMessageGroup(_ groupId: Int64)` (line 753). After the `.waitingToBeSent` arm, add a parallel arm: + +```swift + case let .waitingToBeSent(contextGroupId, content): + if contextGroupId == groupId { + result.append((context, id, content)) + } + case let .waitingForSendGate(contextGroupId, content): + if contextGroupId == groupId { + result.append((context, id, content)) + } +``` + +This lets a partially-parked album drain correctly once the gate opens. + +- [ ] **Step 4: Exclude parked state from `updatePendingMediaUploads`** + +Find `private func updatePendingMediaUploads()` (line 262). Inside its `switch context.state { ... }`, the existing arms cover `.waitingForUploadToStart` and `.uploading`. Parked state is post-upload and should NOT report progress. The switch already has a `default: break` — `.waitingForSendGate` falls through it automatically. **No edit required**, but verify by re-reading the function after the previous edits compile. + +- [ ] **Step 5: Verify the build** + +Run the build command. Expected: **success**. Swift will reject the build if any `switch context.state { ... }` becomes non-exhaustive — fix any such switches by adding a `case .waitingForSendGate: ...` arm matching the closest existing parallel state. Likely candidates: + +- `private enum PendingMessageState`'s `groupId` switch — handled in Step 2. +- `dataForPendingMessageGroup` switch — handled in Step 3. +- The forward branch in `beginSendingMessages` (line ~614, doesn't switch directly). +- Any other `switch .state` blocks (search with `grep -n "switch.*\.state" submodules/TelegramCore/Sources/State/PendingMessageManager.swift`). + +If the compiler reports a non-exhaustive switch elsewhere, add an arm that mirrors the `.waitingToBeSent` arm in that switch, or `case .waitingForSendGate: break` if the new state is irrelevant to that site. + +- [ ] **Step 6: Commit** + +```bash +git add submodules/TelegramCore/Sources/State/PendingMessageManager.swift +git commit -m "PendingMessageManager: add .waitingForSendGate state and update related switches" +``` + +--- + +## Task 4: Add manager-level subscription and parked-state fields + +**Files:** +- Modify: `submodules/TelegramCore/Sources/State/PendingMessageManager.swift` + +Adds the dictionary, the disposable, the subscription in `init`, the disposal in `deinit`, and a stub `handleLiveTypingDraftsUpdate` that only stores the set (drain is wired in Task 5). + +- [ ] **Step 1: Add stored fields** + +In `public final class PendingMessageManager` (line 205), after the existing `private var pendingMessageIds = Set()` (line 232), add: + +```swift + private var pendingMessageIds = Set() + private var liveTypingDraftKeys: Set = [] + private let allTypingDraftsDisposable = MetaDisposable() + private var forwardSendGateGroups: [PeerAndThreadId: [[(PendingMessageContext, Message, ForwardSourceInfoAttribute)]]] = [:] + private let beginSendingMessagesDisposables = DisposableSet() +``` + +(The first and last lines are already present; the three new lines insert between them.) + +- [ ] **Step 2: Subscribe in `init`** + +In `init(network:postbox:accountPeerId:auxiliaryMethods:stateManager:localInputActivityManager:messageMediaPreuploadManager:revalidationContext:)` (line 243), after the field assignments (line 252), add: + +```swift + self.revalidationContext = revalidationContext + + let queue = self.queue + self.allTypingDraftsDisposable.set( + (postbox.combinedView(keys: [.allTypingDrafts]) + |> deliverOn(queue)).start(next: { [weak self] view in + self?.handleLiveTypingDraftsUpdate(view) + }) + ) + } +``` + +- [ ] **Step 3: Dispose in `deinit`** + +In `deinit` (line 255–260), append: + +```swift + deinit { + self.beginSendingMessagesDisposables.dispose() + for (_, disposable) in self.newTopicDisposables { + disposable.dispose() + } + self.allTypingDraftsDisposable.dispose() + } +``` + +- [ ] **Step 4: Add the handler (no drain yet)** + +Add this new private method anywhere in the class (e.g. just before `private func updatePendingMediaUploads()` at line 262): + +```swift + private func handleLiveTypingDraftsUpdate(_ combined: CombinedView) { + assert(self.queue.isCurrent()) + + let new: Set + if let view = combined.views[.allTypingDrafts] as? AllTypingDraftsView { + new = view.keys + } else { + new = [] + } + self.liveTypingDraftKeys = new + } +``` + +This handler is functional — it tracks the live key set correctly. Task 5 augments it to also fire `drainSendGate` for keys that just cleared. + +- [ ] **Step 5: Verify the build** + +Run the build command. Expected: **success**. `CombinedView` is in scope because `Postbox` is already imported at the top of the file. `MetaDisposable` and `Queue` come from `SwiftSignalKit` (already imported). + +- [ ] **Step 6: Commit** + +```bash +git add submodules/TelegramCore/Sources/State/PendingMessageManager.swift +git commit -m "PendingMessageManager: subscribe to .allTypingDrafts and add parking-lot fields" +``` + +--- + +## Task 5: Add gate predicates, drain, and wire the single-message gate + +**Files:** +- Modify: `submodules/TelegramCore/Sources/State/PendingMessageManager.swift` + +Combined task: introduces the predicates, the `drainSendGate` body, augments `handleLiveTypingDraftsUpdate` to call drain, and wires the **first** insertion site (single-message). After this task, single non-grouped messages park correctly when the peer is live-typing and unpark when the draft clears. + +- [ ] **Step 1: Add `shouldGateSend` and `isSendGateOpen` private helpers** + +Anywhere in the `PendingMessageManager` class (group them next to `handleLiveTypingDraftsUpdate` from Task 4): + +```swift + private func shouldGateSend(messageId: MessageId, threadId: Int64?) -> Bool { + if messageId.namespace == Namespaces.Message.ScheduledCloud { + return false + } + if messageId.peerId.namespace == Namespaces.Peer.SecretChat { + return false + } + if messageId.peerId == self.accountPeerId { + return false + } + if threadId == Message.newTopicThreadId { + return false + } + return true + } + + private func isSendGateOpen(for key: PeerAndThreadId) -> Bool { + return !self.liveTypingDraftKeys.contains(key) + } +``` + +- [ ] **Step 2: Augment `handleLiveTypingDraftsUpdate` to call drain** + +Replace the body of `handleLiveTypingDraftsUpdate(_:)` (added in Task 4) with the cleared-key drain logic: + +```swift + private func handleLiveTypingDraftsUpdate(_ combined: CombinedView) { + assert(self.queue.isCurrent()) + + let new: Set + if let view = combined.views[.allTypingDrafts] as? AllTypingDraftsView { + new = view.keys + } else { + new = [] + } + let cleared = self.liveTypingDraftKeys.subtracting(new) + self.liveTypingDraftKeys = new + for key in cleared { + self.drainSendGate(key: key) + } + } +``` + +- [ ] **Step 3: Add the `drainSendGate` body** + +Add this new private method next to `handleLiveTypingDraftsUpdate`: + +```swift + private func drainSendGate(key: PeerAndThreadId) { + assert(self.queue.isCurrent()) + + // (1) Single-message drain: snapshot then commit in messageId.id order. + var singleDrains: [(context: PendingMessageContext, messageId: MessageId, content: PendingMessageUploadedContentAndReuploadInfo)] = [] + for (id, context) in self.messageContexts { + if id.peerId != key.peerId { + continue + } + if context.threadId != key.threadId { + continue + } + if case let .waitingForSendGate(groupId, content) = context.state, groupId == nil { + singleDrains.append((context, id, content)) + } + } + singleDrains.sort(by: { $0.messageId.id < $1.messageId.id }) + for entry in singleDrains { + self.commitSendingSingleMessage(messageContext: entry.context, messageId: entry.messageId, content: entry.content) + } + + // (2) Grouped-album drain: collect distinct groupIds whose members match the key, + // iterate ascending by min messageId.id, fire commitSendingMessageGroup. + var groupKeys: [(groupId: Int64, minMessageId: Int32)] = [] + var seenGroupIds = Set() + for (id, context) in self.messageContexts { + if id.peerId != key.peerId { + continue + } + if context.threadId != key.threadId { + continue + } + if case let .waitingForSendGate(groupId, _) = context.state, let groupId = groupId { + if !seenGroupIds.contains(groupId) { + seenGroupIds.insert(groupId) + groupKeys.append((groupId, id.id)) + } else { + if let index = groupKeys.firstIndex(where: { $0.groupId == groupId }), id.id < groupKeys[index].minMessageId { + groupKeys[index].minMessageId = id.id + } + } + } + } + groupKeys.sort(by: { $0.minMessageId < $1.minMessageId }) + for (groupId, _) in groupKeys { + if let data = self.dataForPendingMessageGroup(groupId) { + self.commitSendingMessageGroup(groupId: groupId, messages: data) + } + } + + // (3) Forward drain: pop parked groups for this key in FIFO order; fire each. + if let parkedGroups = self.forwardSendGateGroups.removeValue(forKey: key) { + for messages in parkedGroups { + for (context, _, _) in messages { + context.state = .sending(groupId: nil) + } + let sendMessage: Signal = self.sendGroupMessagesContent(network: self.network, postbox: self.postbox, stateManager: self.stateManager, accountPeerId: self.accountPeerId, group: messages.map { data in + let (_, message, forwardInfo) = data + return (message.id, PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil, cacheReferenceKey: nil)) + }) + |> map { _ -> PendingMessageResult in + return .progress(1.0) + } + messages[0].0.sendDisposable.set((sendMessage + |> deliverOn(self.queue)).start()) + } + } + + self.updateWaitingUploads(peerId: key.peerId) + self.updatePendingMediaUploads() + } +``` + +The forward dispatch block mirrors the existing forward-fire code at lines 719–731 of `PendingMessageManager.swift`. Keep them in sync if either changes. + +- [ ] **Step 4: Wire the single-message gate at `beginSendingMessage`** + +Replace `private func beginSendingMessage(messageContext:messageId:groupId:content:)` (line 738) with: + +```swift + private func beginSendingMessage(messageContext: PendingMessageContext, messageId: MessageId, groupId: Int64?, content: PendingMessageUploadedContentAndReuploadInfo) { + if let groupId = groupId { + messageContext.state = .waitingToBeSent(groupId: groupId, content: content) + } else { + let key = PeerAndThreadId(peerId: messageId.peerId, threadId: messageContext.threadId) + if self.shouldGateSend(messageId: messageId, threadId: messageContext.threadId) && !self.isSendGateOpen(for: key) { + messageContext.state = .waitingForSendGate(groupId: nil, content: content) + } else { + self.commitSendingSingleMessage(messageContext: messageContext, messageId: messageId, content: content) + } + } + self.updatePendingMediaUploads() + } +``` + +- [ ] **Step 5: Verify the build** + +Run the build command. Expected: **success**. + +- [ ] **Step 6: Commit** + +```bash +git add submodules/TelegramCore/Sources/State/PendingMessageManager.swift +git commit -m "PendingMessageManager: wire typing-draft gate at single-message send + drain" +``` + +--- + +## Task 6: Wire the album-send gate at `commitSendingMessageGroup` + +**Files:** +- Modify: `submodules/TelegramCore/Sources/State/PendingMessageManager.swift` + +- [ ] **Step 1: Replace `commitSendingMessageGroup`** + +Replace `private func commitSendingMessageGroup(groupId:messages:)` (line 794) with: + +```swift + private func commitSendingMessageGroup(groupId: Int64, messages: [(messageContext: PendingMessageContext, messageId: MessageId, content: PendingMessageUploadedContentAndReuploadInfo)]) { + let firstMessageId = messages[0].messageId + let firstThreadId = messages[0].messageContext.threadId + let key = PeerAndThreadId(peerId: firstMessageId.peerId, threadId: firstThreadId) + if self.shouldGateSend(messageId: firstMessageId, threadId: firstThreadId) && !self.isSendGateOpen(for: key) { + for entry in messages { + entry.messageContext.state = .waitingForSendGate(groupId: groupId, content: entry.content) + } + return + } + + for (context, _, _) in messages { + context.state = .sending(groupId: groupId) + } + let sendMessage: Signal = self.sendGroupMessagesContent(network: self.network, postbox: self.postbox, stateManager: self.stateManager, accountPeerId: self.accountPeerId, group: messages.map { ($0.1, $0.2) }) + |> map { next -> PendingMessageResult in + return .progress(1.0) + } + messages[0].0.sendDisposable.set((sendMessage + |> deliverOn(self.queue)).start()) + } +``` + +Album members share `(peerId, threadId)` by construction (the album fires once every member is post-upload in the same `groupId`). + +- [ ] **Step 2: Verify the build** + +Run the build command. Expected: **success**. + +- [ ] **Step 3: Commit** + +```bash +git add submodules/TelegramCore/Sources/State/PendingMessageManager.swift +git commit -m "PendingMessageManager: wire typing-draft gate at album send" +``` + +--- + +## Task 7: Wire the forward-send gate inside `beginSendingMessages` + +**Files:** +- Modify: `submodules/TelegramCore/Sources/State/PendingMessageManager.swift` + +- [ ] **Step 1: Replace the forward-dispatch loop** + +The existing forward-dispatch loop is at lines 714–733 inside `beginSendingMessages`. Replace exactly the body of `for messages in countedMessageGroups { ... }` with the gate-aware version: + +```swift + for messages in countedMessageGroups { + if messages.isEmpty { + continue + } + + let firstMessage = messages[0].1 + let key = PeerAndThreadId(peerId: firstMessage.id.peerId, threadId: firstMessage.threadId) + if strongSelf.shouldGateSend(messageId: firstMessage.id, threadId: firstMessage.threadId) && !strongSelf.isSendGateOpen(for: key) { + for (context, _, forwardInfo) in messages { + context.state = .waitingForSendGate(groupId: nil, content: PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil, cacheReferenceKey: nil)) + } + if strongSelf.forwardSendGateGroups[key] == nil { + strongSelf.forwardSendGateGroups[key] = [] + } + strongSelf.forwardSendGateGroups[key]!.append(messages) + continue + } + + for (context, _, _) in messages { + context.state = .sending(groupId: nil) + } + + let sendMessage: Signal = strongSelf.sendGroupMessagesContent(network: strongSelf.network, postbox: strongSelf.postbox, stateManager: strongSelf.stateManager, accountPeerId: strongSelf.accountPeerId, group: messages.map { data in + let (_, message, forwardInfo) = data + return (message.id, PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil, cacheReferenceKey: nil)) + }) + |> map { next -> PendingMessageResult in + return .progress(1.0) + } + messages[0].0.sendDisposable.set((sendMessage + |> deliverOn(strongSelf.queue)).start()) + } +``` + +The non-gated branch is the existing code, copied verbatim. The gated branch parks every context in `.waitingForSendGate` and appends the whole tuple-array onto `forwardSendGateGroups[key]`. The drain in `Task 5` reads from this dict. + +- [ ] **Step 2: Verify the build** + +Run the build command. Expected: **success**. + +- [ ] **Step 3: Commit** + +```bash +git add submodules/TelegramCore/Sources/State/PendingMessageManager.swift +git commit -m "PendingMessageManager: wire typing-draft gate at forward send" +``` + +--- + +## Task 8: Clean up parked forwards in `updatePendingMessageIds` removal loop + +**Files:** +- Modify: `submodules/TelegramCore/Sources/State/PendingMessageManager.swift` + +When a message is removed from `pendingMessageIds` (e.g. user discarded it, or it was force-deleted), the existing loop disposes context-level state. Parked single/album state lives on the `PendingMessageContext` and gets reset when `state = .none`. Parked forwards live in `forwardSendGateGroups`, which the existing loop does not touch — patch that here. + +- [ ] **Step 1: Add cleanup for `forwardSendGateGroups` in `updatePendingMessageIds`** + +In `updatePendingMessageIds(_:)` (line 284), after the `for id in removedMessageIds { ... }` loop (closes around line 323), add a forward-cleanup pass before `if !addedMessageIds.isEmpty { ... }`: + +```swift + if !removedMessageIds.isEmpty && !self.forwardSendGateGroups.isEmpty { + for (key, parkedGroups) in self.forwardSendGateGroups { + var rebuilt: [[(PendingMessageContext, Message, ForwardSourceInfoAttribute)]] = [] + for group in parkedGroups { + let filtered = group.filter { entry in + return !removedMessageIds.contains(entry.1.id) + } + if !filtered.isEmpty { + rebuilt.append(filtered) + } + } + if rebuilt.isEmpty { + self.forwardSendGateGroups.removeValue(forKey: key) + } else { + self.forwardSendGateGroups[key] = rebuilt + } + } + } +``` + +- [ ] **Step 2: Verify the build** + +Run the build command. Expected: **success**. + +- [ ] **Step 3: Commit** + +```bash +git add submodules/TelegramCore/Sources/State/PendingMessageManager.swift +git commit -m "PendingMessageManager: cleanup parked forwards on pending-message removal" +``` + +--- + +## Task 9: Manual verification + +**Files:** None (verification-only). + +This codebase has no unit tests, so the final acceptance gate is a manual exercise on a debug simulator build. Skip if you don't have a working two-device test setup; in that case, re-confirm only the build is green. + +- [ ] **Step 1: Confirm full build is still green** + +Run the build command from the header. Expected: **success**. + +- [ ] **Step 2: 1:1 chat, text-send delay** + +Setup: log in to two real Telegram accounts on two devices/simulators (account A and account B). Open the A↔B 1:1 chat on both. + +- On B, begin live-typing a draft (Telegram clients with the live-typing-drafts feature emit the typing-draft updates; if B doesn't expose live drafts, simulate by triggering whatever flow populates `transaction.combineTypingDrafts(...)` in your test environment). +- On A, immediately type and send a text message. Confirm: the message appears with a "sending" indicator and does **not** complete until B's draft clears or commits. Once B's draft is cleared, A's message sends within ~1 second. + +- [ ] **Step 3: Album / grouped-media delay** + +Repeat Step 2 with an album of two photos sent from A while B is live-typing. Expected: all album members upload (you can see progress finish), then all sit parked at "sending" until the gate opens. + +- [ ] **Step 4: Forward delay** + +Repeat with a forwarded message (forward a message from a third chat into A↔B while B is live-typing). Expected: forward parks until the gate opens. + +- [ ] **Step 5: Negative — Saved Messages skip** + +In Saved Messages (chat with self), send any message. Confirm: never delays, regardless of typing-draft state. There should be no typing draft for the self peer in the first place — this is just an existence check that the skip-rule does its job. + +- [ ] **Step 6: Negative — secret chat skip** + +In a secret chat, send a message. Confirm: never delays. Server-side, secret chats don't emit typing-draft updates — this verifies the explicit skip-check. + +- [ ] **Step 7: Negative — scheduled message skip (defensive)** + +The `Namespaces.Message.ScheduledCloud` skip in `shouldGateSend` is defensive — in practice, scheduled messages are stored in the scheduled queue and only enter `PendingMessageManager` at delivery time, by which point they've been re-created with cloud namespace. Verifying the defensive branch directly is awkward and not strictly required. If you have an instrumentation path that forces a scheduled-namespace message through `beginSendingMessages`, confirm it doesn't park. + +- [ ] **Step 8: Multi-thread — gate is per-thread** + +In a forum/topic group, on B begin live-typing in topic 1 only. On A, send a message to topic 2 of the same group. Expected: A's message to topic 2 is **not** delayed. + +--- + +## Self-review notes (already applied inline) + +- **Spec coverage:** every section of `2026-04-30-typing-draft-send-delay-design.md` maps to a task — Postbox view (Tasks 1–2), state machine extension (Task 3), subscription (Task 4), predicates + drain + single-send (Task 5), album (Task 6), forwards (Task 7), removal cleanup (Task 8), manual verification (Task 9). +- **Type names verified against actual code:** `PendingMessageState`, `PendingMessageContext`, `PendingMessageUploadedContentAndReuploadInfo`, `ForwardSourceInfoAttribute`, `PendingMessageResult`, `Signal`, `MetaDisposable`, `Queue`, `CombinedView`, `Namespaces.Message.ScheduledCloud`, `Namespaces.Peer.SecretChat`, `Message.newTopicThreadId`, `PeerAndThreadId`, `combineTypingDrafts`, `currentTypingDrafts`, `transaction.updatedTypingDrafts`, `update.value`. All names match the source. +- **`postponeSending` (paid-message) interaction:** untouched. Composes via state-machine ordering (paid postpone gates upload-start; this gate sits post-upload). +- **No unused-private-function warnings:** every helper is introduced together with its first caller (predicates + drain + first call site combined in Task 5). diff --git a/docs/superpowers/postbox-refactor-log.md b/docs/superpowers/postbox-refactor-log.md new file mode 100644 index 0000000000..6f2a69fe99 --- /dev/null +++ b/docs/superpowers/postbox-refactor-log.md @@ -0,0 +1,1971 @@ +# Postbox → TelegramEngine refactor — log + +This file is the historical record of the Postbox → TelegramEngine refactor. It is **not loaded by default** into AI sessions (only `CLAUDE.md` is). Read this file when you need wave-specific context, a full worked example of a pattern, or the running tally of module Postbox-freeness. + +The short, actively-maintained rules and references live in `CLAUDE.md` under the "Postbox → TelegramEngine refactor" section. This file holds the narrative backstory, verbose example scripts, and per-wave outcomes that would otherwise bloat every AI session's context. + +--- + +## Wave-selection guidance — full versions + +The following subsections are trimmed to terse bullets in `CLAUDE.md`. Full versions (rule + backstory + scripts + per-wave examples) live here. + +### Shape-selection backstory + +The "leaf module, drop Postbox in isolation" approach only works for modules whose **public API doesn't leak Postbox domain types**. Most candidate leaf modules DO leak such types (`postbox: Postbox` / `account: Account` in public inits, `Media`/`Message` in public function parameters). Those modules need paired caller-migration waves, not isolated refactors. + +Before selecting a wave's module list, grep each candidate for: +- `:\s*Postbox\b`, `:\s*Account\b`, `:\s*MediaBox\b` in public signatures → abandon candidate +- `Media`/`Message` as public parameter types → likely needs paired wave with callers + +### Inventory at execution time, not just planning time + +**Inventory at execution time, not just planning time.** Wave 2's `SaveToCameraRoll` task was planned from a narrow grep that only matched `MediaResource`/`TelegramMediaResource` and missed three `postbox: Postbox` public-function leaks plus multiple `postbox.mediaBox.*` bodies. Planning-time inventory should grep the full set `\b(postbox|mediaBox|transaction|PostboxView|combinedView|MediaResource|PostboxDecoder|PostboxEncoder|MemoryBuffer)\b|^import Postbox` over the module's Sources, not just the tokens specific to that wave's goal. If the planning inventory under-counts, the executor should re-inventory at Task-1 time and abandon early before editing code. + +### Two feasible wave shapes + +**Two feasible wave shapes.** Wave 1 tried "per-module Postbox drop". Wave 2 tried "per-engine-facade-API migrate MediaResource to EngineMediaResource (modify in place, update all call sites in one commit)". The second shape worked well: narrow, clean commits, no abandonment cascade. Prefer it when the refactor target is an API surface that multiple consumer modules depend on. + +### Enum-payload migrations need a full case-site grep + +**Enum-payload migrations need a full case-site grep, not just a facade call-site grep.** If a wave changes the payload type of a public enum (wave 4 changed `UploadStickerStatus.complete`'s payload from `CloudDocumentMediaResource` to `EngineMediaResource`), inventory ALL construction and destructure sites of the enum across TelegramCore, not just call sites of the facade that returns it. Wave 4's plan undercounted by 6 consumer sites inside `ImportStickers.swift` itself (3 shortcut `.complete(...)` constructions in guard branches, 3 destructure+field-access sites using `CloudDocumentMediaResource`-specific members). For enum-payload waves, grep `case \.|let \.|\.\(` over the enum's defining module before execution and add those sites to the plan. + +### Unused-import sweeps are a valid wave shape + +**Unused-import sweeps are a valid wave shape.** After a round of facade migrations, consumer files accumulate `import Postbox` lines whose last semantic use was removed. Periodically sweep these: + +1. `grep -rl "^import Postbox$" submodules --include="*.swift" | grep -vE "/(TelegramCore|Postbox|TelegramApi)/"` generates the candidate list. +2. `sed -i '' '/^import Postbox$/d' ` (BSD `sed`) speculatively drops the import from every candidate. +3. Run the full build **with `--continueOnError`** — without `--keep_going`, bazel stops at the first failing target and surfaces only a few errors per iteration. `Make.py` forwards `--continueOnError` to `--keep_going`; always use it. +4. Each iteration: extract failing files via `grep -E "^submodules/.*\.swift:[0-9]+:[0-9]+: error:" | awk -F: '{print $1}' | sort -u`, restore via `git checkout -- `, rebuild. +5. The dependency graph has many layers (wave 6 needed ~18 rebuilds to reach a clean build). Per-iteration failures shrink roughly: 18 → 4 → 5 → 3 → 12 → 4 → 13 → 9 → 11 → ... Accelerate by doing **pattern-based preemptive restores** after the first few iterations: scan still-dropped files for tokens that are definitively Postbox-only (`MediaBox`, `PostboxCoding`, `PostboxDecoder`, `PostboxEncoder`, `TempBoxFile`, `ValueBoxKey`, `Postbox\b`, `PeerId`, `MessageId`, `MediaId`, `MessageIndex`, `MessageAndThreadId`, `PeerNameIndex`, etc. — note that CLAUDE.md's "engine typealias cheat sheet" arrows are migration targets, **not** typealiases in TelegramCore — `PeerId` etc. are still raw Postbox types and files using them need `import Postbox`) and restore those files in bulk. +6. Only restore files from the candidate set. If errors surface in `TelegramCore`, `Postbox`, or `TelegramApi`, halt — the sweep has cascaded beyond its scope. +7. Commit the surviving drops as one atomic commit. + +Tally impact from a sweep: dozens of consumer modules can become Postbox-free in a single commit. First run (wave 6): 782 candidates, 18 iterations, 183 survivors, **189 modules** newly Postbox-free. Re-run after every 2-3 facade-migration waves. + +### Public-Postbox-type inventory (wave-11-pattern planning) + +**Public-Postbox-type inventory (wave-11-pattern planning).** For wave-11-shape candidates (modules whose public init takes `postbox: Postbox, network: Network` purely for avatar/setPeer forwarding), grepping only for `Postbox`/`Network` tokens **undercounts** — public-surface types defined in Postbox can leak without ever naming "Postbox" literally. Wave 16 hit this: the plan missed `EngineMessageHistoryThread.Info?` and `PeerStoryStats?`, both Postbox-defined types whose names don't include "Postbox". Mitigation: build a Postbox-defined-public-types allowlist once, then grep the candidate module against it. + +```bash +# Build allowlist once (or re-run if Postbox sources change): +grep -rhE "^public\s+(class|struct|enum|protocol|typealias)\s+\w+" submodules/Postbox/Sources/ \ + | awk '{print $3}' | sed 's/[(:<].*//' | sort -u > /tmp/postbox-public-types.txt + +# Then, for each candidate module, grep its sources for any of those names: +grep -rhoE "\b($(cat /tmp/postbox-public-types.txt | tr '\n' '|' | sed 's/|$//'))\b" \ + submodules//Sources/ | sort -u +``` + +Any hit in a public-surface position (field type, init param type, enum payload type, generic arg) that isn't already a documented typealias is a blocker. "Engine"-prefixed types can still be Postbox-defined — don't trust naming conventions, grep for the defining module. If the module hits only `Postbox` itself (i.e., literal `Postbox`/`Network` pair), it's a clean wave-11 candidate. Otherwise, decide per leak: (a) move the type to TelegramCore if it's a namespace-only class (wave 16a pattern — prototype: `EngineMessageHistoryThread`), (b) accept that the module can't become Postbox-free and ship a partial `engine:`/`stateManager:` collapse that keeps `import Postbox` (wave 16b pattern — `PeerStoryStats` is too baked into Postbox views to move cleanly), (c) abandon the candidate. + +### Wave-shape G: facade addition + consumer sweep in one commit + +**Wave-shape G: facade addition + consumer sweep in one commit.** Validated at scale across waves 19-26. Six consecutive sessions migrated ~95 consumer sites and added ~15 mediaBox facades, all with clean first-pass builds (exception: wave 26 needed a second pass to add `import RangeSet`). Shape recipe: + +1. **Target:** a `MediaBox` (or similar Postbox type) method where Postbox's signature uses clean leaf types (`MediaResourceId`, `Data`, `String`, `Bool`) and the return type is either non-Postbox or has an existing `Engine*` wrapper. +2. **Pre-flight inventory:** grep `context\.account\.postbox\.mediaBox\.` over `submodules/` (excluding TelegramCore/Postbox/TelegramApi). Classify each hit: + - **Shape A**: `context.account.postbox.mediaBox.X(...)` → migratable. + - **Shape B**: `context.account.postbox.mediaBox.X(id: ...)` (different overload) → migratable with identical pattern. + - **Shape C**: `account.postbox.mediaBox.X(...)` where `account: Account` is a local (not `AccountContext`) → skip this wave (needs per-module rework). + - **Shape D**: `self.postbox.mediaBox.X(...)` where `postbox: Postbox` is a stored field → skip this wave. + - Plus: check for `accountManager.mediaBox.X(...)` which is Account-manager-scoped, a different migration path entirely. Never migrate via `TelegramEngine.Resources.*`. +3. **Facade design rules:** + - Signatures take `EngineMediaResource.Id` (`MediaResourceId` aliased at call site via `EngineMediaResource.Id(x.id)`) or `EngineMediaResource` (wraps `resource` when the Postbox overload takes a resource with members accessed via `.id`). + - Parameters with `Bool` defaults (`synchronous: Bool = false`) preserve defaults on the facade. + - Return types: prefer `Void`, `String`, `String?`, `Signal` where `T` is a non-Postbox type or an `Engine*` wrapper. Where Postbox return types are wrapped (e.g., `Signal` → `Signal`), confirm the `Engine*` wrapper exists and decide whether consumer-side field-access rewrites are acceptable for the wave. +4. **WIP interference check:** before starting, `git status --short | grep -v "^??"` to list modified files. If any Shape-A site is in a WIP file, either skip those sites (document the skip in the outcome) or wait for the WIP to commit. Wave 23 hit this in `ChatMessageInteractiveMediaNode.swift`. +5. **Name collision check:** if a facade return type names a Swift stdlib type that has availability restrictions (e.g., `RangeSet` — iOS 18+), verify the third-party module import is present in `TelegramEngineResources.swift`. Wave 26 needed `import RangeSet`. +6. **Replace_all usage:** for files with duplicate identical call text, `replace_all=true` on the exact call expression (without leading whitespace) batches the migration. When leading whitespace varies across identical-call sites within a file, the tool still matches if the unchanged prefix (`context.account.postbox.mediaBox.X(...)`) is unique enough — but verify via post-edit grep. +7. **Cheapness:** ~5-50 sites per wave, single atomic commit, expected first-pass-clean build. If post-migration grep for `context\.account\.postbox\.mediaBox\.` returns empty (exclude Shape-C/D) and build is green, commit. + +--- + +## Wave 1 outcome (2026-04-16) + +4 modules done: `ChatInterfaceState`, `ChatSendMessageActionUI`, `ContactListUI`, `DrawingUI`. +6 modules abandoned with recorded reasons in the wave-1 plan: `ActionSheetPeerItem`, `ChatListSearchRecentPeersNode`, `DirectMediaImageCache`, `FetchManagerImpl`, `GalleryData`, `ICloudResources`. + +## Wave 2 outcome (2026-04-17) + +5 `TelegramEngine` facades migrated to `EngineMediaResource` (signatures changed in place; `_internal_*` Postbox layer unchanged): +- `TelegramEngine.Peers.uploadedPeerPhoto`, `uploadedPeerVideo`, `updatePeerPhoto` +- `TelegramEngine.AccountData.updateAccountPhoto`, `updateFallbackPhoto` +- `TelegramEngine.Contacts.updateContactPhoto` +- `TelegramEngine.Auth.uploadedPeerVideo` + +1 consumer submodule fully de-Postboxed: `MapResourceToAvatarSizes` (signature changed from `(postbox: Postbox, resource: MediaResource, …)` to `(engine: TelegramEngine, resource: EngineMediaResource, …)`; 27 call sites migrated). + +1 consumer signal type swapped: `AuthorizationUI/AuthorizationSequenceController.swift` (`Signal` → `Signal`). + +1 task abandoned with recorded reason in the wave-2 plan: `SaveToCameraRoll` (full-module Postbox coupling, needs its own wave). + +## Wave 3 outcome (2026-04-18) + +3 thin forwarders added on `TelegramEngine.Resources` over `MediaBox`: +- `fetch(reference:userLocation:userContentType:)` → `Signal` (Postbox return types remain a documented accepted leak) +- `status(resource: EngineMediaResource)` → `Signal` +- `data(resource: EngineMediaResource, pathExtension:, waitUntilFetchStatus:)` → `Signal` (takes a `Bool` rather than exposing `ResourceDataRequestOption`, per YAGNI) + +1 consumer submodule fully de-Postboxed: `SaveToCameraRoll`. Public signatures changed from `(context:, postbox: Postbox, userLocation:, …)` to `(context:, userLocation:, …)`; `FetchMediaDataState.data` payload changed from `MediaResourceData` to `EngineMediaResource.ResourceData`; internals rewired through `context.engine.resources.*`. 23 call sites across 14 files migrated atomically with the module. + +Pre-flight verified that `ShareController.swift:2406`'s `self.currentContext.stateManager.postbox` is equivalent to `context.account.postbox` in the `ShareControllerAppAccountContext` path (because `AccountStateManager` is constructed with the account's own `postbox`), so the `postbox:` argument could be dropped without behavior change. + +No tasks abandoned. Shape validated: "per-engine-facade-API migration + full consumer module rewrite" (the wave-2 shape, scaled up to a full module drop). + +Plan: `docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-3.md` + +## Wave 4 outcome (2026-04-18) + +1 `TelegramEngine` facade migrated in place to `EnginePeer` + `EngineMediaResource` (signature changed; `_internal_uploadSticker` keeps its raw `Peer`/`MediaResource` parameter list): + +- `TelegramEngine.Stickers.uploadSticker(peer: Peer → EnginePeer, resource: MediaResource → EngineMediaResource, thumbnail: MediaResource? → EngineMediaResource?, …)` + +1 public enum payload migrated: `UploadStickerStatus.complete(CloudDocumentMediaResource, String)` → `.complete(EngineMediaResource, String)`. `_internal_uploadSticker` wraps `EngineMediaResource(uploadedResource)` at its one `.complete(...)` result-construction site — a narrow, spec-allowed one-line deviation from "internal Postbox-facing stays raw", taken to keep `UploadStickerStatus` as a single public enum. + +**Plan-time inventory undercount** — worth recording as a lesson. The spec and plan enumerated 2 external call sites and 1 internal construction site. Execution uncovered 6 additional consumer sites inside `ImportStickers.swift` itself that also needed adapting: 3 shortcut `.complete(...)` construction sites (lines 204, 371, 492, each emitting `.complete(CloudDocumentMediaResource, String)` directly from `as? CloudDocumentMediaResource` guards) and 3 destructure sites (lines 216, 384, 505) that accessed `CloudDocumentMediaResource`-specific fields. Each construction site now wraps via `EngineMediaResource(resource)`; each destructure site unwraps with `let rawResource = resource._asResource() as? CloudDocumentMediaResource`. MediaEditorScreen's two `stickerFile(resource:)` calls also needed `as! TelegramMediaResource` casts because `_asResource()` returns the Postbox `MediaResource` protocol while `stickerFile` takes the TelegramCore `TelegramMediaResource` sub-protocol. **Future planning-time inventory for enum-payload migrations should grep not only call-sites of the facade but every `case .complete` / `case let .complete` of the migrated enum across the whole TelegramCore source tree.** + +2 external call sites migrated atomically with the facade: +- `submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift:91` (plus a `peer: Peer → EnginePeer(peer)` wrap, since the local `peer` comes from `postbox.loadedPeerWithId(...)` which returns raw `Peer`) +- `submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift:8099` (plus 6 cascading sites inside the enclosing block for the new `UploadStickerStatus.complete` payload) + +No module becomes Postbox-free in this wave (both caller files import Postbox for unrelated reasons). + +Plan: `docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-4.md` + +## Wave 5 outcome (2026-04-18) + +Completes the last explicitly-named future-wave candidate from the wave-2 final review. + +`uploadSecureIdFile(context: SecureIdAccessContext, postbox: Postbox, network: Network, resource: MediaResource)` migrated in place to `(context:, engine: TelegramEngine, resource: EngineMediaResource)`. Function body accesses raw Postbox types via `engine.account.postbox` / `engine.account.network` (internal Postbox-facing layer stays raw per the standing rule). + +1 consumer submodule fully de-Postboxed: `SecureIdVerificationDocumentsContext` (PassportUI/Sources). Signature changed from `(postbox: Postbox, network: Network, context: SecureIdAccessContext, update: ...)` to `(engine: TelegramEngine, context: SecureIdAccessContext, update: ...)`; stored props collapsed into a single `engine: TelegramEngine` field. One instantiation site updated in the same commit. + +After this wave, the "Known future-wave candidates" list contains only the 4 permanently-blocked classes conforming to `TelegramMediaResource`. + +Plan: `docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-5.md` + +## Wave 6 outcome (2026-04-19) + +First build-verified unused-import sweep. Ran the speculative-drop + build-verify methodology (see "Unused-import sweeps" under Wave-selection guidance above): dropped `import Postbox` from all 782 consumer files where a plain `^import Postbox$` line appeared, iterated 18 full builds with `--continueOnError`, restoring imports on files that failed to compile. + +**183 drops survived** (single atomic commit `7b2b74e79b`, 0 insertions / 183 deletions). **189 modules** transitioned to Postbox-free status — full list is inferable by running the methodology's module-scan against HEAD. Representative additions spanning alphabetically: `AccountUtils`, `ActivityIndicator`, `AdUI`, `AlertUI`, `AnimatedStickerNode`, `AppLock`, `AttachmentTextInputPanelNode`, `BotPaymentsUI`, `CalendarMessageScreen`, `CallListUI`, `Camera`, `ChatImportUI`, etc. The running tally below preserves the per-module enumeration only for the ~10 individually-documented waves 1–5 modules. Wave 6's 189 additions are not re-enumerated here because the size would overwhelm the doc; see `git show 7b2b74e79b --stat` for the per-file breakdown and `grep -rL "^(@_exported )?import Postbox" submodules/*/Sources --include="*.swift"` for the current per-module status. + +Deviation from plan: the plan capped at 3 iterations; execution needed 18 because the dependency graph is deep and each bazel build surfaces only the currently-compilable layer. Pattern-based preemptive restores (using the symbol list in the "Unused-import sweeps" guidance) were used from iteration 9 onward to accelerate convergence from iteration-by-iteration single-file restores to bulk restores. No unexpected path cascades; no abandoned state. + +Plan: `docs/superpowers/plans/2026-04-19-postbox-to-telegramengine-wave-6.md` + +## Wave 7 outcome (2026-04-20) + +Closed out the seven remaining raw-Postbox leaks in `TelegramEngine.*` public facades surfaced by a post-wave-6 scouting pass. Single atomic commit, one full build, zero abandonment. + +Seven `TelegramEngine` facades migrated in place (all `_internal_*` implementations kept raw per the standing rule): + +**Messages (3):** +- `downloadMessage(messageId:)` — return `Signal` → `Signal`. Return-side wrap via `|> map { $0.flatMap(EngineMessage.init) }`. +- `topPeerActiveLiveLocationMessages(peerId:)` — return `Signal<(Peer?, [Message]), NoError>` → `Signal<(EnginePeer?, [EngineMessage]), NoError>`. Return-side tuple wrap. +- `getSynchronizeAutosaveItemOperations()` — **deleted**. Dead facade: sole caller (`StoreDownloadedMedia.swift`) already bypassed it by calling `_internal_getSynchronizeAutosaveItemOperations` directly inside its own transaction block. + +**Peers (1):** +- `updatedRemotePeer(peer:)` — return `Signal` → `Signal`. `PeerReference` param kept as-is (no `EnginePeer.Reference` alias today). The sole call site in `ChannelAdminsController.swift` uses `ignoreValues`, so no caller change was needed. + +**Resources (4):** +- `renderStorageUsageStatsMessages(…existingMessages:)` — `[EngineMessage.Id: Message]` → `[EngineMessage.Id: EngineMessage]` on both sides. Facade unwraps input via `.mapValues { $0._asMessage() }`, wraps output via `.mapValues(EngineMessage.init)`. +- `clearStorage(peerId:categories:includeMessages:excludeMessages:)` — `[Message]` → `[EngineMessage]`. Facade unwraps via `.map { $0._asMessage() }`. +- `clearStorage(peerIds:includeMessages:excludeMessages:)` — same shape. +- `clearStorage(messages:)` — same shape. No external callers; migrated for overload-set consistency. + +**Consumer call-site updates** (5 files): +- `ChatListUI/Sources/ChatListSearchListPaneNode.swift`: dropped now-redundant `.flatMap(EngineMessage.init)` wrap at the `downloadMessage` call site. +- `LocationUI/Sources/LocationViewControllerNode.swift`: dropped now-redundant `.map(EngineMessage.init)` at the `topPeerActiveLiveLocationMessages` call site. +- `LiveLocationManager/Sources/LiveLocationSummaryManager.swift`: dropped redundant `EnginePeer(author)` / `EngineMessage(message)` construction (`author`, `message` are now already `EnginePeer` / `EngineMessage`). +- `TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift`: bridged at 4 facade-call points (1 `renderStorageUsageStatsMessages`, 2 `clearStorage` overloads with message arrays; the `includeMessages: [], excludeMessages: []` site at line 3038 needed no change as empty arrays infer to `[EngineMessage]` just as well). + +**Minimal-scope bridging.** `StorageUsageScreen.swift` still has 43 raw `Message`/`MessageId` references inside its `AggregatedData` helper class and surrounding logic — not touched in this wave. A future "StorageUsageScreen full de-Postbox" wave would drop those (migrate `AggregatedData.messages: [MessageId: Message]` → `[EngineMessage.Id: EngineMessage]`, `clearIncludeMessages: [Message]` → `[EngineMessage]`, etc.) and potentially drop `import Postbox`. Out of scope here. + +**No modules became Postbox-free in this wave** — all five touched consumer files still import Postbox for reasons unrelated to the migrated facades. + +Plan / record: `docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-7.md`. + +After this wave, the "Known future-wave candidates" list contains only the 4 permanently-blocked classes conforming to `TelegramMediaResource`. The full public `TelegramEngine.*` facade surface is now engine-typed (modulo those four types). + +## Wave 8 outcome (2026-04-20) + +`StorageUsageScreen` consumer-module migration of raw `Message` domain types to `EngineMessage`. Scope explicitly narrower than a full de-Postbox: two files touched, module remains `import Postbox` due to two out-of-scope site clusters. + +**Types migrated:** +- `StorageFileListPanelComponent.Item.message: Message` → `EngineMessage` (item type co-located with the panel component). +- `StorageUsageScreen.Component.AggregatedData.messages: [MessageId: Message]` → `[EngineMessage.Id: EngineMessage]`; `.clearIncludeMessages` / `.clearExcludeMessages: [Message]` → `[EngineMessage]`. Init param updated to match. +- `StorageUsageScreen.Component.SelectionState.togglePeer(availableMessages:)` param: `[EngineMessage.Id: Message]` → `[EngineMessage.Id: EngineMessage]`. +- `StorageUsageScreen.Component.RenderResult.messages: [MessageId: Message]` → `[EngineMessage.Id: EngineMessage]`. +- `openMessage(message: Message)` → `openMessage(message: EngineMessage)` (external `OpenChatMessageParams.message` / `chatMediaListPreviewControllerData(message:)` calls unwrap via `message._asMessage()` at the two call sites — those APIs still take raw `Message`). + +**Wave-7 facade-boundary bridging dropped:** the `renderStorageUsageStatsMessages` call-site's `(…).mapValues(EngineMessage.init)` / `.mapValues { $0._asMessage() }` bridges and the two `clearStorage` call sites' `.map(EngineMessage.init)` wraps all vanish — `AggregatedData.messages` / `.clearIncludeMessages` / `.clearExcludeMessages` are now engine-typed and pass through the facade unchanged. Inside the `AggregatedData.updateSelected...` selected-messages accumulation loop, four `item.message._asMessage()` calls (for imageItems, which hold EngineMessage) drop back to plain `item.message` since the target array is now `[EngineMessage]`. And `StorageMediaGridPanelComponent.Item(message: EngineMessage(message), …)` drops the `EngineMessage(…)` wrap since `message` is already `EngineMessage`. + +**Out of scope — future-wave candidates (module still imports Postbox):** +- `StorageUsageScreen.swift:1047-1062` and `3131-3185`: preferences-view observation of `AccountSpecificCacheStorageSettings` via `postbox.combinedView` + `PreferencesView`, and a `postbox.transaction { transaction in transaction.getPeer / transaction.getPeerCachedData as? CachedGroupData / CachedChannelData }` block classifying peer-storage-timeout exceptions. Substantial: requires `EngineData`-subscription rewrite for the preferences observation, plus engine-API equivalents for peer-category classification + cached-data subscriber counts. +- `StorageFileListPanelComponent.swift:105`: `Icon.media(Media, TelegramMediaImageRepresentation)` enum case, constructed only as `.media(TelegramMediaFile, …)` or `.media(TelegramMediaImage, …)` (both TelegramCore types). Trivial future wave: split into `.mediaFile(TelegramMediaFile, …)` / `.mediaImage(TelegramMediaImage, …)`, drop `import Postbox`. + +Single atomic commit. Build verified green (59s incremental build, 27 actions). Net −11 lines in `StorageUsageScreen.swift` (simplification). + +Plan / record: `docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-8.md`. + +## Wave 9 outcome (2026-04-20) + +Closes the first of the two "future-wave candidates" left open by wave 8: rewrites both `AccountSpecificCacheStorageSettings` preferences-view observation sites in `StorageUsageScreen.swift` using engine APIs, and drops `import Postbox` from that file. + +**Site 1 — `cacheSettingsExceptionCount` signal** (former lines 1047–1087): +- `postbox.combinedView(keys: [.preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings]))])` + `PreferencesView` extraction → + `context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.accountSpecificCacheStorageSettings))` + `preferencesEntry?.get(AccountSpecificCacheStorageSettings.self) ?? .defaultSettings`. +- Downstream `EngineDataMap` + `EnginePeer` per-category counting logic unchanged (already engine-only). + +**Site 2 — `peerExceptions` signal in `openKeepMediaCategory`** (former lines 3131–3196): +- Same preferences observation replacement as Site 1. +- `postbox.transaction { transaction.getPeer / transaction.getPeerCachedData as? CachedGroupData / CachedChannelData; FoundPeer(peer:subscribers:) }` → `context.engine.data.get(EngineDataMap(...TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))` + pattern match on `EnginePeer.user / .secretChat / .legacyGroup / .channel`. +- Signal element type `[(peer: FoundPeer, value: Int32)]` → `[(peer: EnginePeer, value: Int32)]`. `FoundPeer` wrapper and its `subscribers` field dropped entirely — they were computed by the transaction block but never read by downstream consumers (the only consumer sites read `.isEmpty`, `.count`, and `.prefix(3).map { EnginePeer($0.peer.peer) }`). +- One downstream consumer updated: `peerExceptions.prefix(3).map { EnginePeer($0.peer.peer) }` → `.prefix(3).map { $0.peer }` at the `MultiplePeerAvatarsContextItem` construction (redundant wrap removed since `$0.peer` is already `EnginePeer`). + +**Typealias fixup.** With `import Postbox` removed, `var mergedMedia: [MessageId: Int64]` at former line 2397 needed renaming to `[EngineMessage.Id: Int64]`. `MessageId` is the raw Postbox type name — CLAUDE.md's engine-typealias cheat sheet lists these as migration targets, not pre-existing aliases in TelegramCore. Caught by first-pass build failure (`cannot find type 'MessageId' in scope`). + +**Reusable pattern.** `TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: ValueBoxKey)` (at `TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift:356`) is the general-purpose engine replacement for any `postbox.combinedView(keys: [.preferences(keys: Set([key]))]) + PreferencesView` idiom — takes any `ValueBoxKey`, returns `PreferencesEntry?`, decodes via `.get(T.self)`. Crucially, callers need not `import Postbox` even though `ValueBoxKey` is a Postbox type: passing `PreferencesKeys.` through makes `ValueBoxKey` an inferred-only type that never gets named in the consumer module. Use this pattern when de-Postboxing any future module that observes preferences. + +`StorageUsageScreen.swift` is now Postbox-free. The wave 8 outcome's other candidate (`StorageFileListPanelComponent.swift`'s `Icon.media(Media, ...)` enum case) remains — trivial future wave to split into `.mediaFile(TelegramMediaFile, ...)` / `.mediaImage(TelegramMediaImage, ...)` cases, at which point the `StorageUsageScreen` consumer module as a whole becomes Postbox-free. + +Net: 1 file changed, +30 / -54. Build verified green (27 actions, cached). + +Plan / record: `docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-9.md`. + +## Wave 10 outcome (2026-04-20) + +Closes the second (and last) future-wave candidate from wave 8: eliminates `StorageFileListPanelComponent.swift`'s `Icon.media(Media, TelegramMediaImageRepresentation)` enum case. **`StorageUsageScreen` (the module as a whole) is now fully Postbox-free** — the other in-module file (`StorageUsageScreen.swift`) landed in wave 9. + +**Split the enum case.** `Icon.media(Media, TelegramMediaImageRepresentation)` → two concrete cases `case mediaFile(TelegramMediaFile, TelegramMediaImageRepresentation)` + `case mediaImage(TelegramMediaImage, TelegramMediaImageRepresentation)`. Lossless split: the two construction sites already knew the concrete subtype (`imageIconValue = .media(file, representation)` from a `as? TelegramMediaFile` branch, `.media(image, representation)` from a `as? TelegramMediaImage` branch), and the consumer binding site immediately downcast via `as?` to pick which `setSignal(...)` flavor to call. New split removes the downcast; exhaustiveness-checked switch is both safer and terser. + +**Equatable rewritten.** Old: manual outer-`switch` + inner `if case` dispatch, comparing media by `media.id` only. New: switch-over-tuple `(lhs, rhs)` with id-based equality per concrete type (`lFile.fileId == rFile.fileId`, `lImage.imageId == rImage.imageId`). Same id-based equality semantics as before. + +**Binding-site rewrite.** Old: `if case let .media(media, representation) = component.icon { ... if let file = media as? TelegramMediaFile { ... } else if let image = media as? TelegramMediaImage { ... } }`. New: a compound case-binding pattern `case let .mediaFile(_, representation), let .mediaImage(_, representation):` lifts the shared `representation` variable, then an inner switch dispatches to the right `setSignal` branch. Works because both cases carry the same `TelegramMediaImageRepresentation` payload type; Swift allows compound case patterns when the bindings have identical types. + +**Placeholder `PeerId(...)` construction fixup.** Second-pass build failure after dropping `import Postbox` surfaced a placeholder `PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(0))` in the `measureItem` layout-measurement instance at former line 1062. Naming `PeerId`, `PeerId.Namespace`, and `PeerId.Id` all require `import Postbox` (these are raw Postbox types, not TelegramCore typealiases — consistent with wave 9's `MessageId` → `EngineMessage.Id` fixup). Replaced with `component.context.account.peerId` (a real `EnginePeer.Id` already in scope). Semantically equivalent since the measurement instance's `messageId` is only used for `.peerId` extraction inside image-fetch `userLocation` and for Equatable comparison (the measurement instance isn't compared to anything). + +**Lesson.** Placeholder `PeerId(...)` / `MessageId(peerId:...)` constructions in layout-measurement code are a recurring trap for de-Postbox work. Common pattern in this codebase: construct a dummy component instance purely to call `.update(...)` and read back the returned size. The dummy values are not used meaningfully but naming the types pins `import Postbox`. When de-Postboxing, grep for `PeerId(namespace:`/`MessageId(peerId:` with all-zero args and replace with any convenient real value in scope (`context.account.peerId` is almost always available). + +Net: 1 file changed, +22 / -29 lines (−7 simplification — new switch-over-tuple Equatable is both terser and more idiomatic). + +Plan / record: `docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-10.md`. + +## Wave 11 outcome (2026-04-20) + +Revisits `ActionSheetPeerItem` — one of the six wave-1 abandonments. The wave-1 blocker was that the public init took `postbox: Postbox` + `network: Network` explicitly, forcing the module to `import Postbox`, and the sole external caller (ShareController, out-of-wave at the time) couldn't be edited. This wave resolves the blocker without any rule-2 violation by routing the pair through `AccountStateManager`. + +**Init-surface collapse.** `ActionSheetPeerItem.init(accountPeerId:postbox:network:contentSettings:peer:…)` → `.init(accountPeerId:stateManager:contentSettings:peer:…)`. `AccountStateManager` is a TelegramCore public class whose public API surface includes `postbox: Postbox` and `network: Network` fields; passing the manager as a single handle lets the module hold on to the two values without ever naming `Postbox` in its own source. The setItem call site becomes `self.avatarNode.setPeer(…, postbox: item.stateManager.postbox, network: item.stateManager.network, …)` — Swift's type inference resolves `Postbox` through transitive module visibility (TelegramCore → AvatarNode), no `import Postbox` needed in the consumer. + +**Convenience init unchanged in shape.** The `(context: AccountContext, …)` convenience delegates to `(accountPeerId:stateManager:contentSettings:…)`; the two callable forms stay aligned. + +**Caller (`ShareController.swift:1146`).** Dropped `postbox: info.account.stateManager.postbox, network: info.account.stateManager.network` → single `stateManager: info.account.stateManager`. `ShareControllerAccountContext` (the per-switchable-account protocol) already exposes `stateManager: AccountStateManager`, so this is a collapse, not a signature divergence. ShareController continues to import Postbox for its own unrelated reasons; no change to its dependency profile. + +**Reusable pattern.** For any wave-1-style module that was abandoned because a public init takes `postbox: Postbox, network: Network` with avatar-rendering downstream: collapse to `stateManager: AccountStateManager` (TelegramCore type) and unpack inside the setItem/setPeer body. The pattern applies broadly — most wave-1 abandonments used this param-pair for avatar setup. Candidates to try next: `ChatListSearchRecentPeersNode`, `HorizontalPeerItem`, `SelectablePeerNode`, `ItemListPeerItem`, `ItemListAvatarAndNameInfoItem`, `ItemListStickerPackItem` (verify each by grep first — some may use `postbox` for non-avatar reasons). + +Net: 3 files changed, +8 / -15 lines. Build green (5854 actions, ~6min). + +Plan / record: (no plan doc this wave — single-module, low-complexity). + +## Wave 12 outcome (2026-04-20) + +Applies the wave-11 `stateManager: AccountStateManager` collapse pattern to `HorizontalPeerItem` — another wave-1-era candidate whose public init leaked `postbox: Postbox, network: Network`. Additionally ripples the collapse one layer up into `ChatListSearchRecentPeersNode`'s public init so the `HorizontalPeerItem` call site has `stateManager:` in scope. + +**`HorizontalPeerItem` fully Postbox-free.** `init(postbox: Postbox, network: Network, …)` + matching stored fields → `init(stateManager: AccountStateManager, …)` + `let stateManager`. SelectablePeerNode.setup call site routes via `item.stateManager.postbox` / `.network`. Module drops `import Postbox` and `//submodules/Postbox:Postbox` dep. + +**`ChatListSearchRecentPeersNode` public surface migrated, module still imports Postbox.** Public `init(accountPeerId:postbox:network:…)` → `init(accountPeerId:stateManager:…)`. Two private helpers (`item(…)` on `ChatListSearchRecentPeersEntry` and `preparedRecentPeersTransition(…)`) get the same collapse for forwarding. Internal uses of raw postbox (`_internal_recentPeers`, `postbox.peerView`, `postbox.combinedView`, `_internal_managedUpdatedRecentPeers`) rewritten to `stateManager.postbox` / `stateManager.network` — the module stays on `import Postbox` because of `PostboxViewKey` / `UnreadMessageCountsItem` / `UnreadMessageCountsView` usage inside the peerViews-to-unread-counts pipeline. That pipeline could be rewritten against `EngineDataMap` + `TelegramEngine.EngineData.Item.Peer.Notifications.*` in a future wave, but the public surface simplification is valuable standalone. + +**Two external caller sites migrated:** +- `ShareController/Sources/ShareControllerRecentPeersGridItem.swift:66-67` — `postbox: context.stateManager.postbox, network: context.stateManager.network` → `stateManager: context.stateManager` (ShareControllerAccountContext protocol already exposes `stateManager`). +- `ChatListUI/Sources/ChatListRecentPeersListItem.swift:125-126` — `postbox: item.context.account.postbox, network: item.context.account.network` → `stateManager: item.context.account.stateManager`. +- `SettingsUI/Sources/DeleteAccountPeersItem.swift:51-52` (call site for `HorizontalPeerItem`) — `postbox: context.account.postbox, network: context.account.network` → `stateManager: context.account.stateManager`. + +**Lesson reinforcement.** The wave-11 collapse pattern is very cheap to ripple through intermediate owners. Whenever a consumer module takes `(postbox:Postbox, network:Network)` purely to forward them to another call downstream, collapse to `stateManager: AccountStateManager` — no propagation fan-out required for the raw pair because the stateManager is a single handle. Even when the intermediate owner itself uses raw `postbox.peerView` internally (like this wave's `ChatListSearchRecentPeersNode`), the public surface still gets the collapse at zero cost. + +Net: 6 files changed, +26 / -36 lines. Build verified green (incremental, 136 actions). + +Plan / record: (no plan doc this wave — pattern-application, low-complexity). + +## Wave 13 outcome (2026-04-20) + +Targeted `AttachmentTextInputPanelNode` at the user's request. On inspection, the module was already Postbox-free at the source level (swept in wave 6) — its two `.swift` files compile fine without `import Postbox`. Two leftover items were fixed: + +1. **Dead `//submodules/Postbox:Postbox` BUILD dep** — wave 6 swept `^import Postbox$` lines from source but never touched BUILD files. `AttachmentTextInputPanelNode/BUILD` (and, it turns out, 97 other modules' BUILDs — see wave 14) still listed the dep despite no source file needing it. Removed. +2. **Two raw `peerId?.namespace == Namespaces.Peer.SecretChat` checks** (lines 436, 2102) migrated to use the existing `PeerId.isSecretChat` extension at `submodules/TelegramCore/Sources/Utils/PeerUtils.swift:615`. (First-pass attempt introduced a duplicate `isSecretChat` extension and failed with "invalid redeclaration" — note for future waves: always grep TelegramCore for an existing helper before adding.) + +**No new TelegramEngine methods/types introduced.** The refactor was smaller than anticipated; the module's migration debt had already been paid down by wave 6's source-level sweep. The BUILD-dep leftover and the namespace-equality sites were the only remaining items. Both are quality-of-life cleanups rather than structural migration. + +**Observation that drove wave 14.** Wave 6's methodology-note in the "Unused-import sweeps" guidance only measured Postbox-freeness by `^import Postbox$` lines in sources. After touching `AttachmentTextInputPanelNode/BUILD` in this wave, I noticed many other wave-6-swept modules still carry dead BUILD deps, ~= the wave-6 survivor count. That's the whole of wave 14. + +Net: 2 files changed, +2 / -3 lines. + +Plan / record: (no plan doc this wave — discovery pass). + +## Wave 14 outcome (2026-04-20) + +Build-dep sweep analogous to wave 6's source-import sweep: drop `//submodules/Postbox:Postbox` (and `//submodules/Postbox`) from every BUILD whose source files no longer `import Postbox`. + +**Methodology.** +1. For each `submodules/*/BUILD` referencing `submodules/Postbox`, check whether any `.swift` file in the module's `Sources/` tree has `^import Postbox$`. +2. If none do, speculatively drop the Postbox dep line from the BUILD via `sed -i '' -e '/^[[:space:]]*"\/\/submodules\/Postbox\(:Postbox\)\{0,1\}",[[:space:]]*$/d'`. +3. Full `Make.py build --continueOnError`. +4. Restore any BUILD that now fails to compile (none did). +5. Commit surviving drops. + +**Result.** 98 candidate BUILDs identified. **Zero iterations needed** — first-pass build came up green (80 incremental actions, no restores). Net: 98 BUILD files, −98 lines (each lost exactly its `//submodules/Postbox` dep line). + +**Why zero iterations.** Bazel Swift rules require source-level `import` for symbol resolution. If a module compiled after wave 6's `import Postbox` sweep, then none of its source files are physically referencing Postbox symbols. The BUILD-level dep was always redundant — it was carried for historical reasons (code likely once imported Postbox but was migrated off) but had no effect on either compilation or the actual dependency graph (Postbox is still transitively pulled in by TelegramCore, which every module depends on). Dropping it is a metadata cleanup with no semantic effect. + +**Lesson / reusable pattern.** +- After every source-level `import Postbox` sweep (wave-6 shape), run a matching BUILD-dep sweep immediately. Same candidate set, near-zero execution risk, same commit. +- Script for identifying candidates: + ```bash + find submodules -name "BUILD" -type f | while read build; do + dir=$(dirname "$build") + if grep -q "submodules/Postbox" "$build" 2>/dev/null && [ -d "$dir/Sources" ]; then + if ! grep -rq "^import Postbox$" "$dir/Sources" 2>/dev/null; then + echo "$dir" + fi + fi + done + ``` +- After waves 13+14, 194 modules still list `//submodules/Postbox` in BUILD — all of them have source files still importing Postbox. + +Net (wave 14 alone): 98 files changed, 0 insertions / 98 deletions. + +Plan / record: (no plan doc this wave — mechanical sweep). + +## Wave 15 outcome (2026-04-20) + +Applies the wave-11/12 `stateManager: AccountStateManager` collapse pattern to `SelectablePeerNode` — another wave-1-era candidate listed in the post-wave-14 shortlist. Module becomes fully Postbox-free (source + BUILD). + +**`SelectablePeerNode` fully Postbox-free.** Two public setup methods migrated: +- `setup(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, …)` → `setup(accountPeerId:, stateManager: AccountStateManager, …)`. +- `setupStoryRepost(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, …)` → `setupStoryRepost(accountPeerId:, stateManager: AccountStateManager, …)`. + +Internal forwards rewired: `AvatarNode.setPeer(…, postbox: stateManager.postbox, network: stateManager.network, …)` and `EmojiStatusComponent(postbox: stateManager.postbox, …)`. Neither site names `Postbox` in the consumer — Swift infers through transitive module visibility. + +**Namespaces.Peer.SecretChat fixup (×3).** Replaced `peer.peerId.namespace == Namespaces.Peer.SecretChat` checks with `peer.peerId.isSecretChat` at three sites, matching the wave-13 pattern (`PeerId.isSecretChat` at `TelegramCore/Sources/Utils/PeerUtils.swift:611`). The third site (`updateSelection` in the not-selected branch) additionally needed an `?? false` fallback — previous expression was `self.peer?.peerId.namespace == Namespaces.Peer.SecretChat` (optional-equals-non-optional produces `Bool`), new expression is `(self.peer?.peerId.isSecretChat ?? false)`. + +**Share-Extension boundary — `stateManager:` over `engine:`.** `SelectablePeerNode` is used by `ShareControllerPeerGridItem`, whose context is `ShareControllerAccountContext`. That protocol exposes `stateManager: AccountStateManager` and `engineData: TelegramEngine.EngineData`, but **no `engine: TelegramEngine`** — and the Share Extension's `ShareControllerAccountContextExtension` concrete impl has no `Account`, so constructing a full `TelegramEngine` (`init(account: Account)`) is physically unreachable there. This is the documented "rare but genuine" fallback to `stateManager:` from the user-preference memory (`feedback_postbox_refactor_handle.md`) — prefer `engine:` except when crossing the Share-Extension boundary. + +**Three external call sites migrated:** +- `HorizontalPeerItem/Sources/HorizontalPeerItem.swift:227` (wave 12's `stateManager:` field now forwards directly): `postbox: item.stateManager.postbox, network: item.stateManager.network` → `stateManager: item.stateManager`. +- `ShareController/Sources/ShareControllerPeerGridItem.swift:237` (setup): `postbox: context.stateManager.postbox, network: context.stateManager.network` → `stateManager: context.stateManager`. +- `ShareController/Sources/ShareControllerPeerGridItem.swift:273` (setupStoryRepost): same. + +**Convenience init unchanged.** `setup(context: AccountContext, …)` now delegates with `stateManager: context.account.stateManager`; signature unchanged — `JoinLinkPreviewPeerContentNode.swift:147` (the one caller using the convenience init) needed no edit. + +Net: 4 files changed, +12 / -17 lines. Build verified green (193 actions, 131s — Telegram.ipa target built successfully). + +Plan / record: (no plan doc this wave — pattern-application, low-complexity). + +## Wave 16 outcome (2026-04-20) + +Two-commit wave targeting `ItemListPeerItem`. Planning-time inventory (`project_postbox_wave16_plan.md`) only grepped for `Postbox`/`Network` tokens and missed two Postbox-defined public-surface types: `EngineMessageHistoryThread.Info?` (on `threadInfo`) and `PeerStoryStats?` (on `storyStats`). The first-pass "drop `import Postbox`" attempt failed at build time. Rather than abandon, the wave split into 16a (move `EngineMessageHistoryThread` to TelegramCore — clean, independently valuable) and 16b (partial `engine:` collapse on `ItemListPeerItem`, keeping `import Postbox` because `PeerStoryStats` remains Postbox-defined). + +**Wave 16a — move `EngineMessageHistoryThread` to TelegramCore.** Before: Postbox declared an empty `public final class EngineMessageHistoryThread` namespace with a nested `public final class Item`; TelegramCore's `ForumChannels.swift` added the `.Info` nested type via `public extension EngineMessageHistoryThread { final class Info … }`. The outer name's Postbox residency forced every consumer of `.Info` to `import Postbox` too. After: promote Postbox's internal `MutableMessageHistoryThreadIndexView.Item` to a top-level public type `MessageHistoryThreadIndexItem`; delete the empty `EngineMessageHistoryThread` class from Postbox; move the class shell into `ForumChannels.swift`, collapsing the existing extension into a proper class definition (`public final class EngineMessageHistoryThread { class Info … }`). + +`MessageHistoryThreadIndexView.items` type changes from `[EngineMessageHistoryThread.Item]` to `[MessageHistoryThreadIndexItem]`; its init simplifies (no more wrap/unwrap conversion — the old init re-built items element-by-element just to swap the outer wrapper name). The second public extension on `EngineMessageHistoryThread` (`.NotificationException`, at `ForumChannels.swift:1318`) works unchanged — same-module extension after the class moves. + +Zero consumer-site changes: the two Postbox-consumer iteration sites (`ChatListUI/Sources/Node/ChatListNodeLocation.swift:229`, `ShareController/Sources/ShareControllerNode.swift:2086`) iterate with `for item in view.items` (no type annotation) and access only fields that exist identically on both types (`id`, `info`, `index`, `pinnedIndex`, `tagSummaryInfo`, `topMessage`, `embeddedInterfaceState`). + +Commit `3bb22d503c`. Net: 2 files, +67 / −111 (Postbox file nets −174 lines, TelegramCore file +4). + +**Wave 16b — `ItemListPeerItem.Context` `engine:` collapse.** Wave-11 pattern applied to `ItemListPeerItem.Context.Custom`. Before: `Context.Custom.init(accountPeerId:, postbox: Postbox, network: Network, animationCache:, animationRenderer:, isPremiumDisabled:, resolveInlineStickers:)` + matching stored fields; `Context` had computed `postbox: Postbox` and `network: Network` that switched over the `.account` / `.custom` cases. After: `Context.Custom.init(accountPeerId:, engine: TelegramEngine, animationCache:, animationRenderer:, isPremiumDisabled:, resolveInlineStickers:)`; `Context` has one computed `engine: TelegramEngine` that returns `context.engine` for the `.account` case and `custom.engine` for the `.custom` case. Six internal forwards rewire from `item.context.postbox` / `item.context.network` to `item.context.engine.account.postbox` / `item.context.engine.account.network` (three `EmojiStatusComponent(postbox:…)` sites and three `AvatarNode.setPeer(…, postbox:…, network:…, …)` sites). + +Handle choice: `engine:` (not `stateManager:`). The sole external `.custom(Custom(...))` construction site codebase-wide is `PeerInfoSettingsItems.swift:121` — main-app-only, doesn't cross the Share-Extension boundary. `peerAccountContext` in that loop is typed `AccountContext` (from the `accountsAndPeers: [(AccountContext, EnginePeer, Int32)]` field), so `.engine: TelegramEngine` is directly available. Per the standing guidance from `feedback_postbox_refactor_handle.md`, prefer `engine:` except when physically forced to `stateManager:` by a Share-Extension boundary. + +All 37 other `ItemListPeerItem(…)` construction sites use the `.account(context: AccountContext)` convenience overload (at L485) and need no change. `PeerInfoScreenMemberItem.swift:223` forwards its own `context: ItemListPeerItem.Context` field straight through (pass-through) — no change. + +Module does **not** become Postbox-free: `PeerStoryStats?` remains on the `storyStats` public-surface field. `PeerStoryStats` is defined in `Postbox/Sources/ChatListView.swift:281` and is deeply baked into Postbox view APIs (`PeerView.storyStats`, `PeerStoryStatsView.storyStats`, `ChatListEntry.storyStats`, `MessageHistoryView.peerStoryStats`, `Postbox.getPeerStoryStats(peerId:)`). Moving it would require a cross-module wrapper rewrite across Postbox, TelegramCore, and every view consumer — out of scope for wave 16. + +Commit `a5432e44a8`. Net: 2 files, +17 / −30. + +**Lessons.** +- **Public-surface inventory must go beyond the collapse-target tokens.** Waves 11/12/15's `stateManager`/`engine` collapses were clean because their target modules had no other Postbox-defined public types. Wave 16's planning inventory only grepped for `Postbox`/`Network` and missed `EngineMessageHistoryThread` + `PeerStoryStats` — both symbols whose names happen to not include `Postbox`. For future wave-11-pattern candidates, planning-time grep should include the full alphabet of Postbox-defined public types: `^public\s+(class|struct|enum|protocol|typealias)\s+\w+` over `submodules/Postbox/Sources/` to build an exhaustive type-name allowlist, then grep for any of those names in the candidate module's public surface. +- **"Engine"-prefixed types can still be Postbox-defined.** `EngineMessageHistoryThread` has an "Engine" prefix but was declared in Postbox all along; the `.Info` nested type living in TelegramCore was a code-organization half-measure that still forced `import Postbox` on consumers. Don't trust naming conventions; grep for the defining module. +- **Splitting a failing wave into a cleanup + a partial collapse is often the right move.** Wave 16 could have been abandoned entirely when the build failed — instead, the `EngineMessageHistoryThread` move (which had been a latent cleanup opportunity for the entire history of the `.Info` extension) was promoted to a standalone commit (16a), and the partial `engine:` collapse shipped as a second commit (16b). Both are independently valuable; the wave's "module becomes Postbox-free" goal didn't land but other goals did. +- **The "promote internal Postbox `Item` to top-level, drop Postbox wrapper class, move wrapper class to TelegramCore" pattern generalizes.** Any Postbox-defined class whose only role is to namespace a TelegramCore extension is a candidate for this move. Future audit target: `grep -l "public extension " submodules/TelegramCore/Sources/` where `` is a Postbox-defined outer type with no semantic content of its own. + +Plan / record: `project_postbox_wave16_plan.md` (updated with outcome). + +## Wave 17 outcome (2026-04-20) + +Applies the wave-11/12/15 `stateManager: AccountStateManager` collapse pattern to `ItemListAvatarAndNameInfoItem` — another wave-1-era candidate. Module becomes fully Postbox-free (source + BUILD). Clean one-shot execution (no abandonment, no replan). + +**`ItemListAvatarAndNameInfoItem.ItemContext` enum case collapsed.** Before: `case other(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network)` + matching destructure at L761 + `AvatarNode.setPeer(…, postbox: postbox, network: network, …)` internal forward. After: `case other(accountPeerId: EnginePeer.Id, stateManager: AccountStateManager)` + `case let .other(accountPeerId, stateManager):` destructure + `AvatarNode.setPeer(…, postbox: stateManager.postbox, network: stateManager.network, …)` forward. The `.accountContext(AccountContext)` sister case is unchanged. + +**Share-Extension-boundary handle choice: `stateManager:`.** The sole external `.other(...)` construction site codebase-wide is `DeviceContactInfoController.swift:413`, inside a ternary that fires only when `arguments.context` is not a `ShareControllerAppAccountContext` — i.e., when running inside the Share Extension. `ShareControllerAccountContext` (protocol at `AccountContext/Sources/ShareController.swift:16`) exposes `stateManager: AccountStateManager` but not `engine: TelegramEngine`, and constructing a full `TelegramEngine` is physically unreachable in the Share Extension's `ShareControllerAccountContextExtension` impl (no `Account`). Per `feedback_postbox_refactor_handle.md` and the wave-15 precedent, use `stateManager:` at Share-Extension boundaries. + +**Pre-flight inventory was correct.** Running the public-Postbox-type inventory grep returned only `Postbox` itself (the one enum-case payload leak) — no `EngineMessageHistoryThread`-style surprises. Wave 17 validates the post-wave-16 lesson: when planning-time inventory uses the full Postbox public-types allowlist (not just `Postbox`/`Network` tokens), wave-11-shape candidates execute cleanly. + +**Single external caller migrated:** +- `PeerInfoUI/Sources/DeviceContactInfoController.swift:413` — `postbox: arguments.context.stateManager.postbox, network: arguments.context.stateManager.network` → `stateManager: arguments.context.stateManager`. The enclosing `PeerInfoUI` module still imports Postbox for its own unrelated reasons; that stays. + +The 5 other `ItemListAvatarAndNameInfoItem(itemContext:…)` construction sites codebase-wide all use `.accountContext(arguments.context)` and need no change (`ChannelBannedMemberController.swift:321`, `DeviceContactInfoController.swift:415`, `ChannelAdminController.swift:370`, `CreateChannelController.swift:197`, `CreateGroupController.swift:324`). + +**Pattern-consistency note (reinforced).** `accountPeerId: EnginePeer.Id` is kept as a separate enum-case payload even though `AccountStateManager` also exposes `accountPeerId`. This matches waves 11/12/15 (`ActionSheetPeerItem`, `ChatListSearchRecentPeersNode`, `SelectablePeerNode` all kept `accountPeerId` explicit alongside `stateManager`). Future wave-11-pattern executions should default to this shape unless a specific reason exists to collapse further. + +Net: 3 files changed, +4 / -5 lines (ItemListAvatarAndNameItem.swift: +2 / -3, DeviceContactInfoController.swift: +1 / -1, BUILD: −1). Build verified green for target modules (`ItemListAvatarAndNameInfoItem`, `PeerInfoUI` both compiled and linked successfully); the one unrelated failing target in the full build (`ChatMessageInteractiveMediaNode.swift`) is user-uncommitted work-in-progress that predates this wave. + +Plan / record: (plan doc `project_postbox_wave17_plan.md` deleted post-commit per the plan's own post-commit housekeeping instructions). + +## Wave 18 outcome (2026-04-20) + +Mixed-shape wave targeting `ItemListStickerPackItem`. Originally shortlisted (post-wave-17) as "likely wave-11 shape", but plan-writing-time inspection invalidated that hypothesis — the module's public API doesn't take `postbox:`/`network:`. Actual shape combined three existing wave patterns plus a narrow typealias addition. Module becomes fully Postbox-free (source + BUILD). + +**Three narrow typealiases added to TelegramCore.** `submodules/TelegramCore/Sources/TelegramEngine/Utils/EnginePostboxCoding.swift` grew by 3 lines: + +- `EngineItemCollectionId = ItemCollectionId` — needed at public closure-param positions. +- `EngineFetchResourceSourceType = FetchResourceSourceType` — needed at `var updatedFetchSignal` type annotation. +- `EngineFetchResourceError = FetchResourceError` — same. + +Per CLAUDE.md rule 1 these narrow-utility typealiases are explicitly allowed (same shape as the existing `EngineMemoryBuffer`/`EnginePostboxDecoder`/… batch). Cheat sheet updated. + +**Wave-4 enum-payload migration on `StickerPackThumbnailItem`.** Public enum case `animated(MediaResource, PixelDimensions, Bool, Bool)` → `animated(EngineMediaResource, PixelDimensions, Bool, Bool)`. Equatable `==` simplified: `lhsResource.isEqual(to: rhsResource)` → `lhsResource == rhsResource` (uses `EngineMediaResource.==` which has identical semantics). Two construction sites wrapped via `EngineMediaResource(thumbnail.resource)` / `EngineMediaResource(itemFile.resource)`. Two destructure-and-forward sites unwrap via `resource._asResource()` when handing off to `chatMessageStickerPackThumbnail(resource: MediaResource)` and `AnimatedStickerResourceSource(account:, resource: MediaResource, …)`. One `resource.id` site (for `shortLivedResourceCachePathPrefix`) needs the raw `MediaResourceId`, handled by a local `let rawResource = resource._asResource()` that serves both the `.id` read and the `AnimatedStickerResourceSource` init in the same block. + +**Wave-3 facade swap.** `fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: resourceReference)` → `item.context.engine.resources.fetch(reference: resourceReference, userLocation: .other, userContentType: .sticker)`. Engine facade (`TelegramEngine.Resources.fetch`) already exists from wave 3; no new TelegramEngine API needed. + +**External-caller check confirmed zero source edits needed.** `StickerPackThumbnailItem` has no external consumers (UndoUI declares its own nested-private same-named enum). The 6 external `ItemListStickerPackItem(setPackIdWithRevealedOptions:)` caller sites all pass closures with inferred param types; `EngineItemCollectionId` being a typealias to `ItemCollectionId` makes the types interchangeable. The 3 module-field declarations outside the target module that name `(ItemCollectionId?, ItemCollectionId?) -> Void` explicitly (`SettingsUI/Stickers/ArchivedStickerPacksController.swift:27`, `SettingsUI/Stickers/InstalledStickerPacksController.swift:27`, and the init at L32/L42 of those same files) compile unchanged — those modules still import Postbox for their own reasons, and `EngineItemCollectionId == ItemCollectionId` so no rename is required. + +**BUILD dep dropped.** `//submodules/Postbox:Postbox` removed from `submodules/ItemListStickerPackItem/BUILD`. + +**Pre-existing `ChatMessageInteractiveMediaNode.swift` WIP still present at build time — no longer failing.** The uncommitted change introduces an `allowSticker` validation around secret-chat sticker playback (~30 lines added in the `currentReplaceAnimatedStickerNode` block). Per wave-17's note it had failed to compile; on this wave's full build (`bazel build Telegram/Telegram`, 565 actions, 258s, 0 errors) it compiled and linked without issue. Either the user fixed it between waves 17 and 18, or the bazel dependency graph simply needed a full rebuild. Either way, wave 18's build was clean end-to-end — `Telegram.ipa` target built successfully, zero errors across the entire project. + +**Pattern-consistency note.** Wave 18 is the third wave (after 3 and 9) where the cheapest path requires adding narrow TelegramCore-side typealiases rather than keeping `import Postbox` in the consumer. The threshold is: if the consumer needs to NAME a Postbox-defined type (not just use it via inference), and no engine-prefixed alias exists, adding a narrow typealias is preferred over `import Postbox`. The alternative of refactoring the code to avoid naming the type (e.g., reshaping `var foo: Signal?` to infer from first assignment) is usually unwieldy when the var is conditionally-assigned; typealiases win on readability. + +Net: 3 files changed. +- `submodules/TelegramCore/Sources/TelegramEngine/Utils/EnginePostboxCoding.swift`: +3 / -0. +- `submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift`: ~13 lines touched across 9 sites; net +4 / -4. +- `submodules/ItemListStickerPackItem/BUILD`: 0 / -1. +- `CLAUDE.md`: +3 cheat-sheet lines + this outcome paragraph. + +Plan / record: `memory/project_postbox_wave18_plan.md` (deleted post-commit per the plan's own housekeeping instructions). + +## Wave 19 outcome (2026-04-20) + +Single-facade expansion. Additive-only — adds `TelegramEngine.Resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id) -> String` at `submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift:456`. Body: `self.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(MediaResourceId(id.stringRepresentation))`. + +No consumer migrations this wave. Known consumers (≥25 call sites across ~15 modules: AvatarVideoNode, DrawingUI, SettingsUI/ThemePickerGridItem, PremiumUI/StickersCarouselComponent, ReactionSelectionNode, ReactionContextNode, ChatSendMessageActionUI, ItemListStickerPackItem, ChatThemeScreen, ThemeCarouselItem, PeerInfoBirthdayOverlay, SettingsThemeWallpaperNode, MediaEditorComposerEntity, ChatQrCodeScreen, ChatMessageAnimatedStickerItemNode, ChatMessageItemView, GiftCompositionComponent) migrate in a follow-up wave using the pattern `X.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(Y.resource.id)` → `X.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(Y.resource.id))`. + +**Why not bundle consumer migration in the same wave?** Wave-3's original shape did bundle (3 facades + 1 full consumer module in one commit), but the consumer pool for this particular facade is large (~25 sites) and each call site only partially de-Postboxes its module — the caller modules need full inventory before deciding whether to drop `import Postbox`. Keeping wave 19 narrow (facade-only) lets follow-up waves approach consumer-module migration on a per-module basis without the facade-addition blocking anything. + +Net: 1 file changed, +4 / -0. + +Plan / record: (no plan doc this wave — single-method addition, target pre-identified in `project_postbox_refactor_next_wave.md`). + +## Wave 20 outcome (2026-04-21) + +Consumer sweep for the wave-19 `shortLivedResourceCachePathPrefix` facade. 22 call sites across 16 modules migrated atomically. Pattern (repeated identically at every site): `X.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(Y.resource.id)` → `X.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(Y.resource.id))`. + +**Modules migrated (alphabetical):** +- `AvatarVideoNode/Sources/AvatarVideoNode.swift` (1 site) +- `ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift` (1 site) +- `DrawingUI/Sources/DrawingStickerEntityView.swift` (1 site) +- `ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift` (1 site; simplified from wave-18's `let rawResource = resource._asResource(); …shortLivedResourceCachePathPrefix(rawResource.id)` + `AnimatedStickerResourceSource(…, resource: rawResource, …)` to `…shortLivedResourceCachePathPrefix(id: resource.id)` + `AnimatedStickerResourceSource(…, resource: resource._asResource(), …)` — drops the intermediate `let rawResource`) +- `PremiumUI/Sources/StickersCarouselComponent.swift` (2 sites) +- `ReactionSelectionNode/Sources/ReactionContextNode.swift` (2 sites) +- `ReactionSelectionNode/Sources/ReactionSelectionNode.swift` (6 sites — 4 unique expression templates, handled via targeted Edits against the unique argument expression at each call) +- `SettingsUI/Sources/ThemePickerGridItem.swift` (1 site) +- `TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift` (2 sites) +- `TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift` (1 site) +- `TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift` (1 site) +- `TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift` (1 site) +- `TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift` (3 sites) +- `TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoBirthdayOverlay.swift` (2 sites) +- `TelegramUI/Components/Settings/SettingsThemeWallpaperNode/Sources/SettingsThemeWallpaperNode.swift` (1 site) +- `TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift` (1 site) + +**One site intentionally skipped:** `TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift:245`. That site uses a local `postbox: Postbox` init-parameter, not `context.account.postbox`, so the migration would require changing the init's parameter from `postbox:` to something engine-based and fanning out to its callers. Out of scope — handled by a future module-scoped wave. + +**No modules became Postbox-free this wave.** Each of the 16 migrated modules still has other Postbox usage (raw `Postbox` types in signatures, `fetchedMediaResource(mediaBox:)` calls, `postbox.transaction`, etc.). Consumer-side `shortLivedResourceCachePathPrefix` closure is just one of several reasons these modules import Postbox. Future wave-shape: module-scoped de-Postbox per-module inventory. + +**Pattern validation.** This is the most mechanical consumer sweep to date — all 22 sites followed identical shape, allowing `replace_all=true` for sites with duplicate identical call expressions (ReactionSelectionNode hit this at 3 sites for `largeListAnimation`, 2 for `stillAnimation`, 1 for `listAnimation`). First-pass build was clean (35 actions, 0 errors) — no iteration loop. Confirms the wave-19 facade shape is sound. + +**Build verification.** `bazel build Telegram/Telegram --keep_going` — 2042 action cache hits + 35 new actions, 0 errors, `Telegram.ipa` up-to-date. + +Net: 16 files changed, all edits mechanical (before → after): +22 insertions / -22 deletions at migrated sites, plus 1 deletion in ItemListStickerPackItem (wave-18 `let rawResource` line dropped). Approximate total: +22 / -23. + +Plan / record: (no plan doc this wave — mechanical sweep). + +## Wave 21 outcome (2026-04-21) + +Combined wave-19+wave-20 shape: facade addition + consumer sweep in a single atomic commit. Adds `TelegramEngine.Resources.completedResourcePath(id: EngineMediaResource.Id, pathExtension: String? = nil) -> String?` facade; sweeps 29 consumer sites across 14 files. + +**Facade added at `TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift:460`.** Body: `self.account.postbox.mediaBox.completedResourcePath(id: MediaResourceId(id.stringRepresentation), pathExtension: pathExtension)`. Wraps the Postbox `MediaBox.completedResourcePath(id: MediaResourceId, pathExtension: String?)` overload; consumers that previously called the resource-taking overload (`MediaBox.completedResourcePath(_ resource: MediaResource, …)`) migrate through the id path (`.resource.id` is already `MediaResourceId`). + +**28 Shape-A consumer sites + 1 Shape-B (already-id-overload) migrated:** +- `SettingsUI/Sources/Themes/EditThemeController.swift` (1 site) +- `BrowserUI/Sources/BrowserPdfContent.swift` (1 site) +- `BrowserUI/Sources/BrowserDocumentContent.swift` (1 site) +- `GalleryUI/Sources/SecretMediaPreviewController.swift` (1 site) +- `TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift` (1 site, `pathExtension: "mp4"`) +- `TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperUtils.swift` (7 sites across 3 functions; 4 unique expression templates, handled via `replace_all=true` where identical) +- `TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift` (1 site) +- `TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift` (5 sites; 4 used `resource` expr identically via `replace_all=true`, 1 used `file.file.resource`) +- `TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift` (1 site, `pathExtension: nil`) +- `TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift` (1 site) +- `TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift` (7 sites, all identical `telegramFile.resource` — handled via `replace_all=true`) +- `TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift` (1 site) +- `TelegramUI/Sources/OpenChatMessage.swift` (1 site) +- `TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift` (1 site) +- `TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift` (1 site, Shape B — was already using the `id:` overload; migrated identically to `EngineMediaResource.Id(...)`) + +**8 sites intentionally skipped (Shape C/D).** Listed in the plan — 5 Shape-C sites that access a raw `account: Account` parameter (no `.engine` on `Account`) and 3 Shape-D sites that carry a local `postbox: Postbox` stored field. Both shapes need module-scoped init-signature rework rather than per-site sweep; defer to future waves. + +**No modules became Postbox-free.** Each consumer has other Postbox usage (signatures, transactions, other mediaBox calls). Matches waves 19/20's expectation. + +**Build validation.** `bazel build Telegram/Telegram --keep_going` — clean first-pass build (569 processes, 1556 action cache hits + 30 local + 532 worker, 240s, 0 errors, `Telegram.ipa` up-to-date). + +**Pattern validation.** Wave-shape G (facade addition + consumer sweep in a single commit) works well when the consumer pool is bounded and mechanical. 29 sites in 14 files is comfortably within the threshold. Kept waves 19 and 20 separate because 25+ sites across that many modules was at the edge of reviewability; wave 21's similar fan-out fits because the plan pre-classified every site by shape. When the plan does the classification work upfront, combined waves are cheaper to review and ship. + +Net: 14 files changed. TelegramEngineResources.swift: +4 / -0. Consumer files: +29 / -29 (mechanical rewrite at each site). CLAUDE.md: +outcome paragraph. + +Plan / record: `memory/project_postbox_wave21_plan.md` (deleted post-commit per the plan's own housekeeping instructions). + +## Wave 22 outcome (2026-04-21) + +Follows wave 21's pattern: facade addition + consumer sweep in a single atomic commit. Adds `TelegramEngine.Resources.storeResourceData(id: EngineMediaResource.Id, data: Data, synchronous: Bool = false)` facade; sweeps 46 consumer sites across 17 files. + +**Facade added at `TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift:464`.** Body: `self.account.postbox.mediaBox.storeResourceData(MediaResourceId(id.stringRepresentation), data: data, synchronous: synchronous)`. Wraps Postbox's `MediaBox.storeResourceData(_ id: MediaResourceId, data: Data, synchronous: Bool)` full-file overload. The range-store overload (`MediaBox.storeResourceData(_:range:data:)`) is used at a single site inside `HLSVideoJSNativeContentNode.swift:302` via a local `postbox: Postbox` field (Shape D), which is out of scope for this wave; the range overload gets no facade wrapper this round. + +**46 Shape-A consumer sites migrated:** +- `ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift` (2) +- `DebugSettingsUI/Sources/DebugController.swift` (8 — 6 identical `gzippedData` batched via `replace_all=true`; `logData`, `allStatsData` handled individually) +- `BrowserUI/Sources/BrowserWebContent.swift` (1) +- `TelegramUI/Sources/CreateChannelController.swift` (4) +- `TelegramUI/Sources/CreateGroupController.swift` (4) +- `TelegramUI/Sources/Chat/ChatControllerPaste.swift` (1) +- `TelegramUI/Sources/Chat/ChatControllerOpenDocumentScanner.swift` (3) +- `TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift` (2) +- `TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift` (2) +- `TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift` (2) +- `TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperUtils.swift` (6 — 3 `thumbnailResource`, 3 `resource`; both handled via `replace_all=true`) +- `SettingsUI/Sources/Themes/ThemePreviewController.swift` (1) +- `SettingsUI/Sources/Themes/EditThemeController.swift` (1) +- `TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift` (3) +- `TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift` (2) +- `TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift` (1) +- `TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift` (3) + +**Out of scope — not migrated this wave:** +- `accountManager.mediaBox.storeResourceData(...)` sites (Account-manager-scoped, not account-scoped) — 13+ sites across WallpaperGalleryItem, WallpaperGalleryController, ThemeAccentColorController, WallpaperUtils, WebBrowserSettingsController, ThemeUpdateManager, OpenResolvedUrl, and others. These are a different migration path entirely (not a `TelegramEngine.Resources.*` target) and stay raw. +- `account.postbox.mediaBox.storeResourceData(...)` (raw `Account`, no `AccountContext`) — ~9 sites in LegacyMediaPickerUI, TelegramCallsUI, InAppPurchaseManager, AuthorizationUI, PeerInfoScreenAvatarSetup closures, WallpaperResources. Shape C from wave-21 taxonomy. Needs per-module rework. +- `self.postbox.mediaBox.storeResourceData(...)` / `postbox.mediaBox.storeResourceData(...)` inside TelegramCore internals (`TransformOutgoingMessageMedia.swift`, `AccountStateManager.swift`, `AvailableReactions.swift`, `SaveSecureIdValue.swift`, `PeerPhotoUpdater.swift`, `NotificationSoundList.swift`, `Stories.swift`, `Authorization.swift`, `WebpagePreview.swift`). These are Postbox-internal layer by design — keep as-is. +- `HLSVideoJSNativeContentNode.swift:302` — uses the range-store overload via local `postbox: Postbox` field. Out of scope. + +**No modules became Postbox-free.** Matches waves 19/20/21 expectation — each consumer has other Postbox usage. + +**Build validation.** `bazel build Telegram/Telegram --keep_going` — clean first-pass build (571 processes, 1554 action cache hits + 30 local + 532 worker, 229s, 0 errors, `Telegram.ipa` up-to-date). + +**Pattern validation.** Wave-shape G (facade + consumer sweep in one commit) scales well up to 46 sites in 17 files when the pattern is mechanical. Heavy `replace_all=true` usage where call-text is identical across sites (DebugController's 6 `gzippedData` sites, WallpaperUtils' 6 sites split into 2 batches by first-arg variable, ChatControllerOpenDocumentScanner's identical `(resource.id, data: data, synchronous: true)` pattern) keeps diff noise to the minimum. 46 sites, mostly done via replace_all + a few individual edits. + +Net: 17 consumer files + 1 TelegramCore file + CLAUDE.md. TelegramEngineResources.swift: +4 / -0. Consumer files: +46 / -46 (mechanical rewrite). + +Plan / record: (no plan doc this wave — mechanical sweep following wave-21 recipe). + +## Wave 23 outcome (2026-04-21) + +Smallest wave so far: `cancelInteractiveResourceFetch` facade addition + consumer sweep. Same shape as waves 21/22. + +**Facade added at `TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift:468`.** Body: `self.account.postbox.mediaBox.cancelInteractiveResourceFetch(resourceId: MediaResourceId(id.stringRepresentation))`. Wraps Postbox's `MediaBox.cancelInteractiveResourceFetch(resourceId: MediaResourceId)` overload (the `_ resource: MediaResource` overload delegates to the id version anyway). + +**5 of 7 Shape-A consumer sites migrated:** +- `PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift` (1) +- `GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift` (1) +- `GalleryUI/Sources/Items/ChatImageGalleryItem.swift` (1) +- `GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift` (1) +- `GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift` (1) + +**2 sites intentionally skipped:** `ChatMessageInteractiveMediaNode.swift:1474, 1709` — this file has pre-existing uncommitted WIP (the `allowSticker` validation around secret-chat sticker playback, carried forward since before wave 17). Editing the 2 sites would mix my wave-23 changes with the user's WIP in a single git diff, which `git add` can't cleanly separate. Deferred until the WIP lands or a narrow follow-up wave intentionally includes both. Note: a future wave that aims to drop those 2 sites should first either (a) wait for the WIP to be committed or (b) use `git stash --keep-index` + targeted edits + selective staging to split the diff cleanly. + +**Pattern note on WIP interference.** This is the first wave to hit this failure mode — previous waves' mechanical sweeps happened not to touch `ChatMessageInteractiveMediaNode.swift`. Future sweeps should grep their candidate set against `git status`'s modified-files list before starting, and either (a) defer sites in WIP files, (b) wait for the WIP to commit, or (c) stage selectively via `git add --patch`-equivalent paths. + +**Build validation.** `bazel build Telegram/Telegram --keep_going` — clean first-pass build (558 processes, 1567 action cache hits + 19 local + 532 worker, 236s, 0 errors, `Telegram.ipa` up-to-date). + +Net: 5 consumer files + 1 TelegramCore file + CLAUDE.md. TelegramEngineResources.swift: +4 / -0. Consumer files: +5 / -5. + +Plan / record: (no plan doc this wave — mechanical sweep). + +## Wave 24 outcome (2026-04-21) + +`moveResourceData` facade additions + consumer sweep. Same shape as waves 21-23. + +**Two facades added at `TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift`:** +- `moveResourceData(id: EngineMediaResource.Id, toTempPath: String)` wraps the `(MediaResourceId, toTempPath:)` overload. +- `moveResourceData(from: EngineMediaResource.Id, to: EngineMediaResource.Id, synchronous: Bool = false)` wraps the `(from: MediaResourceId, to: MediaResourceId, synchronous:)` overload. + +Postbox's third overload `(MediaResourceId, fromTempPath:)` has no consumer-side usage; no facade added this wave (YAGNI). + +**6 Shape-A consumer sites migrated (5 files):** +- `TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift` (1, `toTempPath:`) +- `TelegramUI/Sources/OverlayAudioPlayerController.swift` (1, `from:to:synchronous:`) +- `TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift` (2) +- `TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift` (2) + +**Build validation.** `bazel build Telegram/Telegram --keep_going` — clean first-pass build (563 processes, 272s, 0 errors). + +Net: 5 consumer files + 1 TelegramCore file + CLAUDE.md. TelegramEngineResources.swift: +8 / -0. Consumer files: +6 / -6. + +Plan / record: (no plan doc this wave — mechanical sweep). + +## Wave 25 outcome (2026-04-21) + +`copyResourceData` facade additions + consumer sweep. Same shape as waves 21-24. + +**Two facades added:** `copyResourceData(id: EngineMediaResource.Id, fromTempPath: String)` and `copyResourceData(from: EngineMediaResource.Id, to: EngineMediaResource.Id, synchronous: Bool = false)`. + +**4 Shape-A consumer sites migrated (3 files):** +- `PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift` (2, `from:to:synchronous:`) +- `ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift` (1, `from:to:` — simplified from `localResource._asResource().id` to `localResource.id` since operands are `EngineMediaResource`) +- `TelegramUI/Sources/Chat/ChatControllerPaste.swift` (1, `id:fromTempPath:`) + +**Minor simplification lesson.** When a consumer already has an `EngineMediaResource`-typed local (e.g., from a wave-18-migrated callee), prefer `localResource.id` over `EngineMediaResource.Id(localResource._asResource().id)` — the two are semantically equivalent since `EngineMediaResource.id` is defined as `Id(self.resource.id)`. This halves the verbosity at the call site and removes a redundant unwrap-and-rewrap. + +**Build validation.** Clean first-pass build (563 processes, 242s, 0 errors). + +Net: 3 consumer files + 1 TelegramCore file + CLAUDE.md. TelegramEngineResources.swift: +8 / -0. + +Plan / record: (no plan doc this wave — mechanical sweep). + +## Wave 27 outcome (2026-04-22) + +`preferencesView` consumer sweep (wave-9 pattern continuation). No new TelegramCore facades — leverages existing `TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key:)`. + +**Shape.** Replace `context.account.postbox.preferencesView(keys: [])` — returning `Signal` — with `context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: ))` — returning `Signal`. Downstream, rename `.values[]?.get(.self)` → `?.get(.self)` at each closure parameter. + +**30 consumer files, ~40 call sites migrated** across ChatListUI, ContactListUI, DebugSettingsUI, GalleryUI, PeersNearbyUI, SettingsUI, TelegramCallsUI, TelegramUI, TelegramUI/Components, WebSearchUI. Full list in `git show --stat `. + +**Multi-key sites (PresentationCallManager).** 3 sites used `preferencesView(keys: [voipConfiguration, appConfiguration])`. Migrated via the two-arg `engine.data.subscribe(itemA, itemB) |> take(1)` overload, which returns `Signal<(PreferencesEntry?, PreferencesEntry?), NoError>`. Closures that accessed `preferences.values[X]?.get(...)` rewritten to `preferences.0?.get(...)` and `preferences.1?.get(...)`. + +**Direct-postbox-param helper migrated.** `AccountContext.swift`'s `getAppConfiguration(postbox: Postbox)` helper (one internal caller only) was rewritten to `getAppConfiguration(engine: TelegramEngine)` in the same commit, switching its single call site from `getAppConfiguration(postbox: account.postbox)` to `getAppConfiguration(engine: self.engine)`. + +**Annotation update in NotificationExceptionControllerNode.swift.** An explicit signal type `Signal<(…, PreferencesView, …), NoError>` in a `mapToSignal` return was updated to `Signal<(…, PreferencesEntry?, …), NoError>`. The file still imports Postbox because `PreferencesEntry` is (for now) a Postbox-defined type surfaced through TelegramCore's `EnginePreferencesEntry` typealias — a future wave-6-style `import Postbox` sweep would clean up such imports where they're now the only Postbox reference. + +**Deliberately skipped in this wave.** +- `TelegramPermissionsUI/Sources/PermissionSplitTest.swift:100` — `permissionUISplitTest(postbox: Postbox)` is a public API whose product value `PermissionUISplitTest` itself stores `postbox: Postbox` to satisfy the `SplitTest` protocol. Proper migration requires a protocol-level refactor (or wholesale rewrite of the SplitTest abstraction) beyond this wave's scope. +- 5 TelegramCore-internal `postbox.preferencesView(...)` sites (ChatListFiltering × 3, ContentSettings × 1, ManagedGlobalNotificationSettings × 1) — the refactor only migrates consumer modules, not TelegramCore internals. + +**Build validation.** Clean first-pass build (748 processes, 227s, 0 errors). No new facades to test, shape was validated across 30 files on the first attempt. + +**Lesson — multi-key preferencesView migration.** `engine.data.subscribe(itemA, itemB)` exists and returns a Swift tuple. When a Postbox `preferencesView(keys: [K1, K2])` call is inside a `combineLatest(...)` whose downstream closure accesses `.values[K1]` and `.values[K2]`, prefer the two-arg subscribe form (vs. two separate subscribes combined externally) — it preserves `combineLatest` arity exactly. Rewrite `.values[K1]?.get(T.self)` → `.0?.get(T.self)`, `.values[K2]?.get(T.self)` → `.1?.get(T.self)`. The closure parameter name stays (e.g., `preferences`) because the tuple destructure preserves the variable-name semantics at the call site. + +Net: 30 consumer files. No TelegramCore changes. CLAUDE.md facade-inventory table unchanged (no new facades). + +Plan / record: `memory/project_postbox_wave27_plan.md` (deleted post-wave). + +## Wave 26 outcome (2026-04-21) + +`resourceRangesStatus` + `removeCachedResources` facade additions + consumer sweep. Combines two independent small sweeps into one commit. + +**Two facades added:** +- `resourceRangesStatus(resource: EngineMediaResource) -> Signal, NoError>` wraps the single `(MediaResource) -> Signal, NoError>` overload. Takes `EngineMediaResource` (not `id:`) because Postbox's overload only accepts a resource, not an id — consumers pass `.resource` already. Facade unwraps via `_asResource()`. +- `removeCachedResources(ids: [EngineMediaResource.Id], force: Bool = false, notify: Bool = false) -> Signal` wraps the `([MediaResourceId], force:, notify:) -> Signal` overload. Maps ids internally. + +**`import RangeSet` added to `TelegramEngineResources.swift`.** The `RangeSet` return type caused a name collision with Swift stdlib's `RangeSet` (iOS 18+ only) until the local `RangeSet` module is imported. `TelegramCore/BUILD` already declared the dep at line 23 (`//submodules/Utils/RangeSet:RangeSet`), so no BUILD change needed. + +**4 Shape-A consumer sites migrated (3 files):** +- `PhotoResources/Sources/PhotoResources.swift` (1) +- `TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift` (1) +- `ChatListUI/Sources/ChatListSearchContainerNode.swift` (2) + +For `ChatListSearchContainerNode.swift:1398`, the caller uses a `Set` local — wave leaves the local as-is and maps at the call site via `resourceIds.map { EngineMediaResource.Id($0) }`. Migrating the local to `Set` is out of scope (module keeps `import Postbox` for unrelated reasons). + +**Build validation.** Clean build (563 processes, 265s, 0 errors) on the second attempt after adding `import RangeSet`. + +**Lesson — Swift-stdlib-vs-third-party-module name collisions.** When a facade signature references a type name that exists both in Swift stdlib (potentially availability-restricted) and in a third-party module, the compiler picks the stdlib one by default. Fix: import the third-party module explicitly. In this codebase, `RangeSet` is provided by `submodules/Utils/RangeSet:RangeSet`, and TelegramCore already depends on it. Use `import RangeSet` at the file top. + +Net: 3 consumer files + 1 TelegramCore file + CLAUDE.md. TelegramEngineResources.swift: +9 / -0 (including `import RangeSet`). + +Plan / record: (no plan doc this wave — mechanical sweep). + +## Wave 31 outcome (2026-04-23) + +Second build-verified `^import Postbox$` sweep on consumer modules since wave 6 (2026-04-19). Same methodology: speculative-drop + `--continueOnError` build loop with pattern-based preemptive restores. + +**Candidate set narrowing.** Initial candidate grep `grep -rl "^import Postbox$" submodules --include="*.swift"` returned **1184** files. 606 of those live in `submodules/TelegramCore/Sources/` — TelegramCore legitimately `import Postbox`; the TelegramCore files were accidentally included and had to be reverted via `git checkout -- submodules/TelegramCore/Sources/` before re-seeding the drop. Final consumer candidate set: **578** files. **Lesson for future sweep invocations: the candidate-set grep must filter out `submodules/TelegramCore/` as well as `submodules/Postbox/` / `submodules/TelegramApi/`.** Wave 6's methodology note at step 1 (line 37) already calls this out, but the TelegramCore carve-out is easy to miss because TelegramCore doesn't `@_exported import Postbox`, so from a pure re-exports perspective it's indistinguishable from a consumer. + +**9 build iterations to convergence** (plus 1 aborted first iteration for the TelegramCore scope error). Per-iteration failure counts: 18 → 2 → 9 → 12 → 1 → 1 → 3 → 1 → 4 → 0. Surfacing pattern was typical of a speculative-drop sweep: errors bubble one dependency-graph layer at a time. + +**Per-iteration symbol expansion.** The wave-6 preemptive-restore symbol list (CLAUDE.md's "Unused-import sweeps" guidance) needed extensions for this sweep: +- Iter 3 surfaced `CodableEntry`, `CachedMediaResourceRepresentation`, `CachedMediaRepresentationKeepDuration`. +- Iter 4 surfaced `PostboxViewKey`, `OrderedItemListView`, `UnreadMessageCountsItem`, `ChatListEntrySummaryComponents`, `PeerStoryStats`, `ItemCollectionId` (note: typealias `EngineItemCollectionId` exists but raw name still requires `import Postbox`), and broadened `\bMedia\b`, `\bMessage\b`, `\bPeer\b`. +- Iter 5 surfaced `FetchResourceSourceType` (same typealias caveat). +- Iter 6 surfaced `StoryId`. +- Iter 7 surfaced `ChatListIndex`. +- Iter 8 surfaced `PreferencesEntry` (typealias caveat), `PeerView`, `RenderedPeer`. +- Iter 9 surfaced `declareEncodable`. +- Iter 10 surfaced `ItemCollectionItemIndex`, `ValueBoxEncryptionParameters`, `fileSize`, plus a restore-script bug (see below). + +**Restore-script bug: `#if canImport(...)` blocks.** The naive restore inserter picks the last `^import ` line and appends `import Postbox` after it. If the last import sits inside an `#if canImport(AppCenter) ... #endif` preprocessor block, the restored `import Postbox` lands inside that block and is only active under that configuration. `AppDelegate.swift` in `submodules/TelegramUI/Sources/` hit this (original had `import Postbox` at line 7; drop + restore put it inside the `#if canImport(AppCenter)` block at line 51); the build failed in iter10 on `cannot find type 'Postbox' in scope` errors even though a literal `grep ^import Postbox$` matched. Fixed by manually moving the import out of the `#if` block. **Lesson for future restore-script work: insert the restored `import Postbox` BEFORE the first `#if` or `#endif` line, not after the last `import` line, to avoid preprocessor-scope traps.** + +**Results: 9 source-level surviving drops + 2 duplicate-import dedups.** Final diff: 11 files changed, +2 / -13. + +Surviving drops: +- `submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift` +- `submodules/AuthorizationUI/Sources/AuthorizationSequenceSplashController.swift` +- `submodules/DebugSettingsUI/Sources/DebugAccountsController.swift` +- `submodules/LegacyDataImport/Sources/LegacyPreferencesImport.swift` +- `submodules/MediaPlayer/Sources/ChunkMediaPlayerDirectFetchSourceImpl.swift` +- `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift` +- `submodules/TelegramUI/Sources/ChatLinkPreview.swift` +- `submodules/TelegramUI/Sources/ChatSearchResultsController.swift` +- `submodules/TelegramUI/Sources/MediaManager.swift` + +Duplicate-import dedups (files had two `^import Postbox$` lines; kept exactly one — unrelated-but-latent cleanup surfaced incidentally by the sweep): +- `submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift` (2 imports → 1) +- `submodules/TelegramUI/Sources/ChatHistoryListNode.swift` (2 imports → 1) + +**Spurious-diff cleanup step (new procedure, adopted this wave).** After convergence, `git diff --numstat` showed 564 modified files but only 9 were genuine drops. The other 553 were "1 addition + 1 deletion" — files where the original `import Postbox` at line X was deleted by the drop and re-inserted at line Y by the restore (different position because restore inserts after "last import line" regardless of original placement). These aren't semantic changes but do produce noisy diffs. Identified via `git diff --numstat | awk '$1 == 1 && $2 == 1 {print $3}'` and reverted via `xargs -I{} git checkout -- {}`. **Lesson: the wave-6 methodology should add a post-convergence "revert 1-add-1-del spurious diffs" step before committing. Alternative: improve the restore script to insert at the exact original line. Either way, the final diff should be limited to real semantic changes.** + +**No modules became fully Postbox-free this wave.** Each of the five containing modules still has other files importing Postbox (TelegramUI: 350 remaining, LegacyDataImport: 4, MediaPlayer: 9, AuthorizationUI: 2, DebugSettingsUI: 1). By this point most trivially-droppable imports have been drained; the remaining Postbox-importing files mostly carry real usage. **Re-run cadence lesson: yield per re-run is declining.** Wave 6 yielded 183 drops + 189 modules freed; wave 31 yielded 9 drops + 0 modules freed. Consider spacing future sweeps to every 4–6 facade waves rather than 2–3. + +**Wave 14 BUILD-dep sweep companion: 0 drops.** Ran the wave-14-style `find submodules -name BUILD | filter-by-no-source-import` check: **0 BUILD candidates**. The 191 BUILDs still listing `//submodules/Postbox` all have at least one Sources/*.swift that actually imports Postbox. One outlier (`submodules/SpotlightSupport/BUILD`) has zero source files but a non-trivial `deps = [...]` list including `//submodules/Postbox`; deliberately left alone (stale-BUILD-on-empty-module is a different class of cleanup and carries unknown side effects). + +Net: 11 files changed (9 + 2), +2 / -13 lines. Clean first-attempt verification build without `--continueOnError` (880 actions, 1354 action cache hits, 262s). + +Plan / record: (no plan doc this wave — mechanical sweep). + +## Wave 32 outcome (2026-04-24) + +`resourceStatus` residue sweep. One new facade overload (`status(id:resourceSize:)`) + 4 migrated sites across 2 consumer files. Commit `289fc908bc`. + +**Facade added** in `TelegramEngineResources.swift`: +- `status(id: EngineMediaResource.Id, resourceSize: Int64) -> Signal` wraps Postbox's `resourceStatus(MediaResourceId, resourceSize:)` overload. Body mirrors the existing `status(resource:)` facade, converting id via `MediaResourceId(id.stringRepresentation)` and mapping the result via `EngineMediaResource.FetchStatus.init`. + +**4 migrated sites (2 files):** +- `ChatListSearchContainerNode.swift:1059` — new `status(id:resourceSize:)` overload. Caller supplies `EngineMediaResource.Id(downloadResource.id)` directly (String initializer; `downloadResource.id: String`) — no raw `MediaResourceId(...)` wrap needed. Mirrors the pre-existing `EngineMediaResource.Id(downloadResource.id)` usage at line 1107. +- `ChatMessageInteractiveMediaNode.swift:1769` — existing `status(resource:)` facade (wave 3). +- `ChatMessageInteractiveMediaNode.swift:1799` — same. +- `ChatMessageInteractiveMediaNode.swift:1809` — existing `resourceRangesStatus(resource:)` facade (wave 26). + +**Local preserved deliberately.** `let postbox = context.account.postbox` at `ChatMessageInteractiveMediaNode.swift:1767` stays because line 1793 feeds `postbox` to `HLSVideoContent.minimizedHLSQualityPreloadData(postbox: Postbox, ...)` — that is a third-party-function boundary needing raw `Postbox`. Only the `resourceStatus`/`resourceRangesStatus` call sites within that scope migrate. + +**Case-pattern sharing.** `MediaResourceStatus` (raw Postbox) and `EngineMediaResource.FetchStatus` (engine wrapper) have identical case names (`.Fetching`, `.Paused`, `.Local`, `.Remote`). The inner `switch status` at 1770-1779 keeps its `MediaResourceStatus` return type annotation — input case matching works for the engine type, constructed `MediaResourceStatus` return values still compile (`MediaResourceStatus` is in scope via `import Postbox` on line 4). This is the wave-29/30 lesson in action: no enum-case edits required. + +**Inventory scope narrowing from memory's prediction.** The memory's `wave 32+ candidates` section predicted ~12 Shape-B/C sites in the residue sweep. Execution-time re-grep reclassified most of them: +- **Coupled to `accountManager.mediaBox.resourceStatus` siblings (6 sites in 3 files):** `ThemePreviewControllerNode:271+277`, `WallpaperGalleryItem:799+805+834+840`, `SettingsThemeWallpaperNode:284+285`. Each pair has an `accountManager`-sourced fallback whose return type is raw `Signal`. Migrating only the `account.postbox` branch breaks the shared sibling type at the `mapToSignal`/`combineLatest` merge point. Deferred until accountManager-side has an engine facade. +- **Shape-C init-param refactor (3 sites in 3 files):** `LegacyWebSearchGallery:248` (free function `legacyWebSearchItem(account: Account, ...)`), `NativeVideoContent:455` (init takes `postbox: Postbox`), `VerticalListContextResultsChatInputPanelItem:229` (item stores `account: Account`). Each needs an init-param change + caller threading — per-module mini-refactor, not wave-shape-G territory. +- **`approximateSynchronousValue` overload:** only call site (`SettingsThemeWallpaperNode:284`) is in the accountManager-coupled bucket above. Adding the facade now would land dead code. + +Effective wave scope: 4 sites (the uncoupled subset). Still worth committing as its own wave — closes the `resourceStatus` arc for every site where migration is currently unblocked. + +**Build validation.** Clean build (558 processes, 236s, 0 errors). No `--continueOnError` needed — first attempt green. + +**Lesson — siblings-define-scope in resource-status migrations.** When an assignment uses `A.resourceStatus(...)` in one branch and `B.resourceStatus(...)` in another (via `if`/`mapToSignal`/`combineLatest`), the branches' return types must match. If `A` has an engine facade but `B` does not (e.g., `accountManager.mediaBox` has no engine wrapper yet), neither branch is migratable in isolation — the whole group must wait. Pre-flight sibling-check for each `resourceStatus` hit: is the enclosing `statusSignal = ...` expression a single source or a multi-source merge? + +**Lesson — Shape-B/C classification requires read, not grep.** The memory's wave-32 candidate table classified sites by single-line grep ("`account.postbox.mediaBox.resourceStatus`"). That pattern matches both the fully-migratable `context.account.postbox.mediaBox.X` form (Shape-A via AccountContext) AND the `(local) account.postbox.mediaBox.X` Shape-C form (requires init-param refactor). Distinguishing requires reading 5-10 lines of context to find the `account` binding: field? local? init param? closure capture? Add this as a mandatory step in the per-site inventory for future residue waves. + +Plan / record: (no plan doc this wave — small residue sweep). + +--- + +## Wave 33 outcome (2026-04-24) + +`loadedPeerWithId` consumer sweep. 60 sites migrated across 37 consumer files. No new facades, no typealiases. Commit `16d017853a`. + +**Migration pattern** (per user's explicit direction): + +```swift +context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) +|> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } +} +``` + +This replaces `context.account.postbox.loadedPeerWithId(peerId)` while preserving signature shape. The `mapToSignal` wrapper is critical: Postbox's `loadedPeerWithId` returns `.never()` (signal never emits) when the peer is missing — it does NOT wait for loading. The engine-data equivalent `get(Peer.Peer(id:))` returns `Signal` (optional snapshot). Unwrapping with `.never()`-on-nil preserves original semantics exactly, while keeping the outer shape `Signal` non-optional so callers' closures don't have to cascade new optional handling. + +**Category distribution (per pre-flight Explore catalog, 60 sites):** + +| Category | Count | Body change | +|---|---|---| +| Cat-A (trivial) | 22 | Only EnginePeer-compatible members; type swap only. | +| Cat-B (concrete-type cast) | 25 | `peer as? TelegramUser/Group/Channel/SecretChat` → `if case let .user(user)` (etc.). | +| Cat-C (feeds Peer-typed API) | 13 | `peer._asPeer()` at call point (`makePeerInfoController`, `makeChatRecentActionsController`, `makeChatQrCodeScreen`, `FoundPeer.init`, `SendAsPeer.init`). | + +(Cat-B + Cat-C bumped slightly from Explore's catalog after in-edit reclassifications.) + +**Engine-access variations:** +- Most consumer modules use `context.engine.data.get(...)` on `AccountContext`. +- `ShareSearchContainerNode.swift` uses `context.engineData.get(...)` because `ShareControllerAccountContext` exposes `engineData: TelegramEngine.EngineData` but not a full `engine`. +- `CallStatusBarNode.swift` (has raw `account: Account` from switch case) constructs `TelegramEngine(account: account)` inline. +- `PresentationGroupCall.swift` uses `self.accountContext.engine.data` instead of the stored `self.account.postbox`. + +**TelegramCore internal sites (36) unchanged.** `Postbox.swift` (2 defs), `State/AccountViewTracker.swift`, `State/FetchChatList.swift`, `State/SynchronizePeerReadState.swift`, `Suggestions.swift`, and all `TelegramCore/Sources/TelegramEngine/` internal `_internal_*` helpers still call `postbox.loadedPeerWithId(...)` — they are the Postbox-facing layer. + +**Pre-flight efficiency.** An Explore subagent cataloged all 60 sites by category from a single prompt (one-line-per-site output). That catalog made the sweep straightforward: most files fell into identical patterns, enabling template-substitution Edits. Total context spent on discovery was small compared to doing 60 per-site full reads in the main thread. + +**Build validation.** First-pass clean build (47 actions, 70s) after sweep completion. Earlier pilot (2 sites, 20s) validated pattern before scaling to all 60. + +**Lessons:** + +- **`loadedPeerWithId` returns `.never()` on missing peer, not a pending Signal.** Old common misreading: treating it as a "wait-until-loaded" primitive. Actual Postbox source at `Postbox.swift:3925`: `if let peer = self.peerTable.get(id) { return .single(peer) } else { return .never() }`. Preserve this by wrapping `engine.data.get` in `mapToSignal` with the `.never()` fallback — don't replace with plain `|> compactMap { $0 }` (which would drop the signal entirely rather than completing immediately when peer exists). + +- **"Keep the signatures to help the typechecker" as a migration principle.** The user (2026-04-24) explicitly directed: keep call-site outer Signal signatures stable (`Signal` non-optional), even at the cost of a 6-line inline `mapToSignal` wrapper at each site. Rationale: 60 sites × optional-cascade body changes > 60 × 6-line wrapper. This is a general principle for sweeps — if the alternative is rewriting every body to handle optionals, prefer the signal-level wrapper to contain the change. + +- **Pre-flight cataloging via Explore subagent.** For sweeps with variable per-site body shapes (unlike facade-migration-with-identical-call-expression sweeps), a dispatch to `Explore` with a category-classification prompt collapses inventory cost. Explore's output is small (~60 one-line entries); avoids pulling 60 file fragments into the main thread's context. Required for wave shapes where inventory is non-uniform. + +- **Shape-C peer-fed-to-API pattern needs `_asPeer()` at call, not facade.** Because `makePeerInfoController(peer: Peer)` / `FoundPeer(peer: Peer, ...)` / `SendAsPeer(peer: Peer, ...)` / `makeChatQrCodeScreen(peer: Peer, ...)` all stay on raw `Peer` (they're AccountContext-protocol or TelegramCore struct-init APIs whose migration is its own multi-wave effort), the bridge is a single `._asPeer()` at the call. Don't try to also migrate those APIs in the sweep — blast radius too large. + +- **Engine-access varies by containing context.** Plain `context.engine.data` works for ~85% of sites; the remainder need `TelegramEngine(account: account)` construction or `engineData` protocol property. Build a per-site `context` type check into pre-flight for call-site categories where `AccountContext` isn't guaranteed. + +Plan / record: no plan doc this wave — user specified the migration pattern directly; the Explore catalog + commit message captured decisions. + +--- + +## Wave 34 outcome (2026-04-24) + +`FoundPeer.peer: Peer → EnginePeer`. Public field-type migration on the struct in `submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift`. Atomic 12-file commit `fdd5b93998`. ~135 insertions / ~134 deletions. + +**Migration shape.** The field-type change is necessarily atomic (half-migrated FoundPeer doesn't compile across consumers), so all edits land in one commit. `_internal_searchPeers` keeps `import Postbox` (still calls `postbox.transaction` etc.) and wraps raw peer values with `EnginePeer(peer)` at the FoundPeer constructor sites. `==` body changes from `lhs.peer.isEqual(rhs.peer)` to `lhs.peer == rhs.peer`. + +**Final scope (vs planned ~70 semantic edits → actual ~135 line insertions):** +- 5 `._asPeer()` bridge-drops at FoundPeer constructor sites (e.g., `FoundPeer(peer: peer._asPeer(), ...)` → `FoundPeer(peer: peer, ...)`) +- 22+ redundant `EnginePeer(peer.peer)` wrap drops (the field is now EnginePeer; `EnginePeer.init(_ peer: Peer)` doesn't accept an EnginePeer argument so the wrap fails to compile) +- 30+ Postbox-concrete-type downcasts (`peer.peer as? TelegramX` / `is TelegramX`) rewritten to `if case let .X(x) = peer.peer` enum-pattern form +- ~10 `._asPeer()` outflow bridges added where `peer.peer` flows into APIs that still take raw `Peer`: `ContactListPeer.peer(peer:)`, `canSendMessagesToPeer(_:)`, `EngineRenderedPeer(peer:)` legacy paths + +**Inventory undercounting — pattern.** Original Explore inventory pass missed 4 of 12 final consumer files. The grep `grep -rln "FoundPeer\b"` only catches files that name `FoundPeer` as a literal type. Files that USE `peer.peer` access on FoundPeer values without naming the type itself were invisible to that grep. The build verification pass surfaced them: + +| File | Surfaced by | Edits needed | +|---|---|---| +| `TelegramCore/Calls/GroupCalls.swift` | iter 1 | 2 internal FoundPeer constructors needed `EnginePeer(peer)` wraps | +| `ShareController/ShareSearchContainerNode.swift` | iter 2 | 4 errors: 2 C2 downcasts + 2 outflow-bridge needs | +| `ContactListUI/ContactsSearchContainerNode.swift` | iter 3 | 7 errors: nested `if !(peer is X)` rewrite + multiple downcasts/outflows | +| `PeerInfoUI/ChannelMembersSearchContainerNode.swift` | iter 4 | 6 errors across 2 near-identical loop blocks | +| `ChatListUI/ChatListSearchListPaneNode.swift` (extra site) | iter 5 | 1 missed C2 site at line 3723 (in `.globalPeer(foundPeer, …)` enum case body, far from the other ChatListUI edits) | + +5 build iterations total before clean (each iteration: edit → re-build, ~50–60s incremental). First-pass would have needed a much wider pre-flight grep — see lessons. + +**Lessons:** + +- **Inventory grep must include the access pattern, not just the type name.** For a field-type migration, ALL of: + - `(peer:` constructors + - `.peer.` reads (verify `` type is ``, not RenderedPeer/SendAsPeer/etc.) + - `.peer as?` / `.peer is` downcasts + - `(.peer)` arg passes (where `` may take the old protocol) + + Use `for x in Y` binding-tracing to determine if `` is the migrated type. The wave-34 pre-flight ran the first three but not the fourth (outflow-arg sites), and partially missed the second (because the Explore agent classified by literal `FoundPeer` token rather than by `peer.peer` semantics in context). + +- **`if !(peer is A || peer is B)` rewrite uses `switch case A, B: break / default: ...`.** When the original Postbox code uses a negated disjunction of type-checks, the cleanest enum-pattern equivalent is a `switch` with combined cases in one arm — not nested `if case`s. (Used in ChatListSearchListPaneNode:1024 and ContactsSearchContainerNode:502/544.) + +- **Inner `peer` shadowing.** Many `else if let peer = peer.peer as? TelegramChannel` Postbox patterns shadow the loop variable. The enum-pattern rewrite renames the inner binding to `channel` to avoid double-shadowing the EnginePeer outer loop var. Block-internal references to `.info` etc. then move from `peer.info` to `channel.info`. + +- **Build iteration = inventory completion.** When the inventory undercounting becomes apparent (build surfaces 5+ unexpected sites), don't abandon — iterate. Each build is fast (~50s incremental) and each error is actionable (`error: cast from EnginePeer to unrelated type X always fails` → C2 rewrite; `argument type EnginePeer does not conform to expected type Peer` → outflow bridge). The inventory grows by file, fix-then-rebuild converges in 5 iterations even when ~30% of sites were missed up front. + +- **Bridge sites generated by this wave point to next-ring migration targets.** The ~10 `._asPeer()` outflow bridges land at `ContactListPeer.peer(peer:)`, `canSendMessagesToPeer(_:)`, and `EngineRenderedPeer(peer:)` (legacy raw-Peer constructor in some paths — e.g., `EngineRenderedPeer(peer: foundPeer.peer)` doesn't need a bridge in newer EnginePeer-aware paths but does where the local var was already raw-Peer-extracted). These three signatures are the obvious wave-35+ candidates for the next ring of migration. + +**Plan / record:** `docs/superpowers/plans/2026-04-24-foundpeer-engine-peer-migration.md`. Spec: `docs/superpowers/specs/2026-04-24-foundpeer-engine-peer-migration-design.md`. + +--- + +## Wave 35 outcome (2026-04-24) + +`SendAsPeer.peer: Peer → EnginePeer`. Public field-type migration on the struct in `submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift`. Atomic 7-file commit `583c8b1f7c`. 22 insertions / 26 deletions. + +**Migration shape.** Same atomic-field-type pattern as wave 34 but scoped to a smaller consumer surface. The `_internal_*SendAsAvailablePeers` functions keep `import Postbox` and wrap raw peer values with `EnginePeer(peer)` at the 4 SendAsPeer constructor sites. Manual `==` body dropped in favor of synthesized Equatable (`EnginePeer: Equatable`, `Int32?` and `Bool` already Equatable). + +**Final scope (vs planned ~15 semantic edits → actual 22/26 line diff):** +- 3 `._asPeer()` bridge-drops at SendAsPeer constructor sites (ChatControllerLoadDisplayNode:772, ChatTextInputPanelComponent:848, StoryItemSetContainerViewSendMessage:249) +- 7 redundant `EnginePeer(peer.peer)` / `EnginePeer($0.peer)` / `EnginePeer(value.peer)` wrap drops across ChatSendAsPeerListContextItem (4 sites), ChatTextInputPanelNode (1), StoryItemSetContainerViewSendMessage (1), StoryItemSetContainerComponent (1) +- 1 `peer.peer as? TelegramChannel` downcast rewritten to `if case let .channel(channel) = peer.peer` (ChatSendAsPeerListContextItem:73) with `peer.info → channel.info` rename in the shadowed scope +- 2 `EnginePeer(channel)` wraps added where raw `TelegramChannel` is constructed into `SendAsPeer(peer: ...)` (ChatControllerLoadDisplayNode:805, 823) +- 1 signal-chain simplification: `(sendAsPeer?.peer).flatMap(EnginePeer.init)` → `sendAsPeer?.peer` at StoryItemSetContainerViewSendMessage:4080 +- 1 signal-chain simplification: `.map({ EnginePeer($0.peer) })` → `.map({ $0.peer })` at StoryItemSetContainerViewSendMessage:4081 + +**Inventory undercount = 1 site (vs wave 34's 5).** The pre-flight Explore catalog missed `StoryItemSetContainerComponent.swift:3069` (`currentPeer: EnginePeer(value.peer)` → `value.peer`). The implementer caught it during the edit phase before the build, so no iteration was needed. The wave-34 explicit pattern grep (including `.peer as?`/`is`/outflow-args/`EnginePeer(.peer)`/`._asPeer()`) dramatically reduced undercounting — 1/7 sites missed (~14%) vs wave 34's 4/12 (~33%). + +**First-pass clean build.** No errors surfaced by the Bazel build at all. 461 total actions, 196.583s elapsed, `INFO: Build completed successfully`. Contrast with wave 34's 5 build-iterations-to-converge. + +**Lessons:** + +- **Wave 34's explicit-pattern pre-flight inventory works.** For future Peer-typed-API waves, the minimum grep pattern set is: `\b` literal token, `\.\s+(as\?|is)\s+Telegram`, `EnginePeer\(\w+\.\)`, `\(\.` for known outflow APIs, and `\._asPeer\(\)` (to catch bridge-drop opportunities). Wave 35 used this full pattern set and hit ~14% undercount vs wave 34's ~33%. + +- **Smaller target + validated pattern = faster wave.** Wave 35 went from spec-commit (`72d4384af0`) to outcome-commit in a single session with one clean build, versus wave 34's multi-iteration convergence. When the wave is a replay of a just-validated pattern on a smaller surface, expect minimal iteration. + +- **Inner-peer shadowing rename works.** The wave-34 lesson about renaming `peer` → `channel` in `if case let .channel(channel) = peer.peer` applied cleanly. Single instance this wave (ChatSendAsPeerListContextItem:73) — no issues. + +- **Name collisions remain a scope hazard.** Pre-flight identified `sendAsPeers: [EnginePeer]` (LiveStreamSettingsScreen, ShareWithPeersScreen) and `availableSendAsPeers: [EnginePeer]` (ChatSendStarsScreen) as name-only collisions — different type, same identifier. Confirmed these stayed untouched and out of scope. Future Peer-typed-API waves should continue the name-collision disambiguation pass. + +- **Bridge sites generated by this wave — zero new outflow bridges.** Unlike wave 34 (which added ~10 `._asPeer()` outflow bridges pointing to `ContactListPeer.peer(peer:)` / `canSendMessagesToPeer(_:)` / `EngineRenderedPeer(peer:)` as next-ring targets), wave 35 added no outflow bridges. All consumer-side `.peer` flows either stayed as `.peer.id` accesses (PeerId unchanged) or were simplifications of existing `EnginePeer(.peer)` wraps. Net: no new next-ring targets surfaced from wave 35. + +**Plan / record:** `docs/superpowers/plans/2026-04-24-sendaspeer-engine-peer-migration.md`. Spec: `docs/superpowers/specs/2026-04-24-sendaspeer-engine-peer-migration-design.md`. + +--- + +## Wave 36 outcome (2026-04-24) + +`ContactListPeer.peer(peer: Peer, isGlobal:, participantCount:) → peer: EnginePeer`. Enum-case payload migration on the public type in `submodules/AccountContext/Sources/ContactSelectionController.swift`. Atomic 15-file commit `069a060de1`. 57 insertions / 59 deletions. + +**Migration shape.** Same atomic-payload-type pattern as wave 34/35 but wider: 15 consumer files vs wave 35's 7, vs wave 34's 12. Beyond the payload change, the cascading `ContactListPeer.indexName` return type changed from `PeerIndexNameRepresentation` to `EnginePeer.IndexName` — an unexpected discovery during plan-writing that dropped 2 additional `EnginePeer.IndexName(...)` wraps at ContactListNode:517. + +**Final scope (vs planned 8 files / ~41 semantic edits → actual 15 files / 57/59 diff):** + +- **Definition (1 file):** `AccountContext/ContactSelectionController.swift` — case payload type, indexName return type, `==` operator body (`lhsPeer.isEqual(rhsPeer)` → `lhsPeer == rhsPeer`). +- **20 `._asPeer()` outflow bridge-drops** across ContactListNode (12), ContactsSearchContainerNode (3), ContactMultiselectionController (2), ContactMultiselectionControllerNode (1), ContactSelectionControllerNode (2). `replace_all=true` on `._asPeer(), isGlobal:` was the unifying substring. +- **20+ `EnginePeer(peer)` inflow wrap-drops** at destructure sites across ContactListNode (4), ContactsController (1), ContactsSearchContainerNode (4), ContactMultiselectionController (4), ContactMultiselectionControllerNode (1), ContactSelectionController (2), PeerSelectionControllerNode (3), SharedAccountContext (2). +- **2 `EnginePeer.IndexName(...)` wrap-drops** at the sort-comparator at ContactListNode:517 (enabled by the cascading return-type change). +- **8 Postbox-concrete cast rewrites** to EnginePeer case patterns across ContactListNode:182-186/1968 (4 sites, including the 3-branch user/group/channel cast-chain), CallController:524/542 (the intermediate `let peer = EnginePeer(peer)` lines became redundant after migration), StoryItemSetContainerViewSendMessage:2041/2074, DeviceContactInfoController:1419, ChatSendAudioMessageContextPreview:89, ChatControllerOpenAttachmentMenu:557/610/1746/1788 (4 identical sites, `replace_all` on the full line). +- **2 `._asPeer()` outflow bridges ADDED** at ContactMultiselectionController:386/403 where the destructured peer flows into `peerTokenTitle(peer: Peer)` (out-of-scope callee; future-wave bridge target). + +**Inventory undercount = 7 files / ~20 sites (vs wave 35's 1 site).** Much higher miss rate than wave 35 — ~46% by file count. Root cause: the pre-flight grep for ContactListPeer destructures used literal `\(peer, _, _\)` binding; binding names varied in practice (`contact`, `lhsPeer`, `rhsPeer`, `contactPeer`, `id`). Files missed: + +1. `DeviceContactInfoController.swift:1418/1419` — `case let .peer(contact, _, _)` + `contact as? TelegramUser` +2. `CallController.swift:523/541` — `case let .peer(peer, _, _)` + redundant `let peer = EnginePeer(peer)` pattern +3. `ChatSendAudioMessageContextPreview.swift:88/89` — `case let .peer(contact, _, _)` + `contact as? TelegramUser` +4. `PeerSelectionControllerNode.swift:901-903/1590-1592` — 2 destructures with `EnginePeer(peer)` inflow wraps +5. `StoryItemSetContainerViewSendMessage.swift:2040-2041/2073-2074` — 2 `contact as? TelegramUser` casts +6. `ChatControllerOpenAttachmentMenu.swift:556-1787` — 4 `contact as? TelegramUser` casts +7. `SharedAccountContext.swift:3295-3302` — `case let .peer(peer, _, _)` + 2 `EnginePeer(peer)` inflow wraps + +**Six build iterations to converge** vs wave 35's single first-pass-clean. Iterations 1-6 surfaced errors in batches of 2-8 errors; each was a mechanical fix (drop wrap, rewrite cast, add `._asPeer()` bridge for outflow to out-of-scope `peerTokenTitle`). Final iteration (#6) clean. + +**Lessons:** + +- **Pre-flight grep must use `\(\w+, _, _\)` not `\(peer, _, _\)` for enum-payload destructures.** Swift destructure patterns bind the payload to any legal identifier; the variable name is not semantic. Future Peer-typed-enum-payload waves should use `case let \.\((\w+),` (or similar wildcard binding) and then per-destructure scan the next ~15 lines for ` as\?`/` is`/`EnginePeer\(\)` / outflow-arg patterns. + +- **"No-edit consumer" claims need stricter verification.** Wave 36's "verify-only" list included ChatSendAudioMessageContextPreview because the initial inventory found only `[ContactListPeer]` at collection level. The deeper scan missed a `case let .peer(contact, _, _)` + `contact as? TelegramUser` pattern inside the file's `update(...)` method. For future waves, "no-edit" claims should run the wildcard-binding destructure grep described above, not just a construction-site grep. + +- **Outflow-to-out-of-scope-API bridges may need addition during the wave.** ContactMultiselectionController:386/403 needed `._asPeer()` bridges added where none existed pre-migration — the pre-migration code passed raw `Peer` to `peerTokenTitle(peer: Peer)` because the destructured peer was raw Peer. Post-migration, the destructured peer is EnginePeer, so a bridge is required. Future waves with same-scope outflow to not-yet-migrated Peer-typed APIs should pre-flight expect to add bridges. + +- **Cascading computed-property return type migration** (here: `ContactListPeer.indexName` from `PeerIndexNameRepresentation` to `EnginePeer.IndexName`) is a legitimate scope expansion when the enum's properties leak Postbox-typed values. Wave 36 caught this during plan-writing, not execution — a successful plan-review win. Future waves should grep the enum's definition file for computed properties returning Postbox-defined types. + +- **Build-iteration convergence is acceptable** when the wave's surface is large and pre-flight undercount is non-trivial. The cost of 6 build iterations (~5-20 minutes each in the Telegram-iOS build) is real but manageable. The alternative — exhaustive pre-flight to achieve first-pass-clean — is more expensive in plan-writing tokens and controller wall time. For waves expected to have >5 file touches, plan should explicitly budget for 3-5 build iterations. + +- **Ratchet effect confirmed.** Wave 36 was predominantly bridge-removal (20 outflow + 20 inflow + 2 IndexName) with only 2 bridge additions. Matches the expected ratchet behavior: earlier waves 33/34/35 added bridges at Peer/EnginePeer boundaries precisely so wave 36 could drop them atomically. The 2 new bridges added (ContactMultiselectionController:386/403 → peerTokenTitle) become next-wave drop candidates once `peerTokenTitle(peer: Peer)` migrates. + +**Plan / record:** `docs/superpowers/plans/2026-04-24-contactlistpeer-engine-peer-migration.md`. Spec: `docs/superpowers/specs/2026-04-24-contactlistpeer-engine-peer-migration-design.md`. + +--- + +## Wave 37 outcome (2026-04-24) + +`peerTokenTitle(peer: Peer → EnginePeer)`. Private free function in `submodules/TelegramUI/Sources/ContactMultiselectionController.swift`. Atomic single-file commit `734ab44dd2`. 7 insertions / 7 deletions. + +**Migration shape.** Ring-2 cleanup of bridges wave 36 installed. The function body was already round-tripping: callers unwrapped `EnginePeer → Peer` with `._asPeer()` only for the body to re-wrap with `EnginePeer(peer).displayTitle(...)`. Flipping the parameter to `EnginePeer` drops both the 5 call-site bridges and the 1 body-side wrap. Zero semantic change — verified by code-quality reviewer: `displayTitle(strings:displayOrder:)` is defined only on `EnginePeer` (LocalizedPeerData/PeerTitle.swift:29), and `EnginePeer.Id` is a typealias for `PeerId` so `peer.id.isReplies` and `peer.id == accountPeerId` resolve identically. + +**Final scope:** +- 1 signature change (L21): `peer: Peer` → `peer: EnginePeer` +- 1 body simplification (L27): `EnginePeer(peer).displayTitle(...)` → `peer.displayTitle(...)` +- 5 `._asPeer()` bridge-drops at call sites (L171, L201, L386, L403, L748) + +**Pre-flight inventory was exact.** Zero undercount: the pre-flight grep enumerated exactly 6 call-site matches (1 definition + 5 bridges), and the implementer's 3 Edit operations (one unique + two `replace_all=true`) hit all 5 bridge sites on first pass. No scope creep, no out-of-scope edits. + +**First-pass-clean build.** 946 total actions, 259.250s elapsed, `INFO: Build completed successfully`, 0 errors. As a reset wave after wave 36's 6-iteration convergence, this was the expected outcome for a single-file mechanical change targeting a private function. + +**Subagent-driven execution.** First wave executed via `superpowers:subagent-driven-development` with a consolidated implementer dispatch (Tasks 1–3 bundled since all three were grep + mechanical Edit on one file). Two-stage review (spec + code quality) passed cleanly. Controller directly handled build, commit, and log/memory updates. + +**Lessons:** + +- **Bundling mechanical plan-tasks is valid for same-file micro-waves.** When a plan's tasks are all Edit operations on a single file with no inter-task branches, consolidating into one implementer dispatch preserves the review structure (spec + quality still gated) while avoiding per-task subagent overhead. For larger waves with multi-file surface or branching logic, keep per-task dispatch. + +- **Private-function ring-2 cleanups are first-pass-clean candidates.** The ratchet behavior observed in waves 33–36 — prior waves add bridges at next-ring boundaries — creates trivially-closable cleanup waves whenever the next-ring callee is itself a private or small-surface function. Wave 37's target (private free function, 5 bridge sites, all in-file) hit the ceiling of what this shape can yield. + +- **Post-wave 36 bridge inventory reduced by 5.** ContactMultiselectionController:171/201/386/403/748 are now Peer-free at the `peerTokenTitle` arg; the file still retains `import Postbox` for other APIs (L459's `SelectedPeer.peer(peer: Peer, ...)` feed), so file-level Postbox-free remains a later-wave target. + +**Plan / record:** `docs/superpowers/plans/2026-04-24-peertokentitle-engine-peer-migration.md`. Spec: `docs/superpowers/specs/2026-04-24-peertokentitle-engine-peer-migration-design.md`. + +--- + +## Wave 38 outcome (2026-04-24) + +`canSendMessagesToPeer(_ peer: Peer → EnginePeer, ignoreDefault:)`. Public utility function in `submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift`. Atomic multi-file commit `45729bad1c`. 12 files changed, 25 insertions / 24 deletions. + +**Migration shape.** Mixed-direction ring-2 cleanup: 18 Shape-A bridge drops (`._asPeer()` at call sites where caller already holds `EnginePeer`) plus 5 Shape-C bridge adds (`EnginePeer(peer)` wraps where caller holds raw Postbox `Peer`). Function body preserved by inserting a single `let peer = peer._asPeer()` shadow as the first body line — the four `peer as? TelegramUser/TelegramGroup/TelegramSecretChat/TelegramChannel` branches remain unchanged and the file still `import Postbox`. + +**Final scope:** +- 1 signature change (CanSendMessagesToPeer.swift:9): `peer: Peer` → `peer: EnginePeer` +- 1 body insertion (CanSendMessagesToPeer.swift:10): `let peer = peer._asPeer()` +- 18 Shape-A drops across 7 files: ContactsSearchContainerNode (3), ChatImageGalleryItem (1), UniversalVideoGalleryItem (1), StoryItemSetContainerViewSendMessage (1), ShareSearchContainerNode (3), ChatListSearchListPaneNode (6), ChatListNode (3) +- 5 Shape-C adds across 5 files: LegacyAttachmentMenu, ChatInterfaceInputContexts, ShareSearchContainerNode, ShareController, ChatPresentationInterfaceState +- ShareSearchContainerNode has mixed shape (3 drops + 1 add) + +**Pre-flight inventory was exact.** 24 total grep matches = 1 definition + 23 call sites; 18 Shape-A + 5 Shape-C. Post-migration grep: 0 `_asPeer()` residue, 5 `EnginePeer(` wraps, 24 total matches. No scope creep. No WIP-drift adjustments needed. + +**First-pass-clean build.** 568 total actions, 233.471s elapsed, `INFO: Build completed successfully`, 0 compilation errors. This contradicts the memory's pre-wave prediction of 3–5 build iterations per wave-36 lesson — the lesson remains valid for waves with cascading type-inference surfaces, but wave 38's surface (a utility function called only at points that already bridge `EnginePeer ↔ Peer` in both directions) had no such cascades. The Shape-A/C classification captured the full surface before code changes began. + +**Subagent-driven execution.** Executed via `superpowers:subagent-driven-development`. Implementer dispatch bundled plan Tasks 1–5 (grep + all 24 edits + post-edit grep) since all were mechanical and on known files. Two-stage review (spec + code quality) passed without review-loop iterations. Controller handled build, commit, and log/memory updates. + +**Lessons:** + +- **Bundled multi-file dispatch is valid when pre-flight classification is exact.** Wave 36's per-task-dispatch rule (from CLAUDE.md) was formulated for waves with expected 3–5 build iterations driven by type-inference cascades. Wave 38 demonstrates that a multi-file wave with an exact upfront Shape-A/C classification and a cascading-free surface (all call sites are leaf arguments to a `Bool`-returning utility) can use the bundled dispatch shape safely. Gate: (a) all edits are mechanical Edit operations; (b) pre-flight grep enumerates every call-site explicitly; (c) return type of the migrated API does not propagate into caller type inference. When these hold, bundled dispatch saves subagent overhead without sacrificing review coverage. + +- **First-pass-clean multi-file waves exist.** The memory's pre-wave prediction of "not first-pass-clean territory" was conservative and came from wave 36's 6-iteration experience. Wave 38's 12-file mechanical migration built clean on first attempt because the spec's explicit Shape-A/C classification enumerated all 23 sites, and the function's `Bool` return prevents caller-side type-inference cascades. Future multi-file waves should update expectations when (a) the classification is exact and (b) the return type does not propagate. + +- **Shape-C wraps remain valid additions.** Five sites had to add `EnginePeer(peer)` at the call because the enclosing scope still holds raw Postbox `Peer` (from `RenderedPeer.peers` lookups, `ChatPresentationInterfaceState.renderedPeer.peer`, or `as? TelegramChannel` casts). These adds are acceptable — the alternative is a per-scope refactor (RenderedPeer → EngineRenderedPeer cascades) that would expand blast radius. Future waves targeting `RenderedPeer` would drop these 5 wraps. + +**Plan / record:** `docs/superpowers/plans/2026-04-24-cansendmessagestopeer-engine-peer-migration.md`. Spec: `docs/superpowers/specs/2026-04-24-cansendmessagestopeer-engine-peer-migration-design.md`. + +--- + +## Wave 39 outcome (2026-04-24) + +`AccountContext.makePeerInfoController(... peer: Peer → EnginePeer ...)`. Public protocol method on `AccountContext` (`submodules/AccountContext/Sources/AccountContext.swift:1371`) and its `SharedAccountContextImpl` implementation (`submodules/TelegramUI/Sources/SharedAccountContext.swift:1937`). Atomic multi-file commit `5385abc9bd`. **52 files changed**, 80 insertions / 79 deletions. + +**Migration shape.** Body-shadow ring-2 cleanup at the largest scale yet attempted. The protocol + impl signatures change to `peer: EnginePeer`; the impl body adds `let peer = peer._asPeer()` as its first statement, preserving the private downstream `peerInfoControllerImpl(peer: Peer, ...)` and all of its Peer-typed helpers as out-of-scope. Mixed-direction at consumer sites: 58 Shape-A drops + 3 Shape-A-variant guard-statement drops + 12 Shape-C `EnginePeer(...)` wraps. Net **-49 bridges**. + +**Final scope:** +- 1 protocol-signature change (AccountContext.swift:1371) +- 1 impl-signature change + 1 body-shadow insertion (SharedAccountContext.swift:1937–1938) +- 3 Shape-A self-call drops in same file (SharedAccountContext.swift:3335, 3483, 4016) +- 3 Shape-A-variant guard-statement drops (SettingsSearchableItems.swift around 1020/1046/1080): `guard let peer = peer?._asPeer() else { return }` → `guard let peer = peer else { return }`. The call-site `peer:` argument line is unchanged; the upstream guard now binds `peer` as `EnginePeer` (the closure parameter from `engine.data.get(...)`) rather than re-shadowing as raw `Peer`. +- 12 Shape-C `EnginePeer(...)` wraps across 8 files: BlockedPeersController:270, ChannelMembersController:707, ChannelBlacklistController:381, ChatRecentActionsControllerNode:1011, PeerInfoScreen:4306, ChatControllerNavigationButtonAction (4 sites: 441/461/471/492), ChatControllerOpenPeer (2 sites: 218/359), ChatControllerLoadDisplayNode:4362 +- 55 additional Shape-A drops across 42 consumer files +- 4 incidental trailing-whitespace strips picked up by Edit tool (functionally identical) + +**Pre-flight inventory was exact.** Total grep matches at planning time: 75 (1 protocol decl + 1 impl + 73 consumer call sites) of which 58 had inline `peer: ._asPeer()` (Shape-A), 3 had upstream-guard `peer?._asPeer()` patterns (Shape-A-variant), and 12 had raw `peer: ` (Shape-C). Post-migration grep for `_asPeer()` at `makePeerInfoController` call sites: 0. Post-migration grep for `EnginePeer(` at the 12 Shape-C sites: all present. No scope creep, no Postbox/TelegramCore/TelegramApi edits. The implementer noted that `BlockedPeersController.swift:270` already had `peer: peer` (no `_asPeer()`) — this was correctly classified Shape-C in the spec, and the wrap was applied as planned. + +**Property-typing pre-verification matters.** Spec phase identified that `RenderedPeer.peer`/`chatMainPeer` (Postbox extension at PeerUtils.swift:512 and Postbox/RenderedPeer.swift:38) return raw `Peer?`, while `EngineRenderedPeer.peer`/`chatMainPeer` (TelegramCore/Peers/Peer.swift:623/627) return `EnginePeer?`. Six Shape-C sites depend on `RenderedPeer` (Postbox), so `EnginePeer(...)` wraps were correct. Sites that consume `EngineRenderedPeer` would need `peer: peer` only — not a concern here, but a checklist item for next-wave Shape-C planning. + +**First-pass-clean build.** 658 total actions, 210.520s elapsed, 1565 cache hits + 142 disk-cache + 444 worker, `INFO: Build completed successfully`, 0 errors. Strongest confirmation yet of the wave-38 lesson: **first-pass-clean is achievable for 50+ file waves when (a) the migrated API's return type is non-propagating** (`ViewController?` is an optional reference type, like wave-38's `Bool` — caller-side type inference does not branch on it), **(b) pre-flight Shape-A/A-variant/C classification is exact, and (c) the body-shadow boundary is preserved.** Pre-wave estimate budgeted 2–4 iterations on the assumption that a 50-file change at a popular API surface would surface destructure cascades; the actual outcome exceeded expectations. + +**Subagent-driven execution.** Executed via `superpowers:subagent-driven-development`. Implementer dispatch bundled plan Tasks 1–5 (signature + body-shadow + 3 self-call drops + 3 guard rewrites + 12 wraps + 55 Shape-A drops, 70+ Edit operations across 52 files). Two-stage review (spec + code quality) passed without re-review iterations. Spec reviewer verified all 73 sites, the body-shadow placement at line 1938, the guard-statement form change in SettingsSearchableItems, and the unchanged-out-of-scope confirmation for `peerInfoControllerImpl` at line 4434. Code quality reviewer noted only the 4 incidental trailing-whitespace strips as "minor — leave as-is, tiny improvement". Controller handled build, commit, and log/memory updates. + +**Lessons:** + +- **Shape-A-variant: drop the upstream `_asPeer()` rather than wrapping at the call site.** When a guard-statement immediately upstream of a `makePeerInfoController` call unwraps `EnginePeer? → Peer?` via `peer?._asPeer()`, prefer rewriting the guard to `guard let peer = peer else` (keeping the local as `EnginePeer`) over adding `EnginePeer(peer)` at the call. The variant: zero net change at the call line, -1 `_asPeer()` upstream. This pattern occurs whenever a closure parameter from `engine.data.get(...)` is opt-unwrapped immediately before a Peer-typed-API call. + +- **First-pass-clean ceiling is higher than wave-36 implied.** Wave 36's 6-iteration convergence (15 files, ContactListPeer.peer enum-payload migration) led to a memory rule "multi-file = budget 3–5 iterations". Wave 38 first-pass-clean (12 files, Bool return) suggested the rule doesn't apply universally. Wave 39 first-pass-clean (52 files, ViewController? return) extends this: even at 50+ files, the determinant is return-type propagation behavior + pre-flight classification accuracy, not file count. Updated heuristic: **budget for first-pass-clean when** (return-type is non-propagating) AND (Shape classification is exact); **budget for 3–5 iterations when** (return type is a generic container, struct with associated types, or enum that participates in caller-side inference). + +- **Bundled implementer dispatch scales to 70+ edits across 52 files.** The wave-38 lesson ("bundled multi-file dispatch is valid when pre-flight classification is exact") held at this scale. Two-stage review (spec then code-quality) over the aggregate output preserved review coverage. No per-task subagent dispatches were needed. + +- **Atomic-stage exclusion of pre-existing WIP is necessary at large diff sizes.** Wave 39's working tree contained an unrelated WIP file (`ChatMessageTransitionNode.swift` — function-signature/animation changes on `beginAnimation`) that was modified outside the session. The commit phase explicitly enumerated all 52 wave-39 files in the `git add` invocation rather than using `git add -u` or `git add .`. This is a permanent rule for this project: never use bulk-stage commands; always enumerate files when committing a wave. + +- **Ratchet expansion: 12 new Shape-C wraps for future waves.** The 12 `EnginePeer(...)` wraps installed at this wave become drop candidates for later waves migrating their upstream sources: `RenderedPeer → EngineRenderedPeer` (would drop 6 wraps at sites consuming `renderedPeer.peer`/`chatMainPeer`), `RenderedChannelParticipant.peer → EnginePeer` (would drop 2 at ChannelMembers/ChannelBlacklist), and others. Net economics still strongly favor the wave: -61 + 12 = -49 bridges. + +**Plan / record:** `docs/superpowers/plans/2026-04-24-makePeerInfoController-engine-peer-migration.md`. Spec: `docs/superpowers/specs/2026-04-24-makePeerInfoController-engine-peer-migration-design.md`. + +--- + +## Wave 40 outcome (2026-04-24) + +Commit: `d3c48379fe`. Bundle of `AccountContext.makeChatQrCodeScreen` + `makeChatRecentActionsController` peer `Peer → EnginePeer` — the trivial sibling follow-up to wave 39, completing the "Option 1 cluster" (`makePeerInfoController` family) from the wave-38 memory. 8 Swift files + plan doc / 11 edits. Pre-flight classification was already done in the wave-39 design doc's "Out of scope" section — no fresh pre-flight needed. + +**Classification:** +- 2 protocol decls (`AccountContext.swift:1401`, `:1461`) +- 2 impl decls + body-shadows (`SharedAccountContext.swift:2302`, `:2731`) +- 2 Shape-A-variant guard rewrites (`SettingsSearchableItems.swift:971`, `:989` — `guard let peer = peer?._asPeer()` → `guard let peer = peer`, keeping the local as `EnginePeer`) +- 3 Shape-A drops (`ContactsController.swift:478`, `ChannelAdminsController.swift:734`, `GroupStatsController.swift:915`) +- 2 Shape-C wraps (`PeerInfoScreen.swift:4623`, `PeerInfoScreenOpenChat.swift:115`) — both consume `data.peer: Peer?` from `PeerInfoScreenData`, so they're ratchet markers for a future `PeerInfoScreenData.peer Peer → EnginePeer` wave. + +Net −3 bridges (−5 `_asPeer()` drops, +2 `EnginePeer(...)` wraps). + +**Build outcome:** First-pass-clean for wave-40 files. The initial run failed due to pre-existing unrelated WIP in `ChatMessageTransitionNode.swift` (unterminated string literals in debug `print` statements the user was mid-editing); the wave-40 files produced zero diagnostics. After the user fixed the WIP, the subsequent build completed in 23.9s (mostly cache-warm) with zero errors. + +**Lessons:** + +- **Bundled sibling migration with shared pre-flight is cheap.** Wave 39's "Out of scope" section pre-classified all 7 of this wave's consumer sites. That planning overhead was already paid; wave 40 only needed to verify classifications still hold (one grep per site-group) and apply mechanical edits. Total time from plan-write to commit: ~30 minutes (dominated by the ~3-minute build). This validates a general pattern: when a wave defers siblings with an explicit "Out of scope" classification section, the follow-up wave is structurally trivial. + +- **Small-scale bundled implementer dispatch is still the right choice.** 11 edits across 8 files fits comfortably in one implementer call, matching the wave-38/39 lesson at smaller scale. No per-task dispatches were needed; two-stage review (spec then code-quality) took one round each. + +- **Pre-existing WIP in the working tree can cause module-scope build failures that mask the wave's own status.** When `ChatMessageTransitionNode.swift` had parse errors from an unrelated user WIP, the whole `TelegramUI` Swift module failed to parse before type-checking ran — so wave-40 files got syntax-verified but not type-verified. Diagnostic approach: grep the build output for error lines whose file path is NOT in the wave's file list; if the only errors are outside, the wave is likely clean, but complete type verification requires the unrelated WIP to be reverted or fixed. In this case the user fixed it; in future cases either ask the user, temporarily stash the WIP, or note the incomplete verification in the commit. + +**Plan / record:** `docs/superpowers/plans/2026-04-24-makeChatQrCodeScreen-recentActions-engine-peer-migration.md`. No separate spec — the pre-flight was reused from wave 39's spec ("Out of scope" section in `docs/superpowers/specs/2026-04-24-makePeerInfoController-engine-peer-migration-design.md`). + +--- + +## Wave 41 outcome (2026-04-24) + +Commit: `32573c9808`. `RenderedChannelParticipant.peer: Peer → EnginePeer` — a TelegramCore foundational-type field migration affecting 28 files (11 TelegramCore + 17 consumer) / 1124 insertions / 89 deletions. First foundational-type field migration since `FoundPeer.peer` / `SendAsPeer.peer` / `ContactListPeer.peer` (waves 34–36); differs in that `RenderedChannelParticipant` is a public TelegramCore struct with both TelegramCore-internal construction sites and a broad consumer surface, so the wave touched TelegramCore internals too, not just consumer modules. + +**Classification:** +- 1 struct field change (`ChannelParticipants.swift`): `peer: Peer → peer: EnginePeer` + init param + Equatable `lhs.peer.isEqual(rhs.peer) → lhs.peer == rhs.peer` +- 16 TelegramCore-internal constructor sites wrapped with `EnginePeer(peer)` across 9 files (RequestStartBot, AddPeerMember, ChannelAdminEventLogs [7 calls], ChannelBlacklist, ChannelMembers, ChannelOwnershipTransfer [2 calls], JoinChannel, PeerAdmins, Ranks) +- 1 TelegramCore-internal ADD-ASPEER (`SearchGroupMembers.swift:83` — pre-flight miss: `participants.map({ $0.peer })` consumed by outer `[Peer]`-returning closure) +- ~32 consumer DROP sites (removing `EnginePeer(participant.peer)` wraps or `._asPeer()` downgrades) +- 9 consumer CAST sites (`if let user = participant.peer as? TelegramUser, user.botInfo != nil` → `if case let .user(user) = participant.peer, user.botInfo != nil`) +- 3 consumer ADD-ASPEER (PeerInfoMembers:33 contains the `PeerInfoMember.peer: Peer` accessor ratchet; ChatRecentActionsHistoryTransition:675 + :2275 for `SimpleDictionary` subscript assignment) +- 7 consumer ADD-WRAP constructor sites (ChannelMembersSearchContainerNode + ChannelMembersSearchControllerNode legacy-group paths, ChatControllerAdminBanUsers:226) — ratchet markers for a future wave migrating `peerView.peers[id]` / `authors: [Peer]` upstream flows to EnginePeer +- 2 consumer `is TelegramChannel` → `if case .channel = participant.peer` rewrites (ChannelBlacklistController:370, :377) + +Net ~−13 bridges. Drops the 2 Shape-C wraps installed by wave 39 (ChannelMembersController:707, ChannelBlacklistController:381) as promised by the wave-39 ratchet plan. + +**Build outcome:** Three iterations. +1. First build surfaced `SearchGroupMembers.swift:83` — a TelegramCore-internal consumer of `$0.peer` on RCP that the plan's grep missed (grep only looked for `RenderedChannelParticipant(` constructors inside TelegramCore). +2. Second build surfaced `ShareWithPeersScreen.swift:777, 780` — an entire file missed in pre-flight because the initial grep was scoped to files already known to import RCP; this file gets RCP indirectly via `TemporaryCachedPeerDataManager.recent(...)`'s `updated:` callback. +3. Third build: clean (172 total actions). + +**Lessons:** + +- **Property-access migration has a broader grep surface than constructor migration.** Waves 34–36 migrated foundational types by grepping `(` constructors. That works because the consumer's use of the type is always preceded by a construction. Wave 41's RCP differs: RCP is constructed inside TelegramCore and handed to consumers as an opaque reference, so consumer usage looks like `participant.peer.X` (with any name, any subscript, any map closure). **Grep for the type's field-access patterns across the entire repo**, not just files that explicitly reference the type. For RCP specifically: `grep -rn 'RenderedChannelParticipant\|\.peer as?\|EnginePeer(.*\.peer)\|participant\.peer' submodules/` would have caught ShareWithPeersScreen.swift. File this as a **permanent pre-flight rule** for future property-access migrations. + +- **TelegramCore-internal consumer sites need the same inventory discipline as consumer-module sites.** The wave-41 plan enumerated 10 TelegramCore files for *construction* site updates but didn't grep for *consumption* sites — `SearchGroupMembers.swift:83` consumes `$0.peer` on RCP inside TelegramCore. Pre-flight for any foundational-type field migration must grep both construction AND consumption across the whole repo, not stratified by module. + +- **"Plan says N sites" numbers can be low; use `replace_all=true` defensively.** The plan listed 1 `EnginePeer(participant.peer)` site in ChannelBlacklistController.swift but the file actually had 5; listed 2 CAST sites in ChannelMembersSearchControllerNode.swift but the file had 4. The implementer correctly used `replace_all=true` on the unique-pattern greps, catching all sites. **Pre-flight should report "N site(s) per file at these lines" — if confidence is low, recommend replace_all directly.** + +- **Spec review caught `is TelegramChannel` that grep did not.** `participant.peer is TelegramChannel` does not match any of the usual migration-pattern greps (`EnginePeer(...)`, `as? TelegramUser`, `._asPeer()`). The spec reviewer caught it by reading the code. Would have been a build error anyway — the project compiles with `-warnings-as-errors` across 658/665 modules, so Swift's "'is' test always fails" warning promotes to an error — but catching it at spec time saves one build iteration. **Add `is Telegram(Channel|User|Group|SecretChat)` to the pre-flight token set** for any Peer → EnginePeer field migration so it's flagged at plan time rather than iteration time. + +- **First-pass-clean is NOT the right bar for foundational-type field migration.** Waves 37–40 achieved first-pass-clean. Wave 41 needed 3 iterations — and none of the three errors were the implementer's fault; all three were pre-flight misses. Heuristic update: **foundational-type field migrations (where the field is accessed via a generic property name like `.peer`) should budget 2–4 build iterations**, not first-pass-clean. The broader the grep surface, the more misses slip through. + +- **Ratchet economics of ADD-WRAP consumer constructors.** Wave 41 added 7 consumer `peer: EnginePeer(peer)` wraps at `RenderedChannelParticipant(...)` constructor sites in ChannelMembersSearch*Node and ChatControllerAdminBanUsers, where the local is still raw `Peer` from a legacy path (`peerView.peers[id]` / `authors: [Peer]`). These are ratchet markers: when the upstream legacy path migrates to EnginePeer, these wraps drop. The economics are net positive (−13 bridges) even counting the adds. + +**Plan:** `docs/superpowers/plans/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration.md`. +**Spec:** `docs/superpowers/specs/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration-design.md`. + +--- + +## Wave 42 outcome (2026-04-24) + +`PeerInfoScreenData.peer: Peer? → EnginePeer?` — a single-module (18-file) struct-field migration entirely contained within `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/`. `PeerInfoScreenData` is a consumer-module data class, not a public TelegramCore type, so the wave never touched TelegramCore — contrast wave 41 which had to update TelegramCore construction sites. + +**Classification:** +- 1 struct-field change + 1 init-param change in PeerInfoData.swift +- 4 construction-site edits adding `.flatMap(EnginePeer.init)` wraps at `peer:` arguments built from `peerView.peers[...]` (PeerInfoData.swift L1027, L1620, L1867, L2205); 1 construction site unchanged (L1100, `peer: nil`) +- ~25 consumer DROP sites (removing `EnginePeer(peer)` wraps where the local `peer` was bound from `data.peer` / `self.data?.peer`) +- ~25 consumer CAST sites (`if let user = data.peer as? TelegramUser, ...` → `if case let .user(user) = data.peer, ...`) +- ~5 consumer `is TelegramXxx` rewrites on data.peer-derived locals (PeerInfoScreen.swift L3981, L4133, L4192, L4194) +- ~10 consumer ADD-WRAP `?._asPeer()` helper bridges for internal helpers that stay `peer: Peer?` (PeerInfoScreenPerformButtonAction:62 calling `peerInfoIsChatMuted`; PeerInfoScreen:5399/5805 calling `self.headerNode.update(peer:...)`; PeerInfoScreen:5857 calling `peerInfoCanEdit`; etc.) plus one inline `_asPeer()` bridge inside `peerInfoIsCopyProtected` because `isCopyProtectionEnabled` is not exposed on `EnginePeer` + +Net ~−30 bridges. Drops the ~5 wave-40 `EnginePeer(peer)` wraps that were directly in scope (the plan initially overestimated which sites would drop — some `EnginePeer(peer)` calls in PeerInfoScreen.swift are inside closures whose `peer` parameter comes from a caller, not from `data.peer`, and those correctly survived). + +**Build outcome:** Two iterations. +1. First build surfaced `peerInfoIsCopyProtected` helper: `peer.isCopyProtectionEnabled` failed against `EnginePeer` because that property is `Peer`-protocol-only and is not forwarded by `EnginePeer`. Fix: inline `peer._asPeer().isCopyProtectionEnabled` bridge at the helper callsite (helper signature stays `Peer?`). +2. Second build: clean. + +**Lessons:** + +- **EnginePeer is not a strict superset of Peer's property surface.** `EnginePeer` forwards `id`, `displayTitle(...)`, `compactDisplayTitle`, `isPremium`, `smallProfileImage`, `largeProfileImage`, and other common properties, but Peer-only properties like `isCopyProtectionEnabled` are NOT forwarded. Migration-time surprise: a property access that worked on raw `Peer` can fail against `EnginePeer`. **Bridge pattern:** inline `peer._asPeer().isCopyProtectionEnabled` at the callsite, OR if the helper already takes `Peer?`, bridge upstream with `?._asPeer()` and keep the access unchanged. Pre-flight addition: grep migrated consumer surfaces for `.X` accesses where `X` is a Peer-protocol property not in EnginePeer's forwarding set. (Alternative: extend `EnginePeer` with more forwarders, but that's a larger TelegramCore change out of scope for a single wave.) + +- **Wave-42 was not first-pass-clean despite being a consumer-module wave.** Contrast wave 41: 3 iterations due to broader grep-surface misses. Wave 42: 2 iterations due to a single property-forwarding-gap miss. Heuristic update: **even consumer-module property-access migrations budget 2 iterations**, because Peer/EnginePeer interface parity is never verified end-to-end at pre-flight time. + +- **Plan-stated wrap-drop counts can be wrong in both directions.** The plan listed 15+ `EnginePeer(peer)` drop sites in PeerInfoScreen.swift citing specific lines (1331, 1339, 1346, 1561, 2353, ...) — but on inspection, lines 1331/1339/1346 are inside a `peer`-parameter closure (`openPeerContextAction = { ..., peer, ... in ... }`) where `peer` comes from the closure's caller, not from `data.peer`. Those wraps correctly stayed. The implementer correctly judged each binding context before deciding whether to drop. **Rule for plan writing:** when naming specific line numbers as drop-candidates, verify the `if let peer = ...` / closure-param binding in the actual file text, not just the `EnginePeer(peer)` grep result. + +- **Wave-41 lesson reconfirmed (`is Telegram*` rewrite):** all 5 `is TelegramChannel|User|Group|SecretChat` checks on `data.peer`-derived locals were caught at plan time (via the pre-flight grep token set including `is Telegram...`), not at build time. `-warnings-as-errors` would have caught them at build time but pre-flight catches save an iteration. + +- **Internal-helper bridge economics.** The PeerInfoScreen module has ~6 internal helpers (`canEditPeerInfo`, `peerInfoIsChatMuted`, `peerInfoHeaderButtons`, `peerInfoHeaderActionButtons`, `peerInfoCanEdit`, `availableActionsForMemberOfPeer`, `peerInfoIsCopyProtected`) that still take `peer: Peer?`. Wave 42 added ~10 `?._asPeer()` bridges at their callsites. A follow-up wave migrating those helper signatures would drop exactly those 10 bridges — small, well-scoped, strong candidate for wave 42.x. + +**Plan:** `docs/superpowers/plans/2026-04-24-peerinfoscreendata-peer-engine-peer-migration.md`. + +--- + +## Wave 43 outcome (2026-04-24) + +Migrated six PeerInfoScreen module helpers — `canEditPeerInfo`, `availableActionsForMemberOfPeer`, `peerInfoHeaderActionButtons`, `peerInfoHeaderButtons`, `peerInfoCanEdit`, `peerInfoIsChatMuted` (plus nested `isPeerMuted`) — from `peer: Peer?` to `peer: EnginePeer?`. All internal `as? TelegramUser|Channel|Group` and `is TelegramX` patterns inside the helper bodies (PeerInfoData.swift lines 2265–2670) rewritten to `case let .user/.channel/.legacyGroup` / `case .x = peer` enum patterns. No TelegramCore changes. No new typealiases. + +**Classification:** +- 6 helper signature migrations + 12 `as?` / `is` body rewrites in PeerInfoData.swift +- 7 DROPs of `._asPeer()` / `?._asPeer()` bridges installed by wave 42 (sites in PeerInfoScreenAvatarSetup, PeerInfoScreenPerformButtonAction ×3, PeerInfoScreenOpenMember, PeerInfoScreen:5857, PeerInfoProfileItems) +- 2 CONVERTs at PeerInfoScreen.swift:1905/1961 (`peer: group` / `peer: channel` extracted from `case let` → `peer: data.peer`, preserving the `group`/`channel` binding for body use) +- 10 ADD-WRAPs at call sites inside enclosing methods that still take raw `Peer?` (PeerInfoHeaderNode ×3, PeerInfoHeaderEditingContentNode ×5, PeerInfoEditingAvatarNode, PeerInfoEditingAvatarOverlayNode) — `.flatMap(EnginePeer.init)` for optional Peer, `EnginePeer(peer)` for non-optional (post-`guard let peer = peer`) Peer +- 2 ADD-WRAPs at raw-`Peer` member-item sites (PeerInfoScreenMemberItem, PeerInfoMembersPane) + +Net ~+5 wraps overall (7 drops, 12 adds) — less important than the headline: the helper signatures now live on the engine side. The 12 ADDs are all staged for drop in follow-up waves that migrate the four `update(peer: Peer?, ...)` methods (PeerInfoHeaderNode, PeerInfoHeaderEditingContentNode, PeerInfoEditingAvatarNode, PeerInfoEditingAvatarOverlayNode) and the two raw-`Peer` enclosingPeer storage fields (PeerInfoScreenMemberItem.enclosingPeer is actually `Peer?`, PeerInfoMembersPane's local `enclosingPeer: Peer`). + +**Build outcome:** 2 iterations. +1. First build surfaced one error: `PeerInfoScreenMemberItem.item.enclosingPeer` was declared `let enclosingPeer: Peer?` (optional) — the plan had prescribed `EnginePeer(item.enclosingPeer)` (non-optional form). Fix: `.flatMap(EnginePeer.init)` at the callsite. One-line correction. +2. Second build: clean. + +**Commit:** `d53e0d50f4c0e3e68c4e4c1ce255e76f43f56d4b` (12 files + plan). + +**Lessons:** + +- **Plan-declared property optionality can be wrong — verify `let X: T?` vs `let X: T` at plan-write time.** Wave 43's plan described `item.enclosingPeer` as non-optional `Peer` based on how it was used (`item.enclosingPeer as? TelegramChannel`, `item.enclosingPeer is TelegramChannel` compile against `Peer` non-optional OR optional protocol). The declaration was actually `Peer?`. The one-site plan-declaration mismatch cost one build iteration. **Rule for plan writing:** for every ADD-WRAP site, cite the declaration line, not just the usage. `grep -nE "(let|var)\s+\w+:\s*Peer\??" ` gives unambiguous optionality. + +- **Helper-signature migrations with zero Peer-only property access are first-iteration-clean except for optionality surprises.** Wave 42's lesson (EnginePeer property-forwarding gap) did not recur here because wave 43's helpers only did `as?` / `is` casts — pure enum-rewrite territory. Pre-flight verified each helper body used only `.id` and concrete-type accesses (`TelegramChannel.hasPermission(...)` etc., which stay on concrete types post-migration). The only iteration cost was the optionality mismatch above. **Heuristic: if a helper's body does not touch the `peer` parameter outside of `as?`/`is`/`.id`/`.id.isX`, budget 1 iteration + optionality-audit pass.** + +- **`case .x = peer` without binding is the correct `-warnings-as-errors` form when the branch body doesn't need the concrete value.** Wave 43's `peerInfoIsChatMuted` body at lines 2641/2643 uses `case .user = peer` and `case .legacyGroup = peer` (no inner binding) — because the branches only read `globalNotificationSettings.privateChats.enabled` / `.groupChats.enabled`, never the concrete `TelegramUser` / `TelegramGroup`. Had the migration emitted `case let .user(_) = peer` with `_`, or `case let .user(user) = peer` with `user` unused, `-warnings-as-errors` would fail. **Pre-flight rule: for each `peer is TelegramX` → `case .x = peer` rewrite, check whether the branch body accesses the concrete type. If not, use bare-case form; if yes, bind.** + +- **Bundled-dispatch subagent flow + two-stage review works cleanly for ≥10-site single-commit migrations.** Wave 43 dispatched one implementer (bundled Tasks 1–7), then spec reviewer, then code quality reviewer. All three completed in roughly 10 minutes of subagent time. Spec reviewer caught zero misses (implementer self-caught the optionality deviation in iteration 1). Code quality reviewer surfaced only minor observational notes (no Critical / Important issues). **Continues to confirm wave-39/40/41/42 finding: for ≤30-file / ≤50-edit wave shapes, the bundled flow is reliable; TaskCreate tracking adds no value at this size.** + +**Plan:** `docs/superpowers/plans/2026-04-24-peerinfoscreen-helpers-engine-peer-migration.md`. + +--- + +## Wave 44 outcome (2026-04-24) + +Migrated `RenderedChannelParticipant.peers: [PeerId: Peer]` to `[EnginePeer.Id: EnginePeer]`. Closes the wave-41 ratchet — the public RCP struct no longer leaks raw `Peer` types in any field (the surviving `presences: [PeerId: PeerPresence]` is out of scope; PeerPresence is a Postbox protocol requiring a separate migration). No new typealiases. No engine wrapper structs. + +**Classification:** +- 1 declaration: `ChannelParticipants.swift:11/14` field + init default +- 8 TelegramCore producer migrations (`ChannelMembers`, `RequestStartBot`, `ChannelOwnershipTransfer`, `JoinChannel`, `AddPeerMember`, `PeerAdmins`, `ChannelBlacklist`, `Ranks`) — each builds a local `var peers: [EnginePeer.Id: EnginePeer] = [:]` inside a `postbox.transaction` and wraps insertions with `EnginePeer(...)`. All producers pre-existed with the same structural pattern: 14 raw insertions became 14 wrapped insertions. +- 6 consumer DROPs of `EnginePeer(peer).displayTitle(...)` → `peer.displayTitle(...)` in `ChannelAdminsController`, `ChannelMembersSearchContainerNode` (×4), `ChannelBlacklistController` +- 5 consumer DROPs of `.mapValues({ $0._asPeer() })` transforms at RCP constructor call sites in `ChannelAdminsController`, `ChannelMembersSearchContainerNode` (×2), `ChannelMembersSearchControllerNode` (×2) +- 2 consumer ADDs of `._asPeer()` at `ChatRecentActionsHistoryTransition.swift` iteration sites — line 673 (`participant.peers`) and line 2273 (`new.peers` inside `participantSubscriptionExtended`), where the iterated `EnginePeer` is inserted into an outer `SimpleDictionary`. + +Net consumer-surface: **−10 bridges**. TelegramCore-internal: +~12 wraps inside files that already `import Postbox`. No new `import Postbox` in any consumer module; no Postbox-hygiene regression. + +**Build outcome:** 2 iterations. +1. First build surfaced one error: `cannot assign value of type 'EnginePeer' to subscript of type '(any Peer)?'` at `ChatRecentActionsHistoryTransition.swift:2273` — a second RCP.peers iteration site the plan's `participant\.peers` pre-flight grep missed because the local RCP binding was named `new` (from `case let .participantSubscriptionExtended(prev, new)`). Wider grep `\b(participant|new|prev|rcp|renderedParticipant)\.peers\b` confirmed only one additional site. Fix: identical `._asPeer()` unwrap as line 673. +2. Second build: clean. + +**Commit:** `ca69fa8cbb` (14 files, 38 insertions / 38 deletions). + +**Lessons:** + +- **Property-access field migrations need a name-agnostic grep surface, not a binding-name-prefixed one.** Wave 41's lesson said "repo-wide grep surface, not a stratified one." Wave 44 extends it: the binding name of the RCP varies by enum-case-destructure site. In `ChatRecentActionsHistoryTransition` specifically, one switch arm destructures the RCP enum case as `case let .participantSubscriptionExtended(prev, new):` — the local is `new`, not `participant`. Pre-flight grep token set for RCP.peers iteration sites should be `\b(participant|new|prev|rcp|renderedParticipant|channelParticipant)\.peers\b`. For the general case: **for any `T.` migration on a type passed opaquely through enum-case-destructures, enumerate the common destructure binding names (`prev`, `new`, `current`, `lhs`, `rhs`, etc.) in the plan's pre-flight grep.** + +- **Iteration-site-plus-insertion-into-foreign-container pattern is a canonical ADD-UNWRAP shape.** When a foundational type's dict field is migrated to engine types but a caller iterates the dict and inserts values into an outer container still typed with raw Postbox values (here `SimpleDictionary`), an `._asPeer()` unwrap is required at the insertion line. This pattern repeated twice in `ChatRecentActionsHistoryTransition` — both iteration sites have the same enclosing structure (build a raw-Peer `SimpleDictionary` → iterate RCP.peers → insert). **Future migrations on dict-like fields: grep for `for (_, X) in .peers { [X.id] = X }` patterns and anticipate the unwrap.** + +- **Producer-side migration is low-risk for field-type-parameter changes on local transaction-built dicts.** All 8 TelegramCore producers had the identical structural pattern (`var peers: [PeerId: Peer] = [:]` → insertions → pass to RCP constructor). Mechanical `EnginePeer(...)` wrap at each insertion with no side effects. No chain-migration concerns because every producer builds locally. **Shape heuristic: if producers build their contribution dict locally via `transaction.getPeer` calls, field-type migration is mechanical and fits a bundled-dispatch wave well.** + +- **Declaration-level `[PeerId: Peer] = [:]` init default shifts transparently.** The init-default literal `[:]` works as either `[PeerId: Peer]` or `[EnginePeer.Id: EnginePeer]`; 12 RCP construction sites with no `peers:` arg required zero changes. **Shape heuristic: default-valued Postbox-typed parameters are free on migration when the default literal is the empty collection form.** + +- **Consumer `.mapValues({ $0._asPeer() })` transforms on engine-typed source dicts become no-op drops on the target field migration.** Wave 44 dropped 5 such transforms cleanly because each source dict was already `[EnginePeer.Id: EnginePeer]` (verified at plan time by reading 20-line spans around each site). **Shape heuristic: when a consumer-side constructor call site uses `peers: X.mapValues({ $0._asPeer() })`, it signals the source is already engine-typed and the target field-type migration would drop the unwrap transform entirely.** + +- **Wave-41 lesson reconfirmed: foundational-type field migrations budget 2–3 iterations, not first-pass-clean.** Wave 44 hit 2 iterations because of the enum-destructure-binding-name grep miss; a more exhaustive pre-flight grep would have caught it as a first-pass-clean. + +**Plan:** `docs/superpowers/plans/2026-04-24-rcp-peers-engine-migration.md`. + +--- + +## Wave 45 outcome (2026-04-24) + +Migrated the four `update(..., peer: Peer?, ...)` methods across the PeerInfoHeader node hierarchy — `PeerInfoHeaderNode.update`, `PeerInfoHeaderEditingContentNode.update`, `PeerInfoEditingAvatarNode.update`, `PeerInfoEditingAvatarOverlayNode.update` — from raw `Peer?` to `EnginePeer?`. Consumer-surface ratchet that drops ~15 wave-43-era ADD-WRAP bridges. Single-module wave (all four files live in `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/`). No new typealiases. No engine wrapper structs. The stored `PeerInfoHeaderNode.peer: Peer?` field stays raw (intentional scope cap) and is bridged with `peer?._asPeer()` at assignment. + +**Classification:** +- 4 signature changes: `peer: Peer?` → `peer: EnginePeer?` at PHN:496, PHECN:52, PEAN:61, PEAON:63. +- 15 DROP wraps across the four files: + - PHN body: 7 drops at :524 (listContainerNode.peer), :548 (peerInfoHeaderActionButtons), :549 (peerInfoHeaderButtons), :571 (backgroundCoverSubject = .peer), :1218 (displayTitle), :1964 (listContainerNode.update), :2361 (peerInfoIsChatMuted). + - PHECN body: 5 drops at :59, :88, :93, :159, :162 (all `canEditPeerInfo` wraps). + - PEAN body: 2 drops at :66 (canEditPeerInfo) and :88 (avatarNode.setPeer). + - PEAON body: 1 drop at :85 (canEditPeerInfo). +- 5 enum-case rewrites in PHN: `peer as? TelegramChannel` / `peer as? TelegramUser` → `case let .channel(x) = peer` / `case let .user(x) = peer` at :622, :1225, :1238, :1273, :2354. +- 7 enum-case rewrites in PHECN: `peer as? TelegramUser/Group/Channel` at :73, :77, :86 (no-bind), :91 (no-bind), :107 (inner rebind), :154. +- 1 enum-case rewrite in PEAN: `peer as? TelegramChannel` at :93. +- 1 enum-case rewrite in PEAON: `peer as? TelegramChannel` at :74. +- 4 `_asPeer()` bridges ADDED at Peer-only-property sites (wave-42 lesson pattern): + - PEAN:159 `PeerReference(peer._asPeer())` — PeerReference.init takes raw Peer. + - PEAN:166 `peer._asPeer().isCopyProtectionEnabled` — property not forwarded on EnginePeer. + - PHECN:115 `(peer?._asPeer() as? TelegramUser)?.lastName` — inline expression form kept; chose bridge over multi-line `case let` expansion. + - PHN:521 `self.peer = peer?._asPeer()` — stored raw Peer? field stays raw. +- 1 ADD wrap at PHN:363 (`itemsUpdated` closure) — closure reads stored raw `self.peer` and forwards to the migrated `PEAN.update`. +- 1 ADD bridge at PHN:1815 for `self.avatarListNode.update(..., peer: peer?._asPeer(), ...)` — `PeerInfoAvatarListNode.update(size:avatarSize:isExpanded:peer:...)` still takes raw `Peer?` (plan pre-flight missed this site). Candidate for wave 46. +- 2 external call sites in `PeerInfoScreen.swift` at :5399 and :5805 reshuffled: `peer: self.data?.savedMessagesPeer ?? self.data?.peer?._asPeer()` → `peer: self.data?.savedMessagesPeer.flatMap(EnginePeer.init) ?? self.data?.peer`. Net-zero at these sites (swap `?._asPeer()` bridge for `.flatMap(EnginePeer.init)` wrap). + +Net consumer-surface: **−10 bridges** (15 drops − 4 internal `_asPeer()` bridges − 1 internal closure ADD-WRAP at :363 − 1 :1815 bridge the plan missed; the two external call sites are net-zero). No new `import Postbox` in any consumer module; no Postbox-hygiene regression. + +**Build outcome:** 1 iteration (first-pass-clean). + +**Commit:** `6b7a23867c` (5 files, 41 insertions / 41 deletions). + +**Lessons:** + +- **Plan pre-flight grep for call sites must include internal sibling-method callers, not just the target method's direct callers.** The plan enumerated the three internal call sites of PEAN.update / PHECN.update / PEAON.update inside PeerInfoHeaderNode.update's body (:633, :1816, :1817) and the `itemsUpdated` closure (:363), plus the two external call sites (:5399, :5805). It MISSED the `self.avatarListNode.update(..., peer:...)` call at PHN:1815 — a different target node's update method (`PeerInfoAvatarListNode.update(size:avatarSize:isExpanded:peer:...)`) that happens to live on the line immediately before `self.editingContentNode.avatarNode.update(peer: peer, ...)` at :1816. Pre-flight grep token `\.update\(.+peer:` over the four files would have caught it. Fix for future pre-flight grep: for each signature migration, grep ALL `update` methods called with the migrated param and verify each target's signature — not just the siblings being migrated. + +- **Wave-43 binding-rule continues to hold at scale.** All 13 `as? TelegramUser/Group/Channel` rewrites in this wave obeyed the bind-when-used / no-bind-when-unused rule (PHECN:86 and :91 used `case .legacyGroup = peer` / `case .channel = peer` no-bind form because the branch body only appends field keys; all 11 others bind because the branch uses the concrete value). Zero `-warnings-as-errors` unused-binding risks surfaced during review. **Rule confirmed as durable.** + +- **EnginePeer forwarding audit is worth a pre-flight pass for multi-property-accessing methods.** PHN's `update` method body accesses ~15 EnginePeer-forwarded properties (id, isFake, isScam, isPremium, isVerified, debugDisplayTitle, displayTitle, addressName, effectiveProfileColor, emojiStatus, verificationIconFileId, profileImageRepresentations, etc.) plus 2 concrete-class properties requiring enum bindings (`.phone` on TelegramUser; `.info` on TelegramChannel). Without a pre-flight forwarding audit against `submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift:485-560` (the EnginePeer property-forwarding extension), iteration surprises are likely. This wave's audit at plan time caught `isCopyProtectionEnabled` as the only non-forwarded property in PHN's body — matching the single bridge in PHN. **Rule: for multi-access methods, read the EnginePeer property-forwarding extension in full at plan time; enumerate every `peer.X` site and mark forwarded vs not.** + +- **Case-pattern-against-optional is idiomatic and compiles cleanly.** All rewrites use `if case let .user(user) = peer` (or sibling case forms) against `EnginePeer?` — Swift extracts through Optional implicitly. Precedent: `PeerInfoScreenSettingsActions.swift:200` `if case let .user(user) = self.data?.peer, let phoneNumber = user.phone`. Build did not complain at any rewrite site. **Conservative fallback `if let peer, case let .x(y) = peer` is NOT needed; drop from future plans' "fallback" sections.** + +- **Bundling net-zero external-call-site migrations with net-negative internal drops is fine for ratchet-focused waves.** The two PHN.update call sites in PeerInfoScreen.swift swap one bridge form for another (`?._asPeer()` → `.flatMap(EnginePeer.init)`) — literally net-zero on wrap count. The wave accepted this because (a) the internal drops (~15) dominate, (b) migrating `savedMessagesPeer` field to remove the external wrap would expand scope to a different file's public struct field, and (c) the wave-goal was "ratchet wave-43 ADDs", which is accomplished purely via internal drops. **Heuristic: if external call sites are net-zero and the internal scope is net-negative, bundle them; don't let the external net-zero block the ratchet.** + +**Plan:** `docs/superpowers/plans/2026-04-24-peerinfoheader-update-bundle-engine-peer.md`. + +--- + +## Wave 46 outcome (2026-04-25) + +Migrated the PeerInfoScreen-local avatar chain: `PeerInfoAvatarListNode.update(peer:)` together with `PeerInfoAvatarTransformContainerNode.update(peer:)` / `.updateStoryView(peer:)` and the private `Params.peer` stored field — raw `Peer?` → `EnginePeer?` across 4 files. Ratchet wave: drops the wave-45 ADD at `PeerInfoHeaderNode.swift:1815` plus a pre-existing external bridge at `PeerInfoScreen.swift:2574`, collapses one internal wave-45 wrap inside PIATCN's `setPeer` body, and adds 2 `_asPeer()` bridges for Peer-only surfaces (`PeerReference.init` and `.isCopyProtectionEnabled`). Net: −1 bridge. The PeerInfoScreen-local `PeerInfoAvatarListNode` class shadows a same-named submodule class — the wave targets the local (PeerInfoScreen/Sources) file only; the submodule is untouched. No new typealiases. No engine wrapper structs. No `import Postbox` change. + +**Classification:** +- 4 signature changes: `peer: Peer?` → `peer: EnginePeer?` on `PeerInfoAvatarListNode.update(peer:)` and its `arguments` tuple, plus `PeerInfoAvatarTransformContainerNode.update(peer:)`, `.updateStoryView(peer:)`, and the private `Params.peer` stored field. +- 2 DROPs at call sites (ratchet-value): + - `PeerInfoHeaderNode.swift:1815` — `peer: peer?._asPeer()` → `peer: peer` (the wave-45 ADD flagged for ratchet). + - `PeerInfoScreen.swift:2574` — `peer: peer?._asPeer()` → `peer: peer` (pre-existing external bridge; direct caller of `updateStoryView`). +- 1 internal WRAP collapsed inside PIATCN's `setPeer(...)` body: `peer: EnginePeer(peer)` → `peer: peer` (type flowed once upstream migrated). +- 2 ADDs inside PIATCN body for Peer-only surface: + - PIATCN ~:404 `PeerReference(peer._asPeer())` — `PeerReference.init` takes raw `Peer`, not `EnginePeer`. + - PIATCN ~:406 `peer._asPeer().isCopyProtectionEnabled` — property defined on the `Peer` protocol (`PeerUtils.swift:236`), not forwarded on EnginePeer (same finding as wave 45 for PEAN:166). +- 2 `as? TelegramChannel` → `case let .channel(...) = peer` rewrites inside PIATCN (both `update` and `updateStoryView` bodies test `channel.isForumOrMonoForum`; the `let channel` binding is retained because the body uses the concrete value). + +Net consumer-surface: **−1 bridge** (2 drops + 1 wrap collapse − 2 adds). External API: no change. + +**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel build 29.587s; only PeerInfoScreen + TelegramUI recompiled. + +**Commit:** `5ca99da5a7` (4 files, 13 insertions / 13 deletions). + +**Lessons:** + +- **Shadowing-class-name pre-flight disambiguation.** `PeerInfoAvatarListNode` is defined in two places: `submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift` (the submodule) AND `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift` (the PeerInfoScreen-local class that shadows it). Wave 46's pre-flight nearly targeted the wrong file when the plan inspected the signature by class name alone. Mitigation for future waves: when the target method's signature has distinctive parameter names (here `avatarSize:isForum:threadId:threadInfo:`), grep against those distinctive tokens alongside the class name to disambiguate — and when the class name appears in multiple files, resolve the module path before writing the plan. + +- **Chain-audit extends via sibling internal methods (`updateStoryView` pattern).** The initial audit targeted only the `update(peer:)` chain. But `PeerInfoAvatarTransformContainerNode.update`'s tail calls `self.updateStoryView(peer:)` — which also takes raw `Peer?`. Expanding the wave to include `updateStoryView` was a single-file additional edit and additionally dropped the `PeerInfoScreen.swift:2574` external bridge (a direct caller of `updateStoryView`, not `update`). Pattern for future chain waves: grep the target implementation body for `self\.\w+\(.+peer:` to find internal sibling methods whose signature migration naturally bundles with the primary chain. + +- **First-pass-clean extends to chain migrations when both forwarding audit and scope audit are done at plan time.** Wave 46 is the 4th consecutive wave (42/43/44/45/46 — wave 44 needed 2 iterations) hitting 0-to-1-iteration convergence. The 1-iteration-clean pattern is reproducible when (a) the `_asPeer()` bridge sites are identified at plan time by cross-referencing the body against the EnginePeer property-forwarding extension AND `PeerReference`'s init overloads, and (b) enum-case conversions are pre-written in the plan with bindless-vs-binding form pre-classified. Both inputs were present in the wave 46 plan. + +- **Chain-bundling heuristic validated at 3 methods / 4 files.** Migrating a narrow call chain (`PeerInfoAvatarListNode.update` → `PIATCN.update` → `PIATCN.updateStoryView`) + all external call sites in a single commit is the right granularity: the internal `peer` variables flow transparently once each downstream signature migrates, and the external call-site greps (2 sites total) complete the ratchet. Net: clean atomic commit, no bridging churn remains at the boundary. For future chain waves, bundle the chain methods plus external callers in one commit; don't split by method. + +**Plan:** `docs/superpowers/plans/2026-04-25-peerinfo-avatar-chain-engine-peer.md`. + +--- + +## Wave 47 outcome (2026-04-25) + +Migrated the stored `PeerInfoHeaderNode.peer` field from `Peer?` to `EnginePeer?`. Single-file wave; field is `private`, so no external API change. 4 edits / 1 file, all inside `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift`. + +**Edits:** +- Line 92: `private var peer: Peer?` → `private var peer: EnginePeer?` +- Line 363 (`itemsUpdated` closure): `peer: EnginePeer(peer)` → `peer: peer` (drops bridge; `peer` is captured from `strongSelf.peer`). +- Line 521 (assignment in `update`): `self.peer = peer?._asPeer()` → `self.peer = peer`. +- Line 2054 (ProfileLevelInfoScreen push closure): `peer: EnginePeer(peer),` → `peer: peer,`. + +PHN:426 (`peer.profileImageRepresentations.isEmpty`) compiles unchanged because `profileImageRepresentations` is forwarded by `EnginePeer` (`Peer.swift:485`). + +Net consumer-surface: **−3 internal bridges**. + +**Build outcome:** 2 iterations. +- Iteration 1 failed at PHN:363 — pre-flight grep used `self\.peer\b` and missed the `strongSelf.peer` capture inside the `itemsUpdated` closure body. The memory file's wave-47 candidate notes had explicitly flagged PHN:363; the omission was in the executor's grep, not the plan's identification. +- Iteration 2 clean. Full-project Bazel build 28.727s; only PeerInfoScreen + TelegramUI recompiled. + +**Commit:** `d7b7536440` (4 insertions / 4 deletions in PHN.swift; plan file added). + +**Lessons:** + +- **Pre-flight grep for stored-field migrations must include closure-capture aliases (`strongSelf`, `self_`, etc.), not just `self.`.** Wave 47's first-iteration failure was 100% avoidable: the same field is referenced via `strongSelf.peer` inside an `itemsUpdated` closure, and the bare `self\.peer\b` grep missed it. For future stored-field migrations, the canonical grep pattern is `(self|strongSelf|[a-zA-Z_]*[Ss]elf)\.\b`. The convention `strongSelf` appears throughout the PeerInfo codebase whenever closures use `[weak self]`. +- **Memory-stored candidate notes can pre-list the bridge sites.** The memory file `project_postbox_refactor_next_wave.md` had explicitly named PHN:363 (`itemsUpdated` closure read), PHN:521 (stored assignment), and PHN:2054 (ProfileLevelInfoScreen push wrap) before the wave started. Reading those during plan-write would have caught the iteration-1 miss. Treat the wave-N+1 candidate notes in memory as a load-bearing input to the plan, not just narrative. +- **Single-file `private` stored-field migrations are the cleanest possible wave shape.** No external API surface, no cross-module recompilation, blast radius bounded to the same file's other methods. When a wave reaches this shape, it is a near-zero-risk drop. Future stored-field-migration waves should be explicitly classified during planning as "single-file private" / "cross-file private" / "public-surface" to set the iteration budget. + +**Plan:** `docs/superpowers/plans/2026-04-25-phn-peer-stored-field-engine-peer.md`. + +--- + +## Wave 48 outcome (2026-04-25) + +Migrated `PeerInfoScreenData.savedMessagesPeer: Peer? → EnginePeer?`. Cross-file struct-field migration contained within the PeerInfoScreen module; the field has no external consumer (`grep -rEn "(\w+\??)\.savedMessagesPeer\b"` matches only inside the PeerInfoScreen sources). 5 edits across 2 files. + +**Edits:** +- `PeerInfoData.swift:388` — field decl `Peer?` → `EnginePeer?`. +- `PeerInfoData.swift:444` — init param `Peer?` → `EnginePeer?`. +- `PeerInfoData.swift:1622` — drop `?._asPeer()` bridge. The source local `let savedMessagesPeer: Signal` (PID:1313) already produces `EnginePeer?`; the bridge was an artificial demotion. +- `PeerInfoScreen.swift:5399` and `:5805` — drop `.flatMap(EnginePeer.init)` bridge in `headerNode.update(... peer: ...)` call. The `peer` parameter of `headerNode.update` has been `EnginePeer?` since wave 45, and the `??` coalescing operand `self.data?.peer` has been `EnginePeer?` since wave 42; once the field migrates, both ends of the expression are `EnginePeer?` and the `flatMap` bridge falls out. + +The other 4 init kwarg sites (PID:1029, :1102, :1869, :2207) all pass `nil` and require no change. + +**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel build 29.858s; only PeerInfoScreen + TelegramUI recompiled. + +**Commit:** `1e4c2eea33` (5 insertions / 5 deletions; plan file added). + +**Lessons:** + +- **Internal-storage demotion → external re-promotion** is a high-yield ratchet pattern. The field had a `?._asPeer()` demotion at the storage site, then `.flatMap(EnginePeer.init)` re-promotions at every read. Migrating the field type drops both ends in one wave — the underlying signal pipeline never needed the `Peer?` form at all. Pattern to look for in future PSD-class migrations: any field whose initialization expression names `_asPeer()` strongly indicates that the field's source data is already `EnginePeer?`. Grep candidate fields' init expressions for `_asPeer()` to surface these high-leverage migrations. + +- **Containment audit must distinguish field declarations on different types.** The grep `grep -rln "savedMessagesPeer"` returned 6 modules outside PeerInfoScreen. All matched references were independent declarations on unrelated types (TelegramEngineMessages, ChatListUI nodes, ChatControllerContentData). The narrower regex `(\w+\??)\.savedMessagesPeer\b` filtered to actual field-access patterns and confirmed no external consumer. For future struct-field migrations, prefer the access-pattern regex over plain text grep; common field names will hit unrelated declarations and inflate apparent blast radius. + +- **`replace_all=true` is correct for verbatim-duplicated call sites.** PIS:5399 and :5805 are byte-identical `headerNode.update(...)` calls with the same `peer:` argument. Single-Edit-with-replace_all replaced both atomically. No collisions because the `headerNode.update` argument list with `flatMap(EnginePeer.init)` is sufficiently long to be unique. + +- **Wave-shape G' (sibling-of-wave-42 ratchet) revalidated.** Wave 42 migrated `PeerInfoScreenData.peer: Peer? → EnginePeer?`. Wave 48 follows the same pattern on a sibling field. Future waves on `chatPeer`, `linkedDiscussionPeer`, `linkedMonoforumPeer` will not all be this clean: `chatPeer` has 5 `as? TelegramX` checks downstream + cross-method propagation into ClearPeerHistory; `linkedMonoforumPeer` has an `as? TelegramChannel` check at PIPI:1197. Field-by-field selection should consider downstream consumer shape, not just declaration site. + +**Plan:** `docs/superpowers/plans/2026-04-25-peerinfoscreendata-savedmessagespeer-engine-peer.md`. + +--- + +## Wave 49 outcome (2026-04-25) + +Bundled migration: `PeerInfoScreenData.linkedDiscussionPeer` + `.linkedMonoforumPeer`, both `Peer? → EnginePeer?`. Cross-file struct-field migration over 2 files. Bundled because both fields share parallel local-source patterns (raw `peerView.peers[id]` dict lookup) and the same single consumer file (`PeerInfoProfileItems.swift`). The bundle is justified: the source-of-truth init blocks compute `discussionPeer` and `monoforumPeer` as a sibling pair at PID:1836–1843 and again at PID:2131–2138; migrating one without the other would leave a half-Peer-half-EnginePeer init block. + +**Edits:** + +`PeerInfoData.swift` (12 edits via `replace_all=true` on the parallel pair): +- Lines 396–397 — field decls Peer? → EnginePeer?. +- Lines 453–454 — init params Peer? → EnginePeer?. +- Lines 1836+2131 — `var discussionPeer: EnginePeer?` (parallel pair). +- Lines 1838+2133 — `discussionPeer = EnginePeer(peer)` (lift raw Peer at boundary). +- Lines 1841+2136 — `var monoforumPeer: EnginePeer?` (parallel pair). +- Lines 1843+2138 — `monoforumPeer = peerView.peers[linkedMonoforumId].flatMap(EnginePeer.init)` (lift Peer? at boundary). + +`PeerInfoProfileItems.swift` (3 edits): +- :1102 — `EnginePeer(peer).displayTitle(...)` → `peer.displayTitle(...)`. +- :1197 — `if let monoforumPeer = data.linkedMonoforumPeer as? TelegramChannel` → `if case let .channel(monoforumPeer) = data.linkedMonoforumPeer`. The `case .channel` payload is `TelegramChannel`, so the downstream `monoforumPeer.sendPaidMessageStars` access at :1198 continues to compile. +- :1409 — `EnginePeer(linkedDiscussionPeer).displayTitle(...)` → `linkedDiscussionPeer.displayTitle(...)`. + +**Net bridge accounting:** +- ADDs (4): boundary lifts at PID:1838, :1843, :2133, :2138. These lift the Postbox-typed `peerView.peers[...]` dict-lookup result to the engine type at the data-flow boundary — the canonical Postbox→Engine position. Mirrors wave 42's `peer.flatMap(EnginePeer.init)` lift at PID:1620. +- DROPs (2): displayTitle `EnginePeer(...)` wraps at PIPI:1102 and :1409. +- Plus 1 idiom cleanup (PIPI:1197 — `as?` cast → enum-case pattern); no text saving but better Swift idiom. + +The +4/−2 net text-bridge count is acceptable here because the ADDs are not "internal bridges" — they're the canonical Postbox→Engine boundary that any well-typed engine field requires. Wave-tracking should distinguish "boundary lifts" (correct, permanent) from "internal bridges" (incorrect, ratchet target). + +**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel build 29.479s; only PeerInfoScreen + TelegramUI recompiled. Pre-flight EnginePeer-property forwarding audit (`addressName` at Peer.swift:461; `displayTitle` is an EnginePeer instance method; `case .channel` binds `TelegramChannel`; `TelegramChannel.sendPaidMessageStars` exists at SyncCore_TelegramChannel.swift:215) all verified at plan time and proved correct. + +**Commit:** `79698e4513` (15 edits across 2 files; plan added). + +**Lessons:** + +- **Bundle waves around source-of-truth coherence, not just consumer overlap.** Wave 49's bundling was justified primarily by the *source* side: both fields are computed in the same init block as a sibling pair (PID:1836–1843, :2131–2138). Migrating only one would leave the init block in a half-typed state and force an artificial bridge. Future bundling decisions should weight this "shared source-of-truth init" factor at least as heavily as "shared consumer file." + +- **Boundary-lift bridges are not "internal bridges" — wave-tracking should distinguish.** Wave-49's +4 ADDs are at the Postbox↔Engine boundary (raw `peerView.peers[...]` Postbox-typed dict → engine-typed field). They are the *correct* place for the lift; they will not ratchet away in a future wave. Internal bridges (e.g. PID:1622's `_asPeer()` in wave 48, or `EnginePeer(...)` wraps at consumer-side display calls) are the actual ratchet targets. Future wave outcome reports should categorize ADDs as "boundary lift" vs "internal bridge" to avoid confusion in net-bridge accounting. + +- **Parallel-pair `replace_all=true` works for multi-line blocks when the parallel pair is byte-identical.** PID:1836–1843 and PID:2131–2138 are byte-identical 9-line blocks. One Edit-with-replace_all applied the type+wrap rewrite to both. Pre-flight verification: `diff <(sed -n '1836,1843p' file) <(sed -n '2131,2138p' file)` returned empty before the edit. Pattern generalizes to any "compute the same locals in two adjacent init paths" code structure (a common shape in PeerInfoData.swift's signal pipelines). + +- **Whitespace nuance with `replace_all` multi-line edits.** The blank separator between the two `var X: Peer?` blocks had trailing spaces (Swift code with whitespace-trimming editor settings). The new_string used a clean blank line; the resulting file has a plain `\n` separator instead of ` \n`. Cosmetic only — no compile impact — but worth noting for future plans: when the indentation-sensitive pre-text uses trailing whitespace, mirror it exactly in the new_string to avoid inadvertent normalization. + +- **`case .channel` pattern at field-binding compiles cleanly against `EnginePeer?` (re-confirmed wave 45 lesson).** No `if let ... case let .channel = ...` two-step needed; single-step `if case let .channel(monoforumPeer) = data.linkedMonoforumPeer` works directly. + +**Plan:** `docs/superpowers/plans/2026-04-25-peerinfoscreendata-linked-peers-engine-peer.md`. + +--- + +## Wave 50 outcome (2026-04-25) + +`enclosingPeer: Peer? → EnginePeer?` migration across the PeerInfo members chain. 19 edits across 3 files (`PeerInfoScreenMemberItem.swift`, `PeerInfoMembersPane.swift`, `PeerInfoProfileItems.swift`). Cross-file private struct-field migration with stored-form ratchet (wave-47 taxonomy: "cross-file private"). Closes the wave-48-pattern internal-demotion-and-external-re-promotion ratchet at PIMP:354–363, where `engine.data.subscribe(...)` produced an `EnginePeer?` that was demoted to `Peer?` for storage and then re-promoted at every consumer. + +**Edits:** + +`PeerInfoScreenMemberItem.swift` (7 edits): +- :23 — stored field `let enclosingPeer: Peer? → EnginePeer?`. +- :34 — init param `Peer? → EnginePeer?`. +- :152, :154 — `as? TelegramChannel` / `as? TelegramGroup` → `case let .channel(channel)` / `case let .legacyGroup(group)`. +- :178 — `peer: item.enclosingPeer.flatMap(EnginePeer.init)` → `peer: item.enclosingPeer` (auto-promotes to `EnginePeer?`). +- :181, :187 — `is TelegramChannel` → `case .channel = ...` (wave-41 always-false-warning fix). + +`PeerInfoMembersPane.swift` (11 edits): +- :92, :271, :442 — three func sigs `enclosingPeer: Peer → EnginePeer`. +- :113, :115 — `as? TelegramChannel` / `as? TelegramGroup` → `case let .channel(channel)` / `case let .legacyGroup(group)`. +- :139 — `peer: EnginePeer(enclosingPeer)` → `peer: enclosingPeer` (drop wrap, auto-promotes). +- :142, :148 — `is TelegramChannel` → `case .channel = ...`. +- :293 — stored field `private var enclosingPeer: Peer? → EnginePeer?`. +- :361 — `strongSelf.enclosingPeer = enclosingPeer._asPeer()` → `strongSelf.enclosingPeer = enclosingPeer`. +- :363 — `strongSelf.updateState(enclosingPeer: enclosingPeer._asPeer(), ...)` → `..., enclosingPeer: enclosingPeer, ...`. + +`PeerInfoProfileItems.swift` (1 edit): +- :852 — `enclosingPeer: peer._asPeer()` → `enclosingPeer: peer` (boundary lift; `peer` is already `EnginePeer` from the closure scope's `data.peer` post-wave-42). + +**Net bridge accounting:** +- DROPs (5): 2× `_asPeer()` demotion (PIMP:361, :363), 1× `EnginePeer(...)` wrap (PIMP:139), 1× `flatMap(EnginePeer.init)` (PSMI:178), 1× boundary `_asPeer()` lift (PSPB:852). +- ADDs (0): no new bridges. Pattern conversions (`as?` → `case let`, `is` → `case`) are not bridges; they're idiom shifts mandated by the EnginePeer enum representation. +- Pass-through call sites at PIMP:275, :276, :437, :438, :451, :485 needed no edits — types flow transparently through stored field, local var, and func params after the signature changes. + +**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel build 29.290s; only PeerInfoScreen + TelegramUI recompiled. Continues the wave 42/45/46/48/49 first-pass-clean streak (6 of last 8 waves first-pass-clean; wave 44 was 2 iterations, wave 47 was 2 iterations). + +**Commit:** `a1b77bcf74` (19 edits across 3 files). + +**Lessons:** + +- **The wave-48 internal-demotion ratchet pattern is reliably first-pass-clean.** When the source signal already produces the engine type and is being demoted at storage, migrating the storage to the engine type is a 1-iteration target if the consumer's only Peer-only access is through `as? TelegramX` / `is TelegramX` (mechanical conversions to `case let`/`case`). Wave 50 reproduces the wave-48 pattern verbatim and lands the same first-pass-clean outcome. + +- **Pre-flight EnginePeer-property forwarding audit confirmed unnecessary for `case let`-bound concrete types.** The `case let .channel(channel)` / `case let .legacyGroup(group)` patterns bind a `TelegramChannel` / `TelegramGroup` directly — methods like `.hasPermission(.editRank)` and `.hasBannedPermission(.banEditRank)` are class methods on the concrete type and call cleanly without `_asPeer()`. The forwarding audit in earlier waves was load-bearing for properties accessed *off the bare EnginePeer* (e.g., `peer.displayTitle` requiring an EnginePeer method); when accessed off a case-bound concrete type, no audit is needed. + +- **Bundling rationale: PSMI + PIMP + PSPB:852 share the same data-flow contour.** `PeerInfoProfileItems.swift:852` is the only producer of `PeerInfoScreenMemberItem` in the migration's scope (PSI:132 also produces but with `enclosingPeer: nil`, no edit needed). PIMP and PSMI form a tight pane–node pair. The 1-edit PSPB inclusion was non-controversial because the PSPB:852 site is the canonical Postbox→Engine boundary above the migrated chain. Boundary-lift inclusion for a 1-edit site is correct. + +- **Bundled implementer dispatch with a single subagent is the right shape for a 19-edit / 3-file mechanical wave.** Per-task subagent dispatch (1 task per file) would have triggered three implementer roundtrips and three review rounds for what is essentially one cohesive editing unit that doesn't pass build at any per-file checkpoint. The subagent-driven-development skill's "fresh subagent per task" convention adapts to "fresh subagent per cohesive editing unit" when individual tasks are not independently verifiable. + +**Plan:** `docs/superpowers/plans/2026-04-25-peerinfo-enclosingpeer-engine-peer.md`. + +--- + +## Wave 51 outcome (2026-04-25) + +`GroupsInCommonListEntry.peer: Peer → EnginePeer` migration in `PeerInfoGroupsInCommonPaneNode.swift` (single-file private struct-field). 7 edits, 1 file, **first-pass-clean**. Wave-shape: narrow internal struct-field migration with deliberate boundary scoping — public init's `openPeerContextAction: (Bool, Peer, …)` field left unmigrated to avoid cascading into `PeerInfoPaneContainerNode` (parent), `PeerInfoRecommendedPeersPaneNode` (sibling pane sharing the closure type), and upstream callers in `PeerInfoScreen.swift`. Saved for a coordinated wave once a wider closure-type sweep is justified. + +**Edits:** + +`PeerInfoGroupsInCommonPaneNode.swift` (7 edits): +- :28 — stored field `var peer: Peer → var peer: EnginePeer`. +- :35 — `lhs.peer.isEqual(rhs.peer)` → `lhs.peer == rhs.peer` (EnginePeer is `Equatable`; was Peer-protocol method). +- :42 — `item()` closure params `(Peer) -> Void` → `(EnginePeer) -> Void` and `(Peer, ASDisplayNode, ContextGesture?) -> Void` → `(EnginePeer, ASDisplayNode, ContextGesture?) -> Void`. +- :44 — `peer: EnginePeer(self.peer)` → `peer: self.peer` (drop wrap; `ItemListPeerItem.peer:` already takes `EnginePeer`). +- :54 — `preparedTransition()` closure params, same migration as :42. +- :232 — `GroupsInCommonListEntry(... peer: peer)` → `peer: EnginePeer(peer)` (boundary lift; `peer.peer` source is `RenderedPeer.peer: Peer?`). +- :236 — `self?.chatControllerInteraction.openPeer(EnginePeer(peer), …)` → `self?.chatControllerInteraction.openPeer(peer, …)` (drop wrap; `peer` is now `EnginePeer` from migrated closure param). +- :238 — `self?.openPeerContextAction(false, peer, node, gesture)` → `self?.openPeerContextAction(false, peer._asPeer(), node, gesture)` (internal bridge to still-Peer-typed stored field at PIGCP:68/109). + +**Net bridge accounting:** +- DROPs (2): 1× `EnginePeer(...)` wrap (PIGCP:44 → ItemListPeerItem), 1× `EnginePeer(...)` wrap (PIGCP:236 → chatControllerInteraction.openPeer). +- ADDs (2): 1× boundary lift `EnginePeer(peer)` (PIGCP:232; `RenderedPeer.peer: Peer?` → struct field; correct/permanent), 1× internal `_asPeer()` bridge (PIGCP:238) to the still-`Peer`-typed stored `openPeerContextAction` field. +- Net internal bridges: **−1**. Boundary lifts: **+1**. + +**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel build 29.454s; PeerInfoScreen + TelegramUI recompiled. Continues the first-pass-clean streak (waves 42, 45, 46, 48, 49, 50, 51 — 7 of last 9 waves first-pass-clean; waves 44 and 47 each took 2 iterations). + +**Commit:** `f2b67a1b54` (7 edits in 1 file). + +**Lessons:** + +- **Narrow-wave shape works for struct-field migrations even when the field flows into closure params.** The internal closures (`item()`, `preparedTransition()`) can be migrated independently from the public init's stored closure field, by sandwiching the still-Postbox-typed boundary at the closure that captures the stored field. This adds one `_asPeer()` bridge at the boundary but keeps the wave atomic and first-pass-clean. +- **Boundary-lift accounting clarifies wave value.** Net internal-bridge count (−1) is the correct progress metric; boundary lifts (+1) at `RenderedPeer.peer` are permanent until `RenderedPeer → EngineRenderedPeer` lands. The wave is a real ratchet step even with net-zero raw bridge count. +- **Memory-stored field name was wrong (memory said `PeerEntry`, actual was `GroupsInCommonListEntry`).** Verify struct identifiers at plan time. Doesn't affect the wave but should keep memory accurate for future planning. + +--- + +## Wave 52 outcome (2026-04-25) + +`PeerInfoPaneContainerNode.openPeerContextAction` closure parameter `Peer → EnginePeer` cascade across `PeerInfoPaneContainerNode` (PIPC), `PeerInfoGroupsInCommonPaneNode` (PIGCP), `PeerInfoRecommendedPeersPaneNode` (PIRP), and `PeerInfoScreen.swift` (PIS) — closes the wave-51 PIGCP:238 `_asPeer()` bridge and unifies the closure type across both sibling panes. 4 files, 8 type-site edits + 5 drops, **first-pass-clean**. + +**Edits:** + +- `PeerInfoPaneContainerNode.swift` (2): :411 init param closure type, :640 stored field closure type — both `(Bool, Peer, …) → (Bool, EnginePeer, …)`. +- `PeerInfoGroupsInCommonPaneNode.swift` (3): :68 stored field, :109 init param, :238 dropped `peer._asPeer()` (the wave-51 bridge — closed in this wave). +- `PeerInfoRecommendedPeersPaneNode.swift` (5): :68 inner item closure, :88 dropped `peer._asPeer()`, :94 `preparedTransition` closure, :119 stored field, :155 init param. +- `PeerInfoScreen.swift` (3): :1331 dropped `EnginePeer(peer)` wrap (`chatInterfaceInteraction.openPeer`), :1340 dropped `EnginePeer(peer)` wrap (`joinChannel(peer:)`), :1348 dropped `EnginePeer(peer)` wrap (second `chatInterfaceInteraction.openPeer`). + +**Net bridge accounting:** +- DROPs (5): 2× internal `_asPeer()` (PIGCP:238 + PIRP:88), 3× `EnginePeer(peer)` wrap (PIS:1331 / :1340 / :1348). +- ADDs (0). The four forwarding closures (PIPC:1044–1045, PIRP:273–274 / :303–304, PIS:1319) all use parameter type inference, so the closure-type cascade ripples through automatically without explicit edits at those sites. +- Net internal bridges: **−5**. No boundary lifts (the migration unifies closure types end-to-end inside the module; receiving APIs already accepted `EnginePeer`). + +**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel build 29.52s. Streak continues — 8 of last 10 waves first-pass-clean (42, 45, 46, 48, 49, 50, 51, 52; waves 44 and 47 each took 2 iterations). + +**Commit:** `c86aa1aba3` (4 files, 13 insertions, 13 deletions). + +**Lessons:** + +- **Closure-type cascade as a wave-shape variant of struct-field migration.** When the migrating type is a closure parameter (not a struct field), Swift's parameter-type inference at every forwarding-closure site does the cascade automatically — no explicit edits needed at type-inferred sites. Wave 52 had 5 inferred-cascade sites (PIPC:1044, PIRP:273/303, PIS:1319) and only 8 explicit type-site edits (init params + stored fields). Compare to wave 50 (struct-field cascade with 19 explicit edits). +- **Pre-flight receiving-API verification is load-bearing for closure-type migrations.** Wave 52's plan verified `chatControllerInteraction.openPeer: (EnginePeer, …)` and `joinChannel(peer: EnginePeer)` BEFORE writing the wave, ensuring the 3 PIS body wraps would drop without compensating ADDs. If either API still took raw `Peer`, the migration would have re-introduced wraps inside the closure body, defeating the wave's purpose. +- **The wave-51 deferral was the correct call.** Wave 51 explicitly noted `openPeerContextAction: (Bool, Peer, …)` would NOT migrate within that wave's scope. Wave 52 closed it cleanly with a 4-file unit. This validates the wave-47 lesson: "memory-stored wave-N+1 candidate notes are load-bearing plan input." The deferral pre-classified the candidate, and wave 52 picked it up directly. +- **Bundled implementer dispatch with Haiku confirmed for closure-type cascades.** Wave 50 established Haiku-sufficiency for mechanical wave-shape edits (19 edits). Wave 52 reproduces with 13 edits — implementer applied all edits byte-perfectly first attempt, all greps passed, build first-pass-clean. Total cost ~56k implementer tokens. + +--- + +## Wave 53 outcome (2026-04-25) + +`PeerInfoScreenData.chatPeer: Peer? → EnginePeer?` field migration with deliberately narrow scope (defers `ClearPeerHistory.init.chatPeer: Peer` and `openClearHistory.chatPeer: Peer` to a future wave). 3 files / 14 insertions / 14 deletions / **first-pass-clean** Bazel ~29.5s. + +**Edits:** + +`PeerInfoData.swift` (PSD, 6 edits): +- :387 — field decl `let chatPeer: Peer? → let chatPeer: EnginePeer?`. +- :443 — init param `chatPeer: Peer? → chatPeer: EnginePeer?`. +- :1028, :1621, :1868, :2206 — boundary lifts at PSD-internal init call sites: `chatPeer: peer → chatPeer: peer.flatMap(EnginePeer.init)` (1028), `chatPeer: peerView.peers[peerId/groupId] → chatPeer: peerView.peers[...].flatMap(EnginePeer.init)` (1621/1868/2206). Each matches the sibling `peer:` line on the same construction. + +`PeerInfoScreenOpenChat.swift` (PISOC, 2 edits): +- :25 — `chatLocation: .peer(EnginePeer(peer)) → chatLocation: .peer(peer)` (drop wrap). +- :89 — same pattern (drop wrap). + +`PeerInfoScreenPerformButtonAction.swift` (PISPBA, 6 edits): +- :428 — `if let secretChat = chatPeer as? TelegramSecretChat → if case let .secretChat(secretChat) = chatPeer`. +- :431 — `} else if let group = chatPeer as? TelegramGroup → } else if case let .legacyGroup(group) = chatPeer`. +- :435 — `} else if let user = chatPeer as? TelegramUser → } else if case let .user(user) = chatPeer`. +- :439 — `} else if let channel = chatPeer as? TelegramChannel → } else if case let .channel(channel) = chatPeer`. +- :463 — `if let channel = chatPeer as? TelegramChannel → if case let .channel(channel) = chatPeer` (separate `if`, not in the else-if chain). +- :851 — `chatPeer: chatPeer → chatPeer: chatPeer._asPeer()` (1× ADD `_asPeer()` boundary bridge for unmigrated `ClearPeerHistory.init.chatPeer: Peer`). + +**Net bridge accounting:** +- DROPs (2): `EnginePeer(peer)` wrap at PISOC:25 + PISOC:89. +- ADDs (1): `_asPeer()` at PISPBA:851 (boundary with unmigrated `ClearPeerHistory.init`). +- Boundary lifts (4): PSD:1028, :1621, :1868, :2206 — `Peer? → EnginePeer?` at the data-flow boundary into the migrated struct field. Permanent until upstream `RenderedPeer.peer / peerView.peers` migrate. +- Net internal bridges: **−1**. + +**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel ~29.5s. Streak: 9 of last 11 waves first-pass-clean (42, 45, 46, 48, 49, 50, 51, 52, 53; waves 44 and 47 each took 2 iterations). + +**Commit:** `438b4d7f46` (3 files, 14/14 line changes). + +**Lessons:** + +- **Narrow-scope is the right wave shape when bundling produces net-negative accounting.** Bundling wave 53 with `ClearPeerHistory.init` + `openClearHistory` migrations would have dropped 4 `EnginePeer(chatPeer)` wraps in `openClearHistory` body BUT added 4–6 `EnginePeer(...)` boundary lifts at PISPBA call sites (where `channel`/`group`/`user` Peer locals get passed). Pre-flight call-site classification turned this from "tempting bundle" into "net-negative bundle" — the deferral is correct. +- **`as? Telegram*` cluster conversion is mechanically safe even with `else if` chains.** 5 conversions in the same `.more` case body, all sharing the `chatPeer` scrutinee. Each `case let .x(y) = chatPeer` has the same scope semantics as the original `let y = chatPeer as? X` — `y` shadows nothing in the surrounding scope, control flow is preserved, and `else if` chains over EnginePeer's 4-case enum are equivalent to the original sequential `as?` casts. +- **Boundary-lift uniformity check.** Each `chatPeer:` PSD construction site already had a sibling `peer:` line one above with `peer.flatMap(EnginePeer.init)` or `peerView.peers[...].flatMap(EnginePeer.init)`. The wave-53 edits are byte-identical to the sibling pattern. Pre-flight verification ("does the sibling line use the same construction?") is the simplest signal that boundary lifts are idiomatic for the surrounding code. +- **Memory's "30+ sites / multi-iter" overcount.** Memory's wave-53 estimate captured the BUNDLED scope; narrow scope reduces to 14 sites / 1-iteration. Future memory updates should distinguish "bundled candidate site count" from "narrow candidate site count" so wave-N+1 budgets aren't anchored too high. + +--- + +## Wave 54 outcome (2026-04-25) + +`ClearPeerHistory.init.chatPeer` + `openClearHistory.chatPeer` `Peer → EnginePeer` bundled method-signature migration. Closes wave-53's deferred sibling. 2 files / 16 insertions / 16 deletions / **first-pass-clean** Bazel 31.484s. Commit `e3da090a7f`. + +**Edits:** + +`PeerInfoScreen.swift` (PIS, 10 edits): +- :3213 — `openClearHistory(... chatPeer: Peer) → ... chatPeer: EnginePeer)`. +- :3230, :3232, :3251, :3269 — drop 4 internal display-call wraps `EnginePeer(chatPeer).compactDisplayTitle → chatPeer.compactDisplayTitle` (the hot-path ratchet target). +- :7416 — `ClearPeerHistory.init(... chatPeer: Peer, cachedData:) → ... chatPeer: EnginePeer, cachedData:`. +- :7421 — `} else if chatPeer is TelegramSecretChat { → } else if case .secretChat = chatPeer {` (no-bind pattern; case body uses no fields from the secretChat). +- :7425 — `} else if let group = chatPeer as? TelegramGroup { → } else if case let .legacyGroup(group) = chatPeer {`. +- :7436 — `} else if let channel = chatPeer as? TelegramChannel { → } else if case let .channel(channel) = chatPeer {`. +- :7464 — `if let user = chatPeer as? TelegramUser, user.botInfo != nil { → if case let .user(user) = chatPeer, user.botInfo != nil {`. + +`PeerInfoScreenPerformButtonAction.swift` (PISPBA, 6 edits): +- :851 — DROP wave-53 ADD: `chatPeer: chatPeer._asPeer() → chatPeer: chatPeer`. +- :857 — boundary lift: `chatPeer: user → chatPeer: EnginePeer(user)` (TelegramUser local from `case let .user(user)` upstream). +- :1067, :1073 — `chatPeer: channel → chatPeer: EnginePeer(channel)` (TelegramChannel locals). +- :1234, :1240 — `chatPeer: group → chatPeer: EnginePeer(group)` (TelegramGroup locals). + +**Net bridge accounting:** +- DROPs (5): 4 internal `EnginePeer(chatPeer).compactDisplayTitle` wraps in PIS:openClearHistory body + 1 `_asPeer()` bridge at PISPBA:851 (the wave-53 ADD). +- ADDs (5): 5 boundary `EnginePeer(...)` lifts at PISPBA call sites. +- Conversions (4): `is`/`as?` → `case let` on PIS:7421/7425/7436/7464. +- Type-site (2): signature changes on PIS:3213 and PIS:7416. +- Net internal bridges: **0 raw count** — but the **ratchet kills 4 internal display-call wraps in the hot path** (PIS:3230/3232/3251/3269); only call-site boundary lifts remain, and those are permanent until upstream PISPBA sites get further migrated (e.g., to flow EnginePeer locals end-to-end). + +**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel 31.484s. Streak: 10 of last 12 waves first-pass-clean (42, 45, 46, 48, 49, 50, 51, 52, 53, 54; waves 44 and 47 each took 2 iterations). + +**Lessons:** + +- **Bundled deferral closure cleanly inverts the wave-53 narrow-scope decision.** Wave 53 deferred this bundle because the call-site classification at that time was "5 boundary lifts vs. 4 wrap drops = net-negative" — but the analysis missed that closing wave-53's 1 ADD is also a drop. The actual full accounting at wave-54 time: 5 drops (4 wraps + wave-53 ADD) vs. 5 lifts (1 user + 2 channel + 2 group) = 0 raw count, with ratchet benefit of 4 hot-path wraps killed. Wave-by-wave deferral followed by closure is the right shape when the bundle's first half is independently valuable (wave 53 dropped 2 PISOC wraps and migrated the field type on its own merit). +- **`case .secretChat = chatPeer` (no-bind pattern) compiles cleanly under `-warnings-as-errors`.** No "unused binding" warning because there is no binding. The original `chatPeer is TelegramSecretChat` form is structurally similar — both are predicate checks with no name to discard. +- **`peer:` parameter unused in body is fine for `-warnings-as-errors`.** `openClearHistory(... peer: Peer, ...)` body never references `peer`; this passed wave 53 and continues to pass wave 54. Function-parameter unused warnings are not enabled in this codebase's compile flags. Confirms a wave-53 implicit assumption. +- **Bundled signature migration with mechanical `as?` cluster + small call-site count = 1-iteration target.** 2 files, 16 edits, 4 case-let conversions, 5 boundary lifts, 1 wave-53-ADD drop, 4 hot-path wrap drops. Full-project Bazel completed in one shot. + +--- + +## Wave 55 outcome (2026-04-25) + +`PeerInfoScreenViewControllerNode.deletePeerChat(peer:)` private-method first-arg `Peer → EnginePeer`. 1 file / 4 edits / **first-pass-clean** Bazel 29.284s. Commit `4818a0f090`. + +**Edits:** all in `PeerInfoScreen.swift`: +- :4502 — drop `_asPeer()` at `openDeletePeer` ActionSheet button: `self?.deletePeerChat(peer: peer._asPeer(), globally: true) → self?.deletePeerChat(peer: peer, globally: true)`. +- :4556 — same drop at `openLeavePeer` TextAlertAction. +- :4564 — definition signature `peer: Peer → peer: EnginePeer`. +- :4573 — drop internal wrap inside body: `EngineRenderedPeer(peer: EnginePeer(peer)) → EngineRenderedPeer(peer: peer)` passed to `chatListController.maybeAskForPeerChatRemoval`. + +**Net:** 3 drops, 0 adds. Both call sites' `peer` is `EnginePeer` (sourced from `engine.data.get(...Item.Peer.Peer(id:))` returning `Signal`). Hot-path bridges eliminated end-to-end from delete/leave entry points down to `maybeAskForPeerChatRemoval`. Streak: 11 of last 13 first-pass-clean. + +**Lessons:** + +- **Single-file private-method migration with both call sites already EnginePeer = pure-drop wave shape.** The `_asPeer()` bridge at the call site exists *because* the receiving method was Peer-typed; once both endpoints are migrated, the bridge becomes deletable in the same wave. Pre-flight grep for `(.*\._asPeer\(\))` patterns surfaces this shape immediately. + +--- + +## Wave 56 outcome (2026-04-25) + +`PeerInfoInteraction.openPeerInfo` closure-type and `PeerInfoScreenViewControllerNode.openPeerInfo` private-method first-arg `Peer → EnginePeer` cascade. 3 files / 7 edits / **first-pass-clean** Bazel 29.074s. Commit `31433fc1d4`. + +**Edits:** + +`PeerInfoInteraction.swift` (PII, 2 edits): +- :45 — `let openPeerInfo: (Peer, Bool) -> Void → (EnginePeer, Bool) -> Void`. +- :123 — same closure type in init param. + +`PeerInfoScreen.swift` (PIS, 2 edits): +- :4304 — `private func openPeerInfo(peer: Peer, ...) → ... peer: EnginePeer`. +- :4306 — drop internal `EnginePeer(peer)` wrap (passed to `makePeerInfoController` which already takes EnginePeer post-wave-39). +- :1482 — boundary lift: `strongSelf.openPeerInfo(peer: member.peer, ...) → ... peer: EnginePeer(member.peer)` (`PeerInfoMember.peer` is still `Peer`, depends on RenderedPeer foundational migration). + +`PeerInfoProfileItems.swift` (PIPI, 2 edits): +- :524 — drop `_asPeer()` bridge: `interaction.openPeerInfo(managedByBot._asPeer(), false) → interaction.openPeerInfo(managedByBot, false)` (`managedByBot` is EnginePeer? from PSD field). +- :860 — boundary lift: `interaction.openPeerInfo(member.peer, true) → interaction.openPeerInfo(EnginePeer(member.peer), true)`. + +**Net:** 2 drops (PIS:4306 wrap + PIPI:524 _asPeer bridge), 2 boundary lifts (PIS:1482 + PIPI:860 at PeerInfoMember.peer source sites). Net raw count = 0 but 1 hot-path bridge eliminated. The 2 boundary lifts will close when `PeerInfoMember.peer` migrates (depends on RenderedPeer foundational session). Streak: 12 of last 14. + +**Lessons:** + +- **PIS:520-521 lambda forwarding `peer, isMember in self?.openPeerInfo(...)` needs no edit when the closure-field type migrates.** Swift parameter-type inference cascades from the migrated closure-field type to the lambda's parameter, and the lambda body forwards an EnginePeer to the migrated method. This is the same lesson as wave 52's PIPC.openPeerContextAction cascade — when the lambda's only role is to forward the parameter, the type re-bind is invisible at the lambda site. +- **Net-zero migrations are still worth doing when they consolidate a hot path.** Even though wave 56 has 2 drops and 2 adds, the 2 adds are at sources that will likely migrate later (PeerInfoMember.peer foundational), and the 2 drops include a hot-path `_asPeer()` ratchet at PIPI:524. The migration also makes the chain (`interaction.openPeerInfo → self.openPeerInfo → makePeerInfoController`) cleanly EnginePeer end-to-end, so future migrations can reason about it without bridge-hopping. + +--- + +## Wave 57 outcome (2026-04-25) + +Add `EnginePeer.isCopyProtectionEnabled` forwarding property in TelegramCore + drop 4 consumer-side `_asPeer().isCopyProtectionEnabled` bridges. 5 files / 5 edits / **first-pass-clean** Bazel 237.868s (cascade compile from TelegramCore touch). Commit `a5fc9fcf0e`. + +**Edits:** + +`TelegramCore/Sources/TelegramEngine/Peers/Peer.swift` (1 edit, 1 addition): +- Appended `var isCopyProtectionEnabled: Bool { return self._asPeer().isCopyProtectionEnabled }` to the existing `public extension EnginePeer { ... }` block (sibling to isDeleted, isScam, isVerified, isPremium). + +Consumer drops (4 edits across 4 files): +- `PeerInfoEditingAvatarNode.swift:166` — `peer._asPeer().isCopyProtectionEnabled → peer.isCopyProtectionEnabled` (NativeVideoContent.captureProtected). +- `PeerInfoAvatarTransformContainerNode.swift:406` — same. +- `PeerInfoData.swift:2259` — `peerInfoIsCopyProtected(data:)` body. +- `PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift:128` — `canShare = !peer.isCopyProtectionEnabled` (peer from `case let .image(_, _, _, _, peer, _, _, _, _, _, _, _) = entry`, which is `EnginePeer?`). + +**Net:** 4 internal `_asPeer()` bridge drops; 0 adds. Other `_asPeer()` calls at adjacent lines (e.g., `PeerReference(peer._asPeer())` at PIED:159 / PIATCN:404) remain — `PeerReference.init` still takes raw Postbox Peer. + +**Cascade compile:** full-project Bazel 237.868s (vs. ~30s typical) due to TelegramCore touch triggering downstream module rebuilds. Behavior parity verified by build success. + +**Lessons:** + +- **EnginePeer forwarding-extension addition is a high-leverage wave shape when ≥3 consumer sites use the same `_asPeer().` pattern.** Cost: 1 line in TelegramCore. Benefit: drops N consumer-side bridges (4 in this wave) and unlocks future call-site simplifications that pattern-match on the property. The wave-26 lesson "EnginePeer forwarding audit is load-bearing for multi-property methods" extends to "single-property forwarding is a wave shape on its own when the access-count threshold is met." +- **TelegramCore touches incur cascade-recompile cost (~210s extra wall-clock).** Worth budgeting; not a blocker. Future TelegramCore-touching waves should weight cascade time as part of wave-shape planning. +- **`EnginePeer` cases (.user/.legacyGroup/.channel/.secretChat) all preserve the underlying Postbox Peer's flag access via `_asPeer()`** — the forwarding implementation is a one-line wrapper. No case-by-case logic needed for properties that already exist on `Peer` protocol's extension. Rule of thumb: any `var ` on `extension Peer` in PeerUtils.swift can be forwarded mechanically. + +--- + +## Wave 58 outcome (2026-04-25) + +`AccountContext.openAddPeerMembers` + `presentAddMembersImpl` cross-module `groupPeer: Peer → EnginePeer` migration. 6 files / 9 edits / **2 build iterations**. Commit `261c086c15`. Bazel 180.751s (iter 1, failure with 1 error) + 29.946s (iter 2, clean) = ~210s wall-clock. + +**Edits:** + +Protocol + impl (2 edits, 2 files): +- `AccountContext.swift:1456` — protocol method `groupPeer: Peer → groupPeer: EnginePeer`. +- `SharedAccountContext.swift:2351` — implementation signature. + +`PresentAddMembers.swift` (4 edits, 1 file): +- :14 — public function signature. +- :38 — `if let group = groupPeer as? TelegramGroup → if case let .legacyGroup(group) = groupPeer`. +- :47 — `} else if let channel = groupPeer as? TelegramChannel, ... → } else if case let .channel(channel) = groupPeer, ...`. +- :210 — **iter-2 fix:** drop now-redundant `EnginePeer(groupPeer)` wrap inside body — `peer: EnginePeer(groupPeer) → peer: groupPeer` (after parameter is EnginePeer, wrapping it again doesn't compile). + +Call sites (3 edits, 3 files): +- `PeerInfoScreen.swift:4606` — drop `_asPeer()`: `groupPeer: groupPeer._asPeer() → groupPeer: groupPeer` (`groupPeer = data.peer` is EnginePeer? post-wave-42). +- `ChatListUI/Sources/ChatListController.swift:3815` — drop `_asPeer()`: `groupPeer: peer._asPeer() → groupPeer: peer` (`peer` from `engine.data.get(...Item.Peer.Peer(id:))` is EnginePeer). +- `TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift:4006` — boundary lift: `groupPeer: peer → groupPeer: EnginePeer(peer)` (`peer` from `renderedPeer?.peer` is Postbox Peer; needed because `is TelegramGroup || is TelegramChannel` pre-check still runs against the Peer-typed local). + +**Net:** 3 drops (2 `_asPeer()` at PIS:4606 + CLC:3815, 1 redundant `EnginePeer()` wrap at PAM:210), 1 boundary lift (CCLDN:4006), 2 case-let conversions in PAM body. Net internal-bridge progress: **−2**. + +**Iter-2 lesson — pre-flight scope must include "internal wraps inside the migrated function's body."** Pre-flight grep covered: +- `(...:` call sites ✓ +- ` as? Telegram*` casts ✓ +But missed: +- `EnginePeer()` wraps INSIDE the migrated body — once the parameter type changes, those become invalid. + +Adding `\b` (the parameter name) to the pre-flight grep would have caught the PAM:210 site. Cheap rule going forward: grep the body for `EnginePeer\(\)` before declaring inventory complete. + +**Lessons:** + +- **Cross-module migration with protocol method = uniform 2-iter target unless body is small.** `AccountContext` protocol method touches 3 callers + 1 impl + 1 forwarding free function with internal body. Each axis can hide one missed reference. Wave 58 hit one such miss (PAM:210); this is typical. Budget 2 iters for protocol-method migrations even when all axes look complete in inventory. +- **Boundary-lift planning is correct when the call-site's source is foundational.** CCLDN:4006 boundary lift is permanent until `RenderedPeer.peer` migrates. Adding it now is the right shape — defers the foundational migration without inflating the wave to that scope. +- **Pure cross-module migrations don't always cascade-recompile through TelegramCore.** Wave 58 touched TelegramUI consumers + AccountContext protocol but NOT TelegramCore; iter-2 rebuild was 29.9s (typical). Compare to wave 57's 237.9s when TelegramCore was touched. + +--- + +## Wave 59 outcome (2026-04-25) + +`_asPeer() as? TelegramX` micro-cluster migration. 4 files / 7 edits / **first-pass-clean** Bazel 25.833s. Commit `6ca4058ae8`. + +**Edits:** + +- `PeerInfoHeaderEditingContentNode.swift:115` — `(peer?._asPeer() as? TelegramUser)?.lastName ?? ""` → `if case let .user(user) = peer { updateText = user.lastName ?? "" } else { updateText = "" }` (preserves `?? ""` fallback semantics on cast-failure path). +- `StarsTransactionsScreen.swift:1288` — `if let channel = subscription.peer._asPeer() as? TelegramChannel { ... }` → `if case let .channel(channel) = subscription.peer { ... }`. +- `StarsTransactionScreen.swift:280` — `if let creationDate = (subscription.peer._asPeer() as? TelegramChannel)?.creationDate, creationDate > 0 {` → `if case let .channel(channel) = subscription.peer, channel.creationDate > 0 { additionalDate = channel.creationDate ... }`. (TelegramChannel.creationDate is non-optional Int32.) +- `ChatListSearchContainerNode.swift:927` — `if let user = message.author?._asPeer() as? TelegramUser { ... }` → `if case let .user(user) = message.author { ... }` (message.author is EnginePeer?). + +**Net:** 4 internal `_asPeer() as? TelegramX` cast bridges eliminated; 0 adds. PIHEC site adds explicit `else { updateText = "" }` to preserve original cast-failure fallback. + +**Lessons:** + +- **`_asPeer()` is a tag for "this value is EnginePeer" by construction.** `_asPeer()` only exists on `EnginePeer`; calling it on any other type (including `Peer` protocol) doesn't compile. So every consumer-side `X._asPeer()` is provably an EnginePeer source — no need to verify per-site. This invariant makes bulk `_asPeer()` cluster migrations safe. +- **Preserve cast-failure-path semantics when converting `as?` clusters.** Original `(X as? TelegramUser)?.lastName ?? ""` returns `""` if X is nil OR not TelegramUser OR lastName is nil. Naive `if case let .user(user) = X { updateText = user.lastName ?? "" }` only sets updateText for the .user case, leaving it nil for other cases. Add explicit `else` branch when the original `??` fallback is load-bearing. + +--- + +## Wave 60 outcome (2026-04-25) + +Add `PeerReference.init?(_ peer: EnginePeer)` convenience init in TelegramCore + drop **49** consumer-side `PeerReference(X._asPeer())` bridges across 29 files / 53 insertions / 49 deletions. **First-pass-clean** Bazel 238.803s (cascade compile). Commit `b6cc2bfbd1`. + +**TelegramCore extension addition:** + +`submodules/TelegramCore/Sources/ApiUtils/ApiUtils.swift` — appended `init?(_ peer: EnginePeer) { self.init(peer._asPeer()) }` to the existing `public extension PeerReference` block. + +**Consumer drops:** 49 sites where `PeerReference(X._asPeer())` becomes `PeerReference(X)`. 12 distinct expression patterns: `$0`, `author`, `bot.peer`, `component.peer`, `component.slice.effectivePeer`, `component.slice.peer`, `item.peer`, `participantPeer`, `peer`, `peerValue`, `primary.1`, `self.peer`. All EnginePeer-typed by the `_asPeer()` invariant. + +**Execution method:** Python regex replacement (`re.sub` with pattern `PeerReference\(([^)(]+)\._asPeer\(\)\)` → `PeerReference(\1)`). Non-`)`-non-`(` capture group ensures the match is the smallest expression before the trailing `._asPeer())`. Tested against all 12 distinct expression patterns including chained property accesses and closure-arg captures. + +**Net:** 49 internal `_asPeer()` bridge drops; 0 adds. Largest single-wave consumer-drop count to date. + +**Lessons:** + +- **The wave-57 forwarding-extension pattern scales to bulk init/method addition.** Convenience init/method overload in TelegramCore (1 line) drops N consumer bridges (49 in this case). Cost: 1 line + bulk consumer migration. Benefit: O(N) bridges dropped. Threshold for triggering this shape is ≥5 consumer sites (anything fewer is per-site case-by-case). +- **Python regex with `[^)(]+` capture is the safe automation primitive for migrating `Wrapper(X._asPeer())` patterns.** Greedy `.*` captures cause over-match. Non-greedy `.*?` is fragile in BSD sed. The `[^)(]+` capture explicitly avoids both `(` and `)` inside the captured expression, which works for chained property accesses (`a.b.c`), closure args (`$0`), and tuple-indexed accesses (`primary.1`). 49 sites all replaced safely. +- **The `_asPeer()` invariant guarantees migration safety.** Because `_asPeer()` only compiles on EnginePeer, no per-site type verification is needed for `Wrapper(X._asPeer()) → Wrapper(X)` migrations once the EnginePeer-accepting overload is added. + +--- + +## Wave 61 outcome (2026-04-25) + +Drop 6 `_asPeer().X` consumer-side bridges where X is already on the EnginePeer forwarding extension. 3 files / 6 edits / **first-pass-clean** Bazel 19.351s. Commit `5d497cc5e9`. + +**Edits:** + +- `ChannelVisibilityController.swift:1478` — `peer?._asPeer().usernames` → `peer?.usernames`. +- `PeerInfoCoverComponent.swift:74` — `peer._asPeer().profileColor` → `peer.profileColor`. +- `PeerInfoCoverComponent.swift:82` — `peer._asPeer().nameColor` → `peer.nameColor`. +- `StoryItemSetContainerComponent.swift:6700/6956/7274` — `component.slice.effectivePeer._asPeer().usernames` → `component.slice.effectivePeer.usernames` (replace_all=true; 3 sites). + +**Net:** 6 internal `_asPeer()` bridge drops; 0 adds. All 6 sites used properties (`usernames`, `profileColor`, `nameColor`) already present on `public extension EnginePeer` block in TelegramCore — no new TelegramCore touch needed (no cascade compile). + +**Skipped:** `peer._asPeer().indexName.indexTokens` at `ChannelDiscussionGroupSearchContainerNode.swift:157` — `EnginePeer.indexName` returns wrapper enum `EnginePeer.IndexName` without `.indexTokens`. Needs a separate IndexName extension or consumer refactor. Defer. + +--- + +## Wave 62 outcome (2026-04-25) + +Add 4 EnginePeer forwarding entries (isMonoForum, associatedPeerId, hasCustomNameColor, hasSensitiveContent) + drop 5 consumer-side `_asPeer().X` bridges. 6 files / 9 edits / **first-pass-clean** Bazel 237.769s (cascade compile). Commit `cdce0dba01`. + +**TelegramCore extension additions** (Peer.swift, 4 entries — 16 lines): + +```swift +var isMonoForum: Bool { + return self._asPeer().isMonoForum +} +var associatedPeerId: Id? { + return self._asPeer().associatedPeerId +} +var hasCustomNameColor: Bool { + return self._asPeer().hasCustomNameColor +} +func hasSensitiveContent(platform: String) -> Bool { + return self._asPeer().hasSensitiveContent(platform: platform) +} +``` + +All four forwarding implementations live in `submodules/TelegramCore/Sources/Utils/PeerUtils.swift` on the `extension Peer { ... }` block. The `Id` typealias on EnginePeer is `PeerId`, so `associatedPeerId: Id?` is type-equivalent to the underlying `Peer.associatedPeerId: PeerId?`. + +**Consumer drops** (5 sites): + +- `ThemeSettingsController.swift:428` — `accountPeer._asPeer().hasCustomNameColor` → `accountPeer.hasCustomNameColor`. +- `AgeVerificationScreen.swift:31` — `peer._asPeer().hasSensitiveContent(platform: "ios")` → `peer.hasSensitiveContent(platform: "ios")`. +- `TextProcessingScreen.swift:1281` — `peer._asPeer().isMonoForum` → `peer.isMonoForum`. +- `ShareSearchContainerNode.swift:478` — `mainPeer._asPeer().associatedPeerId` → `mainPeer.associatedPeerId`. +- `ChatListSearchListPaneNode.swift:153` — `maybeChatPeer._asPeer().associatedPeerId` → `maybeChatPeer.associatedPeerId`. + +**Net:** 5 internal bridge drops; 0 adds. + +**Lessons:** + +- **Bundle multi-property forwarding additions in a single TelegramCore touch.** When 4 properties each unblock 1-2 consumer drops, bundling the additions into one wave amortizes the cascade-recompile cost (~210s) across 5 drops instead of paying 4× across 4 separate waves. + +--- + +## Wave 63 outcome (2026-04-25) + +`resolvedAreStoriesMuted(peer:)` Peer → EnginePeer cross-module function-signature migration. 5 files / 11 edits / **first-pass-clean** Bazel 239.818s (cascade compile). Commit `499edc0ddb`. + +**TelegramCore signature change** (`ChangePeerNotificationSettings.swift`): +- :65 — `peer: Peer → peer: EnginePeer`. Body uses only `peer.id` (works on EnginePeer). +- :117 — internal call site boundary lift: `peer: peer → peer: EnginePeer(peer)` (peer source is `transaction.getPeer(peerId)` returning Postbox Peer). + +**Consumer drops** (9 sites, 4 files): +- `ContactContextMenus.swift:50` — drop `_asPeer()`. +- `ChatListController.swift:3317` — drop `_asPeer()`. +- `StoryItemSetContainerComponent.swift:7224` — drop `_asPeer()` on `component.slice.effectivePeer`. +- `StoryChatContent.swift` — 6 sites (lines 210, 212, 1287, 1599, 2490, 2492); pattern `peer: peer._asPeer(),` → `peer: peer,` via `replace_all=true`. + +**Net:** 9 drops, 1 boundary lift = **−8 internal bridges**. + +**Lessons:** + +- **Cross-module function-signature migration is a uniform 1-iter target when (a) the function body uses only EnginePeer-compatible properties and (b) all call sites have EnginePeer-typed sources via `_asPeer()`.** Pre-flight grep `.*_asPeer` enumerates all consumer-side bridges; each drop is mechanical. Internal call sites in TelegramCore that pass Postbox Peer-typed locals get a boundary lift. +- **`replace_all=true` shines when the same parameter pattern repeats in a single file.** StoryChatContent had 6 identical `peer: peer._asPeer(),` patterns — one Edit call with `replace_all=true` replaced all of them. Saves 5 round-trips. + +--- + +## Wave 64 outcome (2026-04-25) + +`RenderedPeer.convenience init(peer: EnginePeer)` in TelegramCore + 5 consumer drops. 3 files / 6 edits / **first-pass-clean** Bazel 237.725s (cascade compile). Commit `109c2fe172`. Wave-shape: foundational TelegramCore extension addition (wave-57 pattern). Original `init(peer: Peer)` retained as designated init; Swift overload resolution selects the EnginePeer init only when the argument is EnginePeer-typed. Sites: PeerInfoSettingsItems:131 + ChatListSearchListPaneNode:4173/4213/4336/4371. + +## Wave 65 outcome (2026-04-25) + +`MergedAvatarsNode.update(peers: [Peer]) → [EnginePeer]` + private `PeerAvatarReference.init(peer:)` cascade. 6 files / 9 edits / **3 iter**. Commit `37c680c86c`. Iter-2 missed `groupsInCommon: [Peer]` field at ChatUserInfoItem; iter-3 missed `recentVoterPeers: [Peer]` and `avatarPeers: [Peer]` at ChatMessagePollBubbleContentNode. Lesson: **migrating a public function's array-of-Peer parameter forces a transitive inventory of *all* `[Peer]` field declarations across consumer modules**. Pre-flight grep should include `\[Peer\]\s*=\s*\[\]` and similar local/field decls. + +## Wave 66 outcome (2026-04-25) + +`VoiceChatJoinScreen.setPeer` + `VoiceChatPreviewContentNode.init` chain Peer→EnginePeer. Single file / 5 edits / first-pass-clean. Commit `148aa53f3f`. Net **−3** internal bridges. Wave-55 single-file pure-drop pattern. + +## Wave 67 outcome (2026-04-25) + +`_internal_storedMessageFromSearchPeer` signature Peer→EnginePeer + return type Signal→Signal. 3 files / 5 edits / first-pass-clean Bazel 226.884s. Commit `3732fb66b6`. Drops `peer._asPeer()` and `|> map { EnginePeer(result) }` in `ensurePeerIsLocallyAvailable`. Net **−1** with 2 internal boundary lifts inside TelegramCore body. + +## Wave 68 outcome (2026-04-25) + +`SelectivePrivacyPeer.convenience init(peer: EnginePeer, participantCount:)` + 3 consumer drops. 4 files / 4 edits / first-pass-clean (initial 221s after TelegramCore touch + 17s consumer-only rebuild). Commit `73811a4e5d`. Original `init(peer: Peer)` and stored `peer: Peer` field unchanged — full migration of the field deferred (69 references repo-wide). + +## Wave 69 outcome (2026-04-25) + +`_internal_storedMessageFromSearchPeers` (plural) Peer→EnginePeer. Closes wave-67 sibling. 2 files / 3 edits / first-pass-clean Bazel 225.143s. Commit `42252eb9fd`. Drops `peers.map { $0._asPeer() }` in `ensurePeersAreLocallyAvailable`. + +## Wave 70 outcome (2026-04-25) + +`StatsMessageItem.peer` + `ChannelStatsController .post` enum-case payload + `MessageStatsController` local var Peer→EnginePeer cascade. 3 files / 9 edits / **3 iter**. Commit `f14dfe2273`. Iter-2 missed `entries.append(.post)` building sites at ChannelStatsController:1429/1433. Iter-3 missed `arePeersEqual(lhsPeer, rhsPeer)` Equatable comparison at L639 — replaced with `lhsPeer == rhsPeer` since EnginePeer is Equatable. **Pre-flight rule: when migrating an enum-case payload from Peer to EnginePeer, grep the same enum's `==` Equatable conformance for `arePeersEqual` calls.** Net **−5** internal bridges. + +## Wave 71 outcome (2026-04-25) + +`peerInfoControllerImpl` signature Peer→EnginePeer + drop `_asPeer()` shadow at SAC:1938. 1 file / 6 edits / first-pass-clean Bazel 24.582s. Commit `1650bc1521`. Wave-shape: single-file private-function with shadow-assignment pattern (`let peer = peer._asPeer()` shadowing the EnginePeer parameter to a Peer local). Mechanical conversion of body's 4 `as?`/`is` checks. Cheap rebuild because no public API change. + +## Wave 72 outcome (2026-04-25) + +`canSendMessagesToPeer` body cleanup: drop `_asPeer()` shadow + restructure `as?` casts as exhaustive switch on EnginePeer cases. 1 file / 1 edit / first-pass-clean Bazel 25.792s. Commit `98e7158b7a`. Wave-shape: TelegramCore-internal body cleanup. Public API was already EnginePeer (wave 38) — this wave only refactors the implementation. Cheap rebuild despite TelegramCore touch. + +## Wave 73 outcome (2026-04-25) + +`ChatQrCodeScreenImpl.Subject.peer` enum-payload Peer→EnginePeer + `QrContentNode.peer` field migration + drop `_asPeer()` shadow at SAC:2731. 2 files / 8 edits / **2 iter**. Commit `0faf4a0336`. Iter-2 missed two additional `peer as? TelegramX` casts at QrContentNode body L1742/L1744; also fixed an over-aggressive `replace_all=true` that caught a different `peer: EnginePeer(peer)` site at MessageContentNode L2184 where the local `peer` is Postbox-typed (boundary lift restored). + +**Lesson reinforced: `replace_all=true` on a parametric wrapping pattern (`peer: EnginePeer(peer)`) must verify ALL matches share the same scope/source.** When the same call appears in multiple scopes with different local-variable types, `replace_all` propagates the wrong intent. Mitigation: grep first to verify unique-scope assumption, or fall back to per-site Edit when unsure. + +--- + +## Wave 103 outcome (2026-04-26): ABANDONED + +`ChatRecentActionsControllerNode.peer: Peer → EnginePeer` (wave-71-shadow close). Implementation built and committed (`e60a8692a7`, build clean at iter-3 / 41s), then **reverted** (`git reset --hard HEAD~1`) after pre-flight failure surfaced. + +**Spec promised:** −1 boundary `_asPeer()` (CRAC:277) + −1 `import Postbox` (CRACN:5) + 0 ADD wraps. 7 edits / 2 files / 1 iter. + +**Actual outcome before revert:** −1 boundary `_asPeer()` (CRAC:277) + −1 `EnginePeer(strongSelf.peer)` wrap (CRACN:535, bonus) + 0 `import Postbox` drops (raw `Message`/`MessageId` references for `AdminLogEventAction` payloads block the drop) + **+2 ADD `_asPeer()` wraps** (CRACN:228 for `canSetupAutoremoveTimeout` Peer-protocol extension, CRACN:737 for `chatRecentActionsHistoryPreparedTransition(peer: Peer)` helper). Net wrap delta: **0**, not the promised −1. Net `import Postbox` drop: **0**, not the promised −1. + +**Why "extend in follow-up" was rejected:** Migrating `chatRecentActionsHistoryPreparedTransition(peer:)` to `EnginePeer` to drop the CRACN:737 ADD bridge would cascade into `ChatRecentActionsEntry.item(peer:)`. Inventory found 75 ADD-bridge sites in the helper body: 72 `peers[peer.id] = peer` stores into a `SimpleDictionary` (the type required by `Message(...)` constructor's `peers:` parameter) plus 3 `filterMessageChannelPeer(peer)` calls. Net cost of dropping 1 bridge: **+74 new bridges**. Catastrophic regression. + +**Pre-flight failure modes (lessons):** + +- **Pre-flight grep for `self..X` access must enumerate Peer-protocol extension methods, not just downcast patterns.** Wave-103 spec grepped `self.peer as\?` and `self.peer\.id\b` but missed `self.peer.canSetupAutoremoveTimeout(...)` at CRACN:228. `canSetupAutoremoveTimeout` is a `public extension Peer` method in `AccountContext/Sources/ChatController.swift:1013` — not on `EnginePeer`. **Mitigation:** future wave specs that retype a stored field from `Peer` to `EnginePeer` MUST grep for ALL `self..(` patterns and verify each method is reachable on `EnginePeer` (not just on the `Peer` protocol). Build a method-allowlist for `EnginePeer` from `extension EnginePeer` declarations in TelegramCore and intersect. + +- **Pre-flight grep for `` flowing into Peer-typed function parameters.** Wave-103 spec missed that `peer` was passed to `chatRecentActionsHistoryPreparedTransition(peer: Peer)` at CRACN:737. The helper sits in a different file (`ChatRecentActionsHistoryTransition.swift`) but the same submodule. **Mitigation:** grep `: peer\b|, peer:|peer: peer\b|peer:\s*self\.peer` across the entire submodule, classify each call site as (a) target accepts EnginePeer (clean), (b) target accepts Peer (would need `_asPeer()` ADD or co-migration of target), (c) target accepts `EnginePeer(peer)` wrap (clean drop). + +- **Wave-71-shadow close is NOT always cheap.** The implicit assumption ("caller already has EnginePeer; just drop the boundary `_asPeer()` and retype the storage") only holds when ALL of: (a) every `self..X` access has an EnginePeer-compatible counterpart, (b) every function call passing `self.` accepts EnginePeer, (c) no protocol-conformance constraint on the stored field exists. Wave 71 itself worked because `peerInfoControllerImpl` was a single private function with mechanical body. Wave 103 failed because the file participates in a deep helper-cascade that builds Postbox `Message` values — and `Message`'s `peers: SimpleDictionary` constructor parameter is a hard barrier. + +- **The `Message(... peers: SimpleDictionary, ...)` constructor is a hard wave barrier.** Any helper that builds Message values from a peer-typed parameter has Cat-3 ADD-bridge sites everywhere it stores the migrated peer into the peers dict. ChatRecentActionsHistoryTransition.swift has 70+ such constructions. Future waves should treat Message-building helpers as red-flagged candidates and pre-flight inventory their `peers[X.id] = X` store sites before promising any `peer: Peer → EnginePeer` migration upstream. + +- **Spec self-review must include an "ADD wraps inventory" pass.** The wave-103 spec claimed "ADD wraps: 0" but the pre-flight grep didn't actually verify this — only the cast/`is` patterns were enumerated. Future spec template: a dedicated "ADD-wrap risk grep" section listing the regex patterns checked (Peer-protocol method calls, function calls accepting `: Peer`, dictionary stores into `[PeerId: Peer]`). + +**Outcome:** wave-103 candidate marked **abandoned** in `project_postbox_refactor_next_wave.md`. Spec and plan docs retained at `docs/superpowers/specs/2026-04-26-postbox-wave-103-chat-recent-actions-controller-node-design.md` and `docs/superpowers/plans/2026-04-26-postbox-wave-103-chat-recent-actions-controller-node.md` as record of the failed attempt. + +--- + +## Wave 103 (retry) outcome (2026-04-26) + +`accountManager.mediaBox.storeResourceData(...)` Shape-A drain against the wave-94 `AccountManagerResources.storeResourceData(id:data:synchronous:)` facade. 5 sites / 2 files / 3 Edit calls (1 single + 2 `replace_all=true` batches) / **first-pass-clean** Bazel 29.5s (warm cache). Commit `92230b0691`. Sites: ThemeUpdateManager:112 (with `synchronous: true`), WallpaperResources:973+1214 (`reference.resource.id` pattern, replace_all), WallpaperResources:1260+1523 (`file.file.resource.id` pattern, replace_all). + +**Net delta:** −5 raw `mediaBox.X` accesses, +5 facade calls, +5 `EngineMediaResource.Id(...)` wraps (canonical engine-side, not Postbox bridges). + +**Lesson reinforced:** wave-shape-G drain against an existing facade is the cheapest reliable wave shape. 1-iter, ~30s build, single atomic commit. Pre-flight risk inventory takes ~5 minutes; implementation takes ~30s. Use this shape after a difficult abandonment to rebuild momentum. + +**Wave-shape-G drain after a failed wave-71-shadow:** the contrast between the abandoned wave-103 (`peer: Peer → EnginePeer` field migration with hidden 75-site cascade) and the retry (5-site call-rewrite drain) illustrates why facade-drain waves and field-migration waves are categorically different. Drains have bounded scope (only sites matching a literal text pattern); field migrations have unbounded scope (any consumer of any field-typed value). Future wave selection should prefer drains when the goal is consistent forward progress and reserve field migrations for sessions with explicit budget for cascade investigation. + +--- + +## Wave 104 outcome (2026-04-26) + +`accountManager.mediaBox.resourceData(...)` Shape-A drain (3 of 8 candidate sites) against the wave-32 / wave-94 `AccountManagerResources.data(resource:)` facade. 1 file / 6 Edit calls (3 call rewrites + 3 consumer-side `.complete` → `.isComplete` renames) / **first-pass-clean** Bazel 11.7s. Commit `08fc3f721e`. Sites: WallpaperResources:957 (`reference.resource`), :1164 (`fileReference.media.resource`), :1264 (`file.file.resource`). + +**Net delta:** −3 raw `mediaBox.X` accesses, +3 facade calls, +3 `EngineMediaResource(...)` wraps, +3 consumer field renames (`.complete` → `.isComplete` to match `EngineMediaResource.ResourceData`'s renamed field). + +**Deferred (from the original 8-site candidate set):** +- 2 sites in `FetchCachedRepresentations.swift:482, 490` — flow `data: MediaResourceData` into `fetchCachedScaledImageRepresentation` / `fetchCachedBlurredWallpaperRepresentation` (raw-`MediaResourceData`-typed `resourceData:` parameter). Migration would cascade those functions OR require boundary `MediaResourceData` reconstruction. Defer. +- 3 sites in `WallpaperResources.swift:33, 59, 401` — coupled to postbox-side via `combineLatest(accountManager.mediaBox.resourceData(X), account.postbox.mediaBox.resourceData(X))` returning typed `Signal<(MediaResourceData, MediaResourceData), NoError>`. Migrating one side without the other breaks the tuple type. Defer until postbox-side is also drainable or a paired-resource facade is designed. + +**Lesson — "Postbox-typed-function-parameter barrier" pattern (generalized):** wave 103's abandonment introduced this concept for `Message(... peers: SimpleDictionary, ...)` — any helper that builds a `Message` value forces ADD bridges at every `peers[X.id] = X` store. Wave 104 finds the same pattern at the result-type-flow level: `fetchCachedScaled*Representation(resourceData: MediaResourceData)` is a barrier that forces ADD bridges at every site whose closure flows the migrated result into it. Both are instances of "a Postbox-typed function parameter blocks upstream migration of values that flow into it." Pre-flight inventory must enumerate not just type-level uses of the migrated symbol but also the function-parameter-typed barriers it transitively flows into. + +**Lesson — "field rename at wrapper-type boundaries":** `EngineMediaResource.ResourceData.isComplete` is renamed from `MediaResourceData.complete`. Each migrated call site has a paired consumer rename. This is a small but mandatory step the spec must call out per-site; an undocumented rename would surface as a build error referencing a property that doesn't exist on the new wrapper. The rename is verifiable per-site by reading the closure body that consumes the result. + +--- + +## Wave 105 outcome (2026-04-26) + +`DeviceContactInfoSubject` enum-payload Peer? → EnginePeer? migration. 5 files / 17 edits / **first-pass-clean** Bazel 203s (foundational AccountContext touch). Commit `0c76724409`. Wave-91-pattern multi-module enum-payload + completion-callback signature migration. + +**Type changes:** 3 enum case payloads (`Peer?` → `EnginePeer?`), 2 completion-callback signatures (`(Peer?, ...) -> Void` → `(EnginePeer?, ...) -> Void`), 1 computed property (`peer: Peer?` → `peer: EnginePeer?`). All in `AccountContext.swift`. + +**Net delta:** −10 wraps dropped, +2 wraps added (Pattern E ADD bridges at Chat-side construction sites where `peerAndContactData.0` is raw `Peer?`), +1 downcast → case-let conversion. **Net wrap delta: −8.** + +Per-pattern breakdown: +- Pattern A (5 sites): `_asPeer()` drops at construction sites where source is already `EnginePeer?`. DeviceContactInfoController.swift:1289, 1443, 1489 + StoryItemSetContainerViewSendMessage.swift:2132 + OpenChatMessage.swift:443. +- Pattern B (2 sites): `_asPeer()` drops at completion-call sites. DeviceContactInfoController.swift:1105, 1224. +- Pattern C (3 sites): `.flatMap(EnginePeer.init)` simplifications when destructured `peer` is already `EnginePeer?` post-migration. DeviceContactInfoController.swift:942, 944, 946. +- Pattern D (1 site): downcast `as? TelegramUser` → `case let .user(peer)`. DeviceContactInfoController.swift:849. +- Pattern E (2 sites): ADD `.flatMap(EnginePeer.init)` wraps at construction sites where source is raw `Peer?`. ChatControllerOpenAttachmentMenu.swift:683, 1850. + +**Lesson — "thorough pre-flight inventory pays for itself":** wave 105 was the first wave-71-shadow-style (field/payload migration with cascade risk) attempted after the wave-103 abandonment forced a discipline reset. Pre-flight inventory took ~15 minutes (full 4-layer wave-71-shadow-checklist sweep, including verifying 8 destructure sites + 5 callback consumers + 3 `subject.peer` accesses + 12 construction sites across 8 files). The inventory caught the 2 ADD bridges at Chat sites that an Explore-agent pass had initially miscategorized — verified by direct grep of `peerAndContactData` source signal types. Cost-of-care: 15 minutes of pre-flight + 5 minutes of one-line spec fix vs. the wave-103 cost of a build-and-revert cycle. Recommendation: apply the same discipline to every future wave-71-shadow candidate. The inventory cost is a tiny fraction of the abandoned-wave cost. + +**Lesson — "documented ADD bridges are net-positive":** ADD bridges are not always disqualifying. The wave-71-shadow risk feedback emphasizes inventorying them, not avoiding them entirely. When the migration delivers net-negative wrap delta even after accounting for the ADD bridges (here: −10 drops vs. +2 adds = −8 net), the wave is still worth doing. The ADD bridges are also documented in the spec and commit message, so future waves migrating the upstream `peerAndContactData` source can drop them as part of that migration's natural cascade. + +--- + +## Modules currently free of `import Postbox` (running tally) + +Consumer modules that no longer import Postbox, across all waves and standalone commits: + +- `ChatInterfaceState` (wave 1) +- `ChatSendMessageActionUI` (wave 1) +- `ContactListUI` (wave 1) +- `DrawingUI` (wave 1) +- `StickerPeekUI` (standalone cleanup, 2026-04-17 — import was unused) +- `PromptUI` (standalone cleanup) +- `PresentationDataUtils` (standalone cleanup) +- `MapResourceToAvatarSizes` (wave 2) +- `SaveToCameraRoll` (wave 3) +- `SecureIdVerificationDocumentsContext` (wave 5) +- **Wave 6 batch: 189 additional modules** — see `git show 7b2b74e79b --stat` for the commit that swept unused `import Postbox` lines across 183 files in 16 consumer submodules. Not individually enumerated here for brevity. +- `StorageUsageScreen` (waves 8–10) +- `ActionSheetPeerItem` (wave 11; revisits wave-1 abandonment) +- `HorizontalPeerItem` (wave 12; applies wave-11 pattern) +- `SelectablePeerNode` (wave 15; applies wave-11 pattern; ShareExtension-boundary stateManager fallback) +- `ItemListAvatarAndNameInfoItem` (wave 17; applies wave-11 pattern; ShareExtension-boundary stateManager fallback) +- `ItemListStickerPackItem` (wave 18; mixed-shape — 3 narrow TelegramCore typealiases + wave-4 enum-payload migration + wave-3 facade swap) +- `AttachmentTextInputPanelNode` BUILD cleanup (wave 13; source was already clean from wave 6) +- **Wave 14 BUILD-dep sweep: 98 modules' BUILDs cleaned** — same modules as the wave-6 batch; this sweep fixed their leftover `//submodules/Postbox:Postbox` BUILD deps. Candidate list in `/tmp/postbox-dep-candidates.txt` at commit time; derivable by the script in "Wave 14 outcome". diff --git a/docs/superpowers/specs/2026-04-30-typing-draft-send-delay-design.md b/docs/superpowers/specs/2026-04-30-typing-draft-send-delay-design.md new file mode 100644 index 0000000000..09952f55d1 --- /dev/null +++ b/docs/superpowers/specs/2026-04-30-typing-draft-send-delay-design.md @@ -0,0 +1,178 @@ +# Typing-Draft Send Delay — Design + +**Date:** 2026-04-30 +**Component:** `submodules/TelegramCore/Sources/State/PendingMessageManager.swift` (+ minimal Postbox additions) + +## Goal + +Delay outgoing messages while the peer in the same `(peerId, threadId)` is "live-typing" an incoming message (i.e. `Postbox.combinedView(keys: [.typingDrafts(...)])` reports a non-nil draft for that key). Messages park after their content is fully uploaded, then drain in `messageId.id` order once the typing-draft for that key clears. + +## Behavior summary + +- **Scope.** All "deliver-now" outgoing message types: regular text/media single sends, grouped media albums, and forwards. Excluded: scheduled messages, secret-chat messages, and Saved Messages (account-self peer). +- **Pipeline.** Uploads run in parallel as they do today. The gate sits between "upload complete" and the actual MTProto send call. +- **Release.** As soon as the typing-draft for the message's `(peerId, threadId)` clears (the view's set no longer contains that key) — no extra grace delay, no upper-bound timeout. +- **Keying.** Strictly per-thread. `threadId == nil` is the normal value for non-threaded chats and gates with `PeerAndThreadId(peerId: ..., threadId: nil)`. The `Message.newTopicThreadId` sentinel does not gate (already handled by `.waitingForNewTopic`). +- **Always-on.** No preference toggle. +- **Composes with paid-message postpone.** Paid postpone gates upload-start; typing-draft gate gates post-upload send. Both must be clear before send. + +## Architecture + +All logic lives in `PendingMessageManager`. Postbox gains one new public view; no other Postbox API changes. + +### Postbox additions + +1. New file `submodules/Postbox/Sources/AllTypingDraftsView.swift`: + - `MutableAllTypingDraftsView: MutablePostboxView` + - `init(postbox:)` seeds `keys` from `postbox.currentTypingDrafts.keys`. + - `replay(postbox:transaction:)` diffs against `transaction.updatedTypingDrafts`: insert key when `update.value != nil`, remove when nil. Returns `true` if the set changed. + - `refreshDueToExternalTransaction(postbox:)` reloads from `postbox.currentTypingDrafts.keys` and returns `true`. + - `immutableView()` returns an `AllTypingDraftsView`. + - `public final class AllTypingDraftsView: PostboxView` exposes `public let keys: Set`. +2. `submodules/Postbox/Sources/Views.swift`: + - Add `case allTypingDrafts` to `PostboxViewKey` (no associated payload). + - Wire constant `Hashable` combine and `==` matching for the new case. + - Add the `case .allTypingDrafts` arm to `postboxViewForKey` returning `MutableAllTypingDraftsView(postbox:)`. +3. `PostboxImpl.currentTypingDrafts` is `fileprivate(set)`, accessible from view files in the same module. No new accessor needed. + +### PendingMessageManager additions + +New `PendingMessageState` case: + +```swift +case waitingForSendGate(groupId: Int64?, content: PendingMessageUploadedContentAndReuploadInfo) +``` + +Added to `PendingMessageState.groupId`'s switch. Excluded from `updatePendingMediaUploads`'s upload-progress aggregation. + +New stored state on the manager: + +```swift +private var liveTypingDraftKeys: Set = [] +private let allTypingDraftsDisposable = MetaDisposable() +private var forwardSendGateGroups: [PeerAndThreadId: [[(PendingMessageContext, Message, ForwardSourceInfoAttribute)]]] = [:] +``` + +In `init`, subscribe once: + +```swift +self.allTypingDraftsDisposable.set( + (postbox.combinedView(keys: [.allTypingDrafts]) + |> deliverOn(self.queue)).start(next: { [weak self] view in + self?.handleLiveTypingDraftsUpdate(view) + }) +) +``` + +Dispose in `deinit`. + +## Gate predicate + +```swift +private func isSendGateOpen(for key: PeerAndThreadId) -> Bool { + return !self.liveTypingDraftKeys.contains(key) +} + +private func shouldGateSend(messageId: MessageId, threadId: Int64?) -> Bool { + if messageId.namespace == Namespaces.Message.ScheduledCloud { return false } + if messageId.peerId.namespace == Namespaces.Peer.SecretChat { return false } + if messageId.peerId == self.accountPeerId { return false } + if threadId == Message.newTopicThreadId { return false } + return true +} +``` + +A pending context is gate-applicable if `shouldGateSend(...)` returns true. The gate is open if `isSendGateOpen(...)` returns true. Sites delay the send only when `shouldGateSend && !isSendGateOpen`. + +## Gate insertion points + +### (a) Single-message — `beginSendingMessage(messageContext:messageId:groupId:content:)` + +Today: `groupId == nil → commitSendingSingleMessage`; otherwise `state = .waitingToBeSent(groupId: ..., content: ...)`. + +New: when `groupId == nil`, additionally check the gate: + +```swift +let key = PeerAndThreadId(peerId: messageId.peerId, threadId: messageContext.threadId) +if shouldGateSend(messageId: messageId, threadId: messageContext.threadId) && !isSendGateOpen(for: key) { + messageContext.state = .waitingForSendGate(groupId: nil, content: content) +} else { + self.commitSendingSingleMessage(messageContext: messageContext, messageId: messageId, content: content) +} +``` + +The grouped path (`groupId != nil`) is unchanged here; gating for albums happens in (b). + +### (b) Grouped-album — `commitSendingMessageGroup(groupId:messages:)` + +Today: flips every group context to `.sending(groupId:)`, fires `sendGroupMessagesContent`. + +New: derive a representative key from the first message's `(peerId, threadId)`. (Group members share both by construction.) If `shouldGateSend && !isSendGateOpen`, flip every group context to `.waitingForSendGate(groupId: groupId, content: )` and return. Otherwise unchanged. + +`dataForPendingMessageGroup(_ groupId:)` is updated to recognize `.waitingForSendGate(groupId: contextGroupId, ...)` the same way it recognizes `.waitingToBeSent` — i.e. a group becomes "ready" when every member is in `.waitingToBeSent` OR `.waitingForSendGate`. This prevents partial-park deadlocks. + +### (c) Forwards — inside `beginSendingMessages`, lines 714–733 + +Today: builds `countedMessageGroups` and immediately fires `sendGroupMessagesContent` per group. + +The pre-existing `messagesToForward` bucketing is by `PeerIdAndNamespace` only — not by `threadId`. The downstream `sendGroupMessagesContent` network call requires thread homogeneity (a forward dispatch targets a single destination thread), so in practice every group already shares `threadId`. The gate uses this assumption: derive the key from `messages[0].1.threadId` of each `countedMessageGroup`. If a future caller violates the assumption, the existing dispatch path is already broken. + +New: per group, derive `key = PeerAndThreadId(peerId: messages[0].1.id.peerId, threadId: messages[0].1.threadId)`. If `shouldGateSend && !isSendGateOpen`, flip every context in the group to `.waitingForSendGate(groupId: nil, content: PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil, cacheReferenceKey: nil))` and append the entire `[(PendingMessageContext, Message, ForwardSourceInfoAttribute)]` group to `forwardSendGateGroups[key]`. Otherwise fire as today. + +Forward groups within a key drain in FIFO order. + +## Drain logic + +`drainSendGate(key: PeerAndThreadId)` runs on `self.queue`. Idempotent. + +1. **Single-message drain.** Snapshot `messageContexts` filtering on `state == .waitingForSendGate(groupId: nil, ...)` AND `PeerAndThreadId(peerId: contextId.peerId, threadId: context.threadId) == key`. Sort by `messageId.id` ascending. For each, extract the parked `content`, call `commitSendingSingleMessage(messageContext:messageId:content:)`. +2. **Grouped-album drain.** Collect distinct `groupId`s among `.waitingForSendGate(groupId: , ...)` contexts whose key matches. Iterate in ascending min-`messageId.id`-in-group order. For each, call `dataForPendingMessageGroup(groupId)` (which now sees the parked members as ready) and pass the result to `commitSendingMessageGroup(groupId:messages:)`. +3. **Forward drain.** Pop `forwardSendGateGroups.removeValue(forKey: key)`. For each parked group (FIFO): flip every context to `.sending(groupId: nil)`, build the `[(MessageId, PendingMessageUploadedContentAndReuploadInfo)]` array, fire `sendGroupMessagesContent` exactly mirroring the existing forward-fire code (lines 719–731). +4. After (1)–(3), call `updateWaitingUploads(peerId: key.peerId)` and `updatePendingMediaUploads()` once. + +`handleLiveTypingDraftsUpdate(_ view: CombinedView)`: + +```swift +let view = (view.views[.allTypingDrafts] as? AllTypingDraftsView) +let new = view?.keys ?? [] +let cleared = self.liveTypingDraftKeys.subtracting(new) +self.liveTypingDraftKeys = new +for key in cleared { + self.drainSendGate(key: key) +} +``` + +Single-emission semantics: a `(false → true)` transition (key newly populated) parks future arrivals only; in-flight `.sending` continues. A `(true → false)` transition fires drain. + +## Side effects on existing helpers + +- `PendingMessageState.groupId` switch (line 37): add `.waitingForSendGate` case returning the case's `groupId` (forward parking uses `groupId == nil`; grouped-album parking uses the real groupId). +- `updatePendingMediaUploads()` (line 262): `.waitingForSendGate` is **not** treated as uploading. Excluded from the switch (or explicitly returns `default` early). +- `dataForPendingMessageGroup(_ groupId:)` (line 753): add `.waitingForSendGate(contextGroupId, content)` arm — if `contextGroupId == groupId`, append `(context, id, content)` to result, same as the existing `.waitingToBeSent` arm. +- `updatePendingMessageIds(_:)` (line 284): in the existing `for id in removedMessageIds` loop, additionally drop `forwardSendGateGroups[*]` entries whose contained context matches `id`. (Single/album parking is auto-cleaned because parked state lives on the context, which gets `state = .none`.) Implementation: walk the dict, filter out the removed context from each parked group, drop any group that empties out, drop any key whose value-array empties out. + +## Edge cases + +- **First-emit race.** `liveTypingDraftKeys` initializes to `[]`. If a send is attempted before the first view emit and a draft is actually active, that single message slips through. Tolerated. +- **Saved Messages / secret chats / scheduled / new-topic sentinel.** All explicit skip-cases in `shouldGateSend`. +- **Self-typing on another device.** A draft we authored on another device is treated like any other — our outgoing send to that chat parks until it clears. This is consistent with the design intent (drafts visibly commit before subsequent sends arrive). No author filter. +- **Removed-while-parked.** Handled by `updatePendingMessageIds(_:)` extension above. +- **Re-entrancy.** Drain helpers snapshot work-lists before iterating, so mid-iteration mutations to `messageContexts` (e.g. a fired send completes synchronously) don't corrupt the loop. +- **Paid postpone composition.** Paid postpone gates upload-start; once paid commit fires, upload runs; once upload completes, the typing-draft gate parks at `.waitingForSendGate`; once the draft clears, send fires. Stacked sequentially without interaction. +- **Subscription teardown.** `allTypingDraftsDisposable.dispose()` in `deinit`. + +## Testing + +This codebase has no unit tests. Verification is via full build + manual exercise: + +- Build: `python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError` (prefixed with `source ~/.zshrc 2>/dev/null;`). +- Manual: in a 1:1 chat with another device, induce a live-typing draft on the peer side and confirm an outgoing text send parks (chat shows "sending" status held until draft clears or commits). Repeat for: media single send, grouped media album, forward. +- Negative manual: scheduled message — confirm not gated. Saved Messages — confirm not gated. Secret chat — confirm not gated. + +## Out of scope + +- Per-chat opt-in toggle. +- Upper-bound timeout / fallback send. +- Grace-delay after draft clears. +- UI affordance ("waiting for X to finish typing…"). +- Filtering self-authored drafts. diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 99ffead8cf..2245bafe09 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -111,18 +111,18 @@ public enum TextLinkItem: Equatable { public final class AccountWithInfo: Equatable { public let account: Account - public let peer: Peer - - public init(account: Account, peer: Peer) { + public let peer: EnginePeer + + public init(account: Account, peer: EnginePeer) { self.account = account self.peer = peer } - + public static func ==(lhs: AccountWithInfo, rhs: AccountWithInfo) -> Bool { if lhs.account !== rhs.account { return false } - if !arePeersEqual(lhs.peer, rhs.peer) { + if lhs.peer != rhs.peer { return false } return true @@ -225,6 +225,7 @@ public struct ResolvedBotAdminRights: OptionSet { public static let manageVideoChats = ResolvedBotAdminRights(rawValue: 512) public static let canBeAnonymous = ResolvedBotAdminRights(rawValue: 1024) public static let manageChat = ResolvedBotAdminRights(rawValue: 2048) + public static let manageTopics = ResolvedBotAdminRights(rawValue: 4096) public var chatAdminRights: TelegramChatAdminRightsFlags? { var flags = TelegramChatAdminRightsFlags() @@ -259,6 +260,9 @@ public struct ResolvedBotAdminRights: OptionSet { if self.contains(ResolvedBotAdminRights.canBeAnonymous) { flags.insert(.canBeAnonymous) } + if self.contains(ResolvedBotAdminRights.manageTopics) { + flags.insert(.canManageTopics) + } if flags.isEmpty && !self.contains(ResolvedBotAdminRights.manageChat) { return nil @@ -328,6 +332,7 @@ public enum ResolvedUrl { case unknownDeepLink(path: String) case oauth(url: String) case createBot(parentBot: PeerId, username: String?, title: String?) + case textStyle(style: TelegramComposeAIMessageMode.CloudStyle.Custom, initialPreview: AIMessageStylePreview?) public enum ResolvedCollectible { case gift(StarGift.UniqueGift) @@ -403,18 +408,6 @@ public final class ChatPeekTimeout { } } -public final class ChatPeerNearbyData: Equatable { - public static func == (lhs: ChatPeerNearbyData, rhs: ChatPeerNearbyData) -> Bool { - return lhs.distance == rhs.distance - } - - public let distance: Int32 - - public init(distance: Int32) { - self.distance = distance - } -} - public final class ChatGreetingData: Equatable { public static func == (lhs: ChatGreetingData, rhs: ChatGreetingData) -> Bool { return lhs.uuid == rhs.uuid @@ -582,7 +575,6 @@ public final class NavigateToChatControllerParams { public let scrollToEndIfExists: Bool public let activateMessageSearch: (ChatSearchDomain, String)? public let peekData: ChatPeekTimeout? - public let peerNearbyData: ChatPeerNearbyData? public let reportReason: NavigateToChatControllerParams.ReportReason? public let animated: Bool public let forceAnimatedScroll: Bool @@ -618,7 +610,6 @@ public final class NavigateToChatControllerParams { scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, - peerNearbyData: ChatPeerNearbyData? = nil, reportReason: NavigateToChatControllerParams.ReportReason? = nil, animated: Bool = true, forceAnimatedScroll: Bool = false, @@ -653,7 +644,6 @@ public final class NavigateToChatControllerParams { self.scrollToEndIfExists = scrollToEndIfExists self.activateMessageSearch = activateMessageSearch self.peekData = peekData - self.peerNearbyData = peerNearbyData self.reportReason = reportReason self.animated = animated self.forceAnimatedScroll = forceAnimatedScroll @@ -691,7 +681,6 @@ public final class NavigateToChatControllerParams { scrollToEndIfExists: self.scrollToEndIfExists, activateMessageSearch: self.activateMessageSearch, peekData: self.peekData, - peerNearbyData: self.peerNearbyData, reportReason: self.reportReason, animated: self.animated, forceAnimatedScroll: self.forceAnimatedScroll, @@ -712,11 +701,11 @@ public final class NavigateToChatControllerParams { } public enum DeviceContactInfoSubject { - case vcard(Peer?, DeviceContactStableId?, DeviceContactExtendedData) - case filter(peer: Peer?, contactId: DeviceContactStableId?, contactData: DeviceContactExtendedData, completion: (Peer?, DeviceContactExtendedData) -> Void) - case create(peer: Peer?, contactData: DeviceContactExtendedData, isSharing: Bool, shareViaException: Bool, completion: (Peer?, DeviceContactStableId, DeviceContactExtendedData) -> Void) - - public var peer: Peer? { + case vcard(EnginePeer?, DeviceContactStableId?, DeviceContactExtendedData) + case filter(peer: EnginePeer?, contactId: DeviceContactStableId?, contactData: DeviceContactExtendedData, completion: (EnginePeer?, DeviceContactExtendedData) -> Void) + case create(peer: EnginePeer?, contactData: DeviceContactExtendedData, isSharing: Bool, shareViaException: Bool, completion: (EnginePeer?, DeviceContactStableId, DeviceContactExtendedData) -> Void) + + public var peer: EnginePeer? { switch self { case let .vcard(peer, _, _): return peer @@ -748,7 +737,7 @@ public enum PeerInfoControllerMode { case generic case calls(messages: [Message]) case nearbyPeer(distance: Int32) - case group(PeerId) + case group(sourceMessageId: MessageId) case reaction(MessageId) case forumTopic(thread: ChatReplyThreadMessage) case recommendedChannels @@ -1319,6 +1308,14 @@ public final class TextProcessingScreenSendContextActions { public enum TextProcessingScreenMode { case edit(saveRestoreStateId: EnginePeer.Id?, completion: (TextWithEntities) -> Void, send: ((TextWithEntities) -> Void)?, sendContextActions: TextProcessingScreenSendContextActions?) case translate(fromLanguage: String?) + case preview(style: TelegramComposeAIMessageMode.CloudStyle.Custom, authorPeer: EnginePeer?, initialPreview: AIMessageStylePreview?, isAlreadyAdded: Bool, added: () -> Void) +} + +public enum EmojiStatusSelectionControllerMode { + case statusSelection + case backgroundSelection(completion: (TelegramMediaFile?) -> Void) + case customStatusSelection(completion: (TelegramMediaFile?, Int32?) -> Void) + case quickReactionSelection(completion: () -> Void) } public protocol SharedAccountContext: AnyObject { @@ -1371,7 +1368,7 @@ public protocol SharedAccountContext: AnyObject { func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, chatLocationContextHolder: Atomic, tag: HistoryViewInputTag?) -> Signal<(MessageIndex?, Bool), NoError> func makeOverlayAudioPlayerController(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController - func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController? + func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController? func makeChannelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant) -> ViewController? func makeDeviceContactInfoController(context: ShareControllerAccountContext, environment: ShareControllerEnvironment, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController func makePeersNearbyController(context: AccountContext) -> ViewController @@ -1401,14 +1398,14 @@ public protocol SharedAccountContext: AnyObject { func makeProxySettingsController(context: AccountContext) -> ViewController func makeLocalizationListController(context: AccountContext) -> ViewController func makeCreateGroupController(context: AccountContext, peerIds: [PeerId], initialTitle: String?, mode: CreateGroupMode, completion: ((PeerId, @escaping () -> Void) -> Void)?) -> ViewController - func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController + func makeChatRecentActionsController(context: AccountContext, peer: EnginePeer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController func makePrivacyAndSecurityController(context: AccountContext) -> ViewController func makeBioPrivacyController(context: AccountContext, settings: Promise, present: @escaping (ViewController) -> Void) func makeBirthdayPrivacyController(context: AccountContext, settings: Promise, openedFromBirthdayScreen: Bool, present: @escaping (ViewController) -> Void) func makeSetupTwoFactorAuthController(context: AccountContext) -> ViewController func makeStorageManagementController(context: AccountContext) -> ViewController func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, audio: Bool, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, presentDocumentScanner: (() -> Void)?, send: @escaping ([AnyMediaReference], Bool, Int32?, NSAttributedString?) -> Void) -> AttachmentFileController - func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, hasTimer: Bool, customEmojiAvailable: Bool, pushViewController: @escaping (ViewController) -> Void, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? + func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, hasTimer: Bool, customEmojiAvailable: Bool, pushViewController: @escaping (ViewController) -> Void, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void, getNavigationController: @escaping () -> NavigationController?) -> NSObject? func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, stories: Bool, forceDark: Bool) -> ViewController func makeStorySearchController(context: AccountContext, scope: StorySearchControllerScope, listContext: SearchStoryListContext?) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController @@ -1445,7 +1442,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) @@ -1456,12 +1453,12 @@ public protocol SharedAccountContext: AnyObject { completion: @escaping (UIImage?) -> Void, completedWithUploadingImage: @escaping (UIImage, Signal) -> UIView? ) - func openAddPeerMembers(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, parentController: ViewController, groupPeer: Peer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable) + func openAddPeerMembers(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, parentController: ViewController, groupPeer: EnginePeer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable) func makeInstantPageController(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController? func makeInstantPageController(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController - func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController + func makeChatQrCodeScreen(context: AccountContext, peer: EnginePeer, threadId: Int64?, temporary: Bool) -> ViewController func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, proceed: (() -> Void)?) -> ViewController func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController @@ -1487,6 +1484,7 @@ public protocol SharedAccountContext: AnyObject { func makeInstalledStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, forceTheme: PresentationTheme?) -> ViewController func makeChannelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, boosts: Bool, boostStatus: ChannelBoostStatus?) -> ViewController func makeMessagesStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, messageId: EngineMessage.Id) -> ViewController + func makePollStatsScreen(context: AccountContext, messageId: EngineMessage.Id) -> ViewController func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, targetPeerId: EnginePeer.Id?, customTheme: PresentationTheme?, completion: @escaping (Int64) -> Void) -> ViewController @@ -1519,7 +1517,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 @@ -1550,7 +1548,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 @@ -1574,6 +1572,7 @@ public protocol SharedAccountContext: AnyObject { openAutomatically: Bool, completion: @escaping (EnginePeer.Id?) -> Void ) async -> ViewController? + func makeEmojiStatusSelectionController(context: AccountContext, mode: EmojiStatusSelectionControllerMode, sourceView: UIView, emojiContent: Signal, currentSelection: Int64?, color: UIColor?, destinationItemView: @escaping () -> UIView?) -> ViewController func navigateToCurrentCall() var hasOngoingCall: ValuePromise { get } diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 12b6591828..496e3c3b11 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -66,6 +66,9 @@ public final class ChatMessageItemAssociatedData: Equatable { public let showSensitiveContent: Bool public let isSuspiciousPeer: Bool public let showTextAsPlaceholder: Bool + public let accountCountry: String? + public let isParticipant: Bool + public let invitedOn: Int32? public init( automaticDownloadPeerType: MediaAutoDownloadPeerType, @@ -102,7 +105,10 @@ public final class ChatMessageItemAssociatedData: Equatable { isInline: Bool = false, showSensitiveContent: Bool = false, isSuspiciousPeer: Bool = false, - showTextAsPlaceholder: Bool = false + showTextAsPlaceholder: Bool = false, + accountCountry: String? = nil, + isParticipant: Bool = false, + invitedOn: Int32? = nil ) { self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadPeerId = automaticDownloadPeerId @@ -139,6 +145,9 @@ public final class ChatMessageItemAssociatedData: Equatable { self.showSensitiveContent = showSensitiveContent self.isSuspiciousPeer = isSuspiciousPeer self.showTextAsPlaceholder = showTextAsPlaceholder + self.accountCountry = accountCountry + self.isParticipant = isParticipant + self.invitedOn = invitedOn } public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool { @@ -235,6 +244,15 @@ public final class ChatMessageItemAssociatedData: Equatable { if lhs.isSuspiciousPeer != rhs.isSuspiciousPeer { return false } + if lhs.accountCountry != rhs.accountCountry { + return false + } + if lhs.isParticipant != rhs.isParticipant { + return false + } + if lhs.invitedOn != rhs.invitedOn { + return false + } return true } } @@ -1099,6 +1117,7 @@ public protocol ChatController: ViewController { func activateSearch(domain: ChatSearchDomain, query: String) func activateInput(type: ChatControllerActivateInput) func beginClearHistory(type: InteractiveHistoryClearingType) + func presentReactionDeletionOptions(author: Peer, messageId: MessageId) func performScrollToTop() -> Bool func transferScrollingVelocity(_ velocity: CGFloat) diff --git a/submodules/AccountContext/Sources/ContactSelectionController.swift b/submodules/AccountContext/Sources/ContactSelectionController.swift index 54d5cb44ad..21c60a36d4 100644 --- a/submodules/AccountContext/Sources/ContactSelectionController.swift +++ b/submodules/AccountContext/Sources/ContactSelectionController.swift @@ -9,7 +9,7 @@ public protocol ContactSelectionController: ViewController { var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?, ChatSendMessageActionSheetController.SendParameters?)?, NoError> { get } var displayProgress: Bool { get set } var dismissed: (() -> Void)? { get set } - var presentScheduleTimePicker: (@escaping (Int32, Int32?) -> Void) -> Void { get set } + var presentScheduleTimePicker: (@escaping (Int32, Int32?, Bool) -> Void) -> Void { get set } func dismissSearch() } @@ -59,9 +59,9 @@ public enum ContactListAction: Equatable { } public enum ContactListPeer: Equatable { - case peer(peer: Peer, isGlobal: Bool, participantCount: Int32?) + case peer(peer: EnginePeer, isGlobal: Bool, participantCount: Int32?) case deviceContact(DeviceContactStableId, DeviceContactBasicData) - + public var id: ContactListPeerId { switch self { case let .peer(peer, _, _): @@ -70,8 +70,8 @@ public enum ContactListPeer: Equatable { return .deviceContact(id) } } - - public var indexName: PeerIndexNameRepresentation { + + public var indexName: EnginePeer.IndexName { switch self { case let .peer(peer, _, _): return peer.indexName @@ -79,11 +79,11 @@ public enum ContactListPeer: Equatable { return .personName(first: contact.firstName, last: contact.lastName, addressNames: [], phoneNumber: "") } } - + public static func ==(lhs: ContactListPeer, rhs: ContactListPeer) -> Bool { switch lhs { case let .peer(lhsPeer, lhsIsGlobal, lhsParticipantCount): - if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs, lhsPeer.isEqual(rhsPeer), lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount { + if case let .peer(rhsPeer, rhsIsGlobal, rhsParticipantCount) = rhs, lhsPeer == rhsPeer, lhsIsGlobal == rhsIsGlobal, lhsParticipantCount == rhsParticipantCount { return true } else { return false diff --git a/submodules/AccountContext/Sources/FetchMediaUtils.swift b/submodules/AccountContext/Sources/FetchMediaUtils.swift index d23937221a..805ae7a967 100644 --- a/submodules/AccountContext/Sources/FetchMediaUtils.swift +++ b/submodules/AccountContext/Sources/FetchMediaUtils.swift @@ -78,7 +78,8 @@ public func messageMediaFileStatus(context: AccountContext, messageId: MessageId var thumbnailStatus: Signal = .single(nil) if let videoThumbnail = file.videoThumbnails.first { - thumbnailStatus = context.account.postbox.mediaBox.resourceStatus(videoThumbnail.resource) + thumbnailStatus = context.engine.resources.status(resource: EngineMediaResource(videoThumbnail.resource)) + |> map { $0._asStatus() } |> map(Optional.init) } 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/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index cfce7d1d14..359d11a4ba 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -45,7 +45,7 @@ public enum PremiumIntroSource { case todo case copyProtection case aiTools - case auth(String) + case auth(String, Int32) case premiumGift(TelegramMediaFile) } 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/ActionSheetPeerItem/BUILD b/submodules/ActionSheetPeerItem/BUILD index d3aa0cc103..cacfba4843 100644 --- a/submodules/ActionSheetPeerItem/BUILD +++ b/submodules/ActionSheetPeerItem/BUILD @@ -11,7 +11,6 @@ swift_library( ], deps = [ "//submodules/TelegramCore:TelegramCore", - "//submodules/Postbox", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", "//submodules/AvatarNode:AvatarNode", diff --git a/submodules/ActionSheetPeerItem/Sources/ActionSheetPeerItem.swift b/submodules/ActionSheetPeerItem/Sources/ActionSheetPeerItem.swift index c918a8712b..417ae1956f 100644 --- a/submodules/ActionSheetPeerItem/Sources/ActionSheetPeerItem.swift +++ b/submodules/ActionSheetPeerItem/Sources/ActionSheetPeerItem.swift @@ -3,15 +3,13 @@ import UIKit import AsyncDisplayKit import Display import TelegramCore -import Postbox import TelegramPresentationData import AvatarNode import AccountContext public class ActionSheetPeerItem: ActionSheetItem { public let accountPeerId: EnginePeer.Id - public let postbox: Postbox - public let network: Network + public let stateManager: AccountStateManager public let contentSettings: ContentSettings public let peer: EnginePeer public let theme: PresentationTheme @@ -19,12 +17,11 @@ public class ActionSheetPeerItem: ActionSheetItem { public let isSelected: Bool public let strings: PresentationStrings public let action: () -> Void - + public convenience init(context: AccountContext, peer: EnginePeer, title: String, isSelected: Bool, strings: PresentationStrings, theme: PresentationTheme, action: @escaping () -> Void) { self.init( accountPeerId: context.account.peerId, - postbox: context.account.postbox, - network: context.account.network, + stateManager: context.account.stateManager, contentSettings: context.currentContentSettings.with { $0 }, peer: peer, title: title, @@ -34,11 +31,10 @@ public class ActionSheetPeerItem: ActionSheetItem { action: action ) } - + public init( accountPeerId: EnginePeer.Id, - postbox: Postbox, - network: Network, + stateManager: AccountStateManager, contentSettings: ContentSettings, peer: EnginePeer, title: String, @@ -48,8 +44,7 @@ public class ActionSheetPeerItem: ActionSheetItem { action: @escaping () -> Void ) { self.accountPeerId = accountPeerId - self.postbox = postbox - self.network = network + self.stateManager = stateManager self.contentSettings = contentSettings self.peer = peer self.title = title @@ -154,7 +149,7 @@ public class ActionSheetPeerItemNode: ActionSheetItemNode { let textColor: UIColor = self.theme.primaryTextColor self.label.attributedText = NSAttributedString(string: item.title, font: defaultFont, textColor: textColor) - self.avatarNode.setPeer(accountPeerId: item.accountPeerId, postbox: item.postbox, network: item.network, contentSettings: item.contentSettings, theme: item.theme, peer: item.peer) + self.avatarNode.setPeer(accountPeerId: item.accountPeerId, postbox: item.stateManager.postbox, network: item.stateManager.network, contentSettings: item.contentSettings, theme: item.theme, peer: item.peer) self.checkNode.isHidden = !item.isSelected diff --git a/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift b/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift index 598e68063a..c00eb947a1 100644 --- a/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift +++ b/submodules/AnimatedAvatarSetNode/Sources/AnimatedAvatarSetNode.swift @@ -91,7 +91,7 @@ private final class ContentNode: ASDisplayNode { self.addSubnode(self.clippedNode) if let peer = peer { - if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: synchronousLoad) { + if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: synchronousLoad) { let image = generateImage(size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.lightGray.cgColor) @@ -328,7 +328,7 @@ public final class AnimatedAvatarSetView: UIView { self.addSubview(self.clippedView) if let peer = peer { - if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: synchronousLoad) { + if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: synchronousLoad) { let image = generateImage(size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.lightGray.cgColor) 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/AttachmentTextInputPanelNode/BUILD b/submodules/AttachmentTextInputPanelNode/BUILD index 25e0d545af..095e118ae6 100644 --- a/submodules/AttachmentTextInputPanelNode/BUILD +++ b/submodules/AttachmentTextInputPanelNode/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TextFormat:TextFormat", @@ -28,6 +27,7 @@ swift_library( "//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState", "//submodules/Pasteboard:Pasteboard", "//submodules/ContextUI:ContextUI", + "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode:ChatEntityKeyboardInputNode", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/ComponentFlow:ComponentFlow", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index fbc3cb7cce..4d6f82e9f1 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -24,6 +24,7 @@ import AnimationCache import MultiAnimationRenderer import TextNodeWithEntities import ChatInputTextNode +import ChatEntityKeyboardInputNode private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) private let minInputFontSize: CGFloat = 5.0 @@ -44,15 +45,15 @@ private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPres } else { result = 31.0 } - + if case .regular = metrics.widthClass { result = max(33.0, result) } - + if glass { result = max(38.0, result) } - + return result } @@ -88,7 +89,7 @@ private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackground return current.3 } } - + let image = generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in context.clear(CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) @@ -103,7 +104,7 @@ private func textInputBackgroundImage(backgroundColor: UIColor?, inputBackground context.setFillColor(UIColor.clear.cgColor) } context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: diameter, height: diameter)) - + if !caption { context.setBlendMode(.normal) context.setStrokeColor(strokeColor.cgColor) @@ -132,31 +133,29 @@ private class CaptionEditableTextNode: ChatInputTextNode { } } -public protocol AttachmentTextInputPanelInputView: UIView { - var insertText: ((NSAttributedString) -> Void)? { get set } - var deleteBackwards: (() -> Void)? { get set } - var switchToKeyboard: (() -> Void)? { get set } - var presentController: ((ViewController) -> Void)? { get set } +private enum AttachmentTextInputMode { + case text + case emoji } final class CustomEmojiContainerView: UIView { private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView? - + private var emojiLayers: [InlineStickerItemLayer.Key: UIView] = [:] - + init(emojiViewProvider: @escaping (ChatTextInputTextCustomEmojiAttribute) -> UIView?) { self.emojiViewProvider = emojiViewProvider - + super.init(frame: CGRect()) } - + required init(coder: NSCoder) { preconditionFailure() } - + func update(emojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)]) { var nextIndexById: [Int64: Int] = [:] - + var validKeys = Set() for (rect, emoji) in emojiRects { let index: Int @@ -166,9 +165,9 @@ final class CustomEmojiContainerView: UIView { index = 0 } nextIndexById[emoji.fileId] = index + 1 - + let key = InlineStickerItemLayer.Key(id: emoji.fileId, index: index) - + let view: UIView if let current = self.emojiLayers[key] { view = current @@ -179,14 +178,14 @@ final class CustomEmojiContainerView: UIView { } else { continue } - + let size = CGSize(width: 24.0, height: 24.0) - + view.frame = CGRect(origin: CGPoint(x: floor(rect.midX - size.width / 2.0), y: floor(rect.midY - size.height / 2.0)), size: size) - + validKeys.insert(key) } - + var removeKeys: [InlineStickerItemLayer.Key] = [] for (key, view) in self.emojiLayers { if !validKeys.contains(key) { @@ -203,11 +202,11 @@ final class CustomEmojiContainerView: UIView { private func makeTextInputTheme(context: AccountContext, interfaceState: ChatPresentationInterfaceState) -> ChatInputTextView.Theme { let lineStyle: ChatInputTextView.Theme.Quote.LineStyle let authorNameColor: UIColor - + if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = peer.info, let nameColor = peer.nameColor { let colors = context.peerNameColors.get(nameColor) authorNameColor = colors.main - + if let secondary = colors.secondary, let tertiary = colors.tertiary { lineStyle = .tripleDashed(mainColor: colors.main, secondaryColor: secondary, tertiaryColor: tertiary) } else if let secondary = colors.secondary { @@ -217,7 +216,7 @@ private func makeTextInputTheme(context: AccountContext, interfaceState: ChatPre } } else if let accountPeerColor = interfaceState.accountPeerColor { authorNameColor = interfaceState.theme.list.itemAccentColor - + switch accountPeerColor.style { case .solid: lineStyle = .solid(color: authorNameColor) @@ -230,14 +229,14 @@ private func makeTextInputTheme(context: AccountContext, interfaceState: ChatPre lineStyle = .solid(color: interfaceState.theme.list.itemAccentColor) authorNameColor = interfaceState.theme.list.itemAccentColor } - + let codeBackgroundColor: UIColor if interfaceState.theme.overallDarkAppearance { codeBackgroundColor = UIColor(white: 1.0, alpha: 0.05) } else { codeBackgroundColor = UIColor(white: 0.0, alpha: 0.05) } - + return ChatInputTextView.Theme( quote: ChatInputTextView.Theme.Quote( background: authorNameColor.withMultipliedAlpha(interfaceState.theme.overallDarkAppearance ? 0.2 : 0.1), @@ -251,14 +250,16 @@ private func makeTextInputTheme(context: AccountContext, interfaceState: ChatPre public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, ASEditableTextNodeDelegate, ChatInputTextNodeDelegate { private let context: AccountContext - + private let glass: Bool private let isCaption: Bool private let isAttachment: Bool - + private let customEmojiAvailable: Bool + private let presentController: (ViewController) -> Void - private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? - + private let presentInGlobalOverlay: (ViewController) -> Void + private let getNavigationController: () -> NavigationController? + public var textPlaceholderNode: ImmediateTextNode private let textInputContainerBackgroundNode: ASImageNode public let textInputContainer: ASDisplayNode @@ -268,7 +269,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS private var oneLineNode: TextNodeWithEntities private var oneLineNodeAttributedText: NSAttributedString? private var oneLineDustNode: InvisibleInkDustNode? - + let textInputBackgroundNode: ASDisplayNode let textInputBackgroundImageNode: ASImageNode private var transparentTextInputBackgroundImage: UIImage? @@ -278,34 +279,49 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS private var aiButton: (button: HighlightTrackingButton, icon: UIImageView)? private var heightDependentAiButtonAlpha: CGFloat = 0.0 public var isAIEnabled: Bool = false - + public var opaqueActionButtons: ASDisplayNode { return self.actionButtons } - + public let inputModeView: ComponentHostView - private var validLayout: (CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool)? - + private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, textFieldMaxHeight: CGFloat, availableHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool)? + private var currentInputMode: AttachmentTextInputMode = .text + private var currentAdditionalInputHeight: CGFloat = 0.0 + private var currentSafeAreaInset: UIEdgeInsets = .zero + private var currentContainerBottomInset: CGFloat = 0.0 + private var usesContainerLayout = false + private var currentIsCaptionAbove = false + private var currentHeight: CGFloat? + private var isTransitioningToTextKeyboard = false + + private let inputMediaNodeDataPromise = Promise() + private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? + private var inputMediaNodeDataDisposable: Disposable? + private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext() + private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction? + private var inputMediaNode: ChatEntityKeyboardInputNode? + public var sendMessage: (AttachmentTextInputPanelSendMode, ChatSendMessageActionSheetController.SendParameters?) -> Void = { _, _ in } public var invokeAICompose: (() -> Void)? public var updateHeight: (Bool) -> Void = { _ in } private var updatingInputState = false - + private var currentPlaceholder: String? - + public var effectivePresentationInterfaceState: (() -> ChatPresentationInterfaceState?)? private var presentationInterfaceState: ChatPresentationInterfaceState? private var initializedPlaceholder = false - + private let inputMenu: TextInputMenu - + private var theme: PresentationTheme? private var strings: PresentationStrings? - + private let hapticFeedback = HapticFeedback() - + public var inputTextState: ChatTextInputState { if let textInputNode = self.textInputNode { let selectionRange: Range = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length) @@ -314,7 +330,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return ChatTextInputState() } } - + var storedInputLanguage: String? var effectiveInputLanguage: String? { if let textInputNode = textInputNode, textInputNode.isFirstResponder() { @@ -323,7 +339,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return self.storedInputLanguage } } - + var enablePredictiveInput: Bool = true { didSet { if let textInputNode = self.textInputNode { @@ -331,28 +347,28 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } } - + public var interfaceInteraction: ChatPanelInterfaceInteraction? - + public func animateIn(transition: ContainedViewLayoutTransition) { self.actionButtons.animateIn(transition: transition) } - + public func updateSendButtonEnabled(_ enabled: Bool, animated: Bool) { self.actionButtons.isUserInteractionEnabled = enabled - + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate transition.updateAlpha(node: self.actionButtons, alpha: enabled ? 1.0 : 0.3) } - + public func updateInputTextState(_ state: ChatTextInputState, animated: Bool) { if state.inputText.length != 0 && self.textInputNode == nil { self.loadTextInputNode() } - + if let textInputNode = self.textInputNode, let _ = self.presentationInterfaceState, !self.skipUpdate { self.updatingInputState = true - + var textColor: UIColor = .black var accentTextColor: UIColor = .blue var baseFontSize: CGFloat = 17.0 @@ -370,7 +386,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self.updateSpoiler() } } - + public var text: String { get { return self.textInputNode?.attributedText?.string ?? "" @@ -387,52 +403,54 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } } - + public func caption() -> NSAttributedString { return self.textInputNode?.attributedText ?? NSAttributedString() } - + private let textInputViewInternalInsets = UIEdgeInsets(top: 1.0, left: 13.0, bottom: 1.0, right: 13.0) - + private var spoilersRevealed = false - + public var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer - + private var maxCaptionLength: Int32? - - public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, glass: Bool = false, isCaption: Bool = false, isAttachment: Bool = false, isScheduledMessages: Bool = false, presentController: @escaping (ViewController) -> Void, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { + + public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, glass: Bool = false, isCaption: Bool = false, isAttachment: Bool = false, isScheduledMessages: Bool = false, customEmojiAvailable: Bool, presentController: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void, getNavigationController: @escaping () -> NavigationController?) { self.context = context self.presentationInterfaceState = presentationInterfaceState self.glass = glass self.isCaption = isCaption self.isAttachment = isAttachment + self.customEmojiAvailable = customEmojiAvailable self.presentController = presentController - self.makeEntityInputView = makeEntityInputView - + self.presentInGlobalOverlay = presentInGlobalOverlay + self.getNavigationController = getNavigationController + self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer - + var hasSpoilers = true - if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat { + if presentationInterfaceState.chatLocation.peerId?.isSecretChat == true { hasSpoilers = false } self.inputMenu = TextInputMenu(hasSpoilers: hasSpoilers) - + self.textInputContainerBackgroundNode = ASImageNode() self.textInputContainerBackgroundNode.isUserInteractionEnabled = false self.textInputContainerBackgroundNode.displaysAsynchronously = false - + self.textInputContainer = ASDisplayNode() if !isCaption && !glass { self.textInputContainer.addSubnode(self.textInputContainerBackgroundNode) } - + self.inputModeView = ComponentHostView() self.textInputContainer.view.addSubview(self.inputModeView) self.textInputContainer.clipsToBounds = true - + self.textInputBackgroundNode = ASDisplayNode() self.textInputBackgroundImageNode = ASImageNode() self.textInputBackgroundImageNode.displaysAsynchronously = false @@ -440,16 +458,16 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self.textPlaceholderNode = ImmediateTextNode() self.textPlaceholderNode.maximumNumberOfLines = 1 self.textPlaceholderNode.isUserInteractionEnabled = false - + self.oneLineNode = TextNodeWithEntities() self.oneLineNode.textNode.isUserInteractionEnabled = false - + self.actionButtons = AttachmentTextInputActionButtonsNode(presentationInterfaceState: presentationInterfaceState, glass: glass, presentController: presentController) self.counterTextNode = ImmediateTextNode() self.counterTextNode.textAlignment = .center - + super.init() - + if !isScheduledMessages { self.actionButtons.sendButtonLongPressed = { [weak self] node, gesture in self?.interfaceInteraction?.displaySendMessageOptions(node, gesture) @@ -458,27 +476,27 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } else { self.actionButtons.sendButtonLongPressEnabled = false } - + self.actionButtons.sendButton.addTarget(self, action: #selector(self.sendButtonPressed), forControlEvents: .touchUpInside) self.actionButtons.sendButton.alpha = 1.0 self.actionButtons.updateAccessibility() - + self.addSubnode(self.textInputContainer) self.addSubnode(self.textInputBackgroundNode) - + if !glass { self.textInputBackgroundNode.addSubnode(self.textInputBackgroundImageNode) } - + self.addSubnode(self.textPlaceholderNode) - + self.addSubnode(self.actionButtons) self.addSubnode(self.counterTextNode) - + if isCaption { self.addSubnode(self.oneLineNode.textNode) } - + self.textInputBackgroundImageNode.clipsToBounds = true let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))) recognizer.touchDown = { [weak self] in @@ -490,7 +508,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS guard let strongSelf = self, let textInputNode = strongSelf.textInputNode else { return true } - + if textInputNode.textView.isFirstResponder { return true } else { @@ -498,17 +516,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } self.textInputBackgroundNode.view.addGestureRecognizer(recognizer) - + self.emojiViewProvider = { [weak self] emoji in guard let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState else { return UIView() } - + return EmojiTextAttachmentView(context: context, userLocation: .other, emoji: emoji, file: emoji.file, cache: strongSelf.animationCache, renderer: strongSelf.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12), pointSize: CGSize(width: 24.0, height: 24.0)) } - + self.updateSendButtonEnabled(isCaption || isAttachment, animated: false) - + if self.isCaption || self.isAttachment { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) |> mapToSignal { peer -> Signal in @@ -525,45 +543,148 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self?.maxCaptionLength = maxCaptionLength }) } + + self.inputMediaNodeDataPromise.set( + ChatEntityKeyboardInputNode.inputData( + context: context, + chatPeerId: nil, + areCustomEmojiEnabled: customEmojiAvailable, + hasTrending: false, + hasStickers: false, + hasGifs: false, + sendGif: nil + ) + ) + self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + self.inputMediaNodeData = value + if case .emoji = self.currentInputMode { + self.requestRelayout(animated: false) + } + }) + self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( + sendSticker: { _, _, _, _, _, _, _, _, _ in + return false + }, + sendEmoji: { _, _, _ in + }, + sendGif: { _, _, _, _, _ in + return false + }, + sendBotContextResultAsGif: { _, _, _, _, _, _ in + return false + }, + editGif: { _, _ in + }, + updateChoosingSticker: { _ in + }, + switchToTextInput: { [weak self] in + self?.activateInput() + }, + dismissTextInput: { + }, + insertText: { [weak self] text in + self?.insertTextFromInputMedia(text) + }, + backwardsDeleteText: { [weak self] in + self?.deleteBackwardsFromInputMedia() + }, + openStickerEditor: { + }, + presentController: { [weak self] controller, _ in + self?.presentController(controller) + }, + presentGlobalOverlayController: { [weak self] controller, _ in + self?.presentInGlobalOverlay(controller) + }, + getNavigationController: { [weak self] in + return self?.getNavigationController() + }, + requestLayout: { [weak self] transition in + self?.requestRelayout(animated: transition.isAnimated) + } + ) + self.inputMediaInteraction?.forceTheme = presentationInterfaceState.theme } - + public var sendPressed: ((NSAttributedString?) -> Void)? public var focusUpdated: ((Bool) -> Void)? public var heightUpdated: ((Bool) -> Void)? public var timerUpdated: ((NSNumber?) -> Void)? public var captionIsAboveUpdated: ((Bool) -> Void)? - + public var additionalInputHeight: CGFloat { + return self.currentAdditionalInputHeight + } + public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat { + self.currentSafeAreaInset = .zero + self.currentContainerBottomInset = 0.0 + self.usesContainerLayout = false guard let presentationInterfaceState = self.presentationInterfaceState else { return 0.0 } - return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false) + return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), textFieldMaxHeight: size.height, availableHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false) } - + + @objc(updateContainerLayoutSize:safeAreaInset:bottomInset:keyboardHeight:animated:) + public func updateContainerLayoutSize(_ size: CGSize, safeAreaInset: UIEdgeInsets, bottomInset: CGFloat, keyboardHeight: CGFloat, animated: Bool) -> CGFloat { + self.currentSafeAreaInset = safeAreaInset + self.currentContainerBottomInset = bottomInset + self.usesContainerLayout = true + guard let presentationInterfaceState = self.presentationInterfaceState else { + return 0.0 + } + return self.updateLayout(width: size.width, leftInset: 0.0, rightInset: 0.0, bottomInset: safeAreaInset.bottom, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), textFieldMaxHeight: size.height, availableHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false) + } + public func setCaption(_ caption: NSAttributedString?) { self.interfaceInteraction?.updateTextInputStateAndMode { state, inputMode in return (ChatTextInputState(inputText: caption ?? NSAttributedString()), inputMode) } } - + public func setTimeout(_ timeout: Int32, isVideo: Bool, isCaptionAbove: Bool) { + self.currentIsCaptionAbove = isCaptionAbove } - + public func animate(_ view: UIView, frame: CGRect) { - + let transition = ComponentTransition.spring(duration: 0.4) + transition.setFrame(view: view, frame: frame) } - + public func onAnimateOut() { } - + public func activateInput() { + self.loadTextInputNodeIfNeeded() + + let wasEmoji = self.currentInputMode == .emoji + self.currentInputMode = .text + if let textInputNode = self.textInputNode { + self.isTransitioningToTextKeyboard = wasEmoji && textInputNode.textView.isFirstResponder + self.applyCurrentInputMode(reload: textInputNode.textView.isFirstResponder) + if !textInputNode.textView.isFirstResponder { + textInputNode.textView.becomeFirstResponder() + } + } + self.requestRelayout(animated: wasEmoji) } - + public func dismissInput() -> Bool { - self.ensureUnfocused() + let wasEmoji = self.currentInputMode == .emoji + if wasEmoji { + self.currentInputMode = .text + self.isTransitioningToTextKeyboard = false + self.requestRelayout(animated: true) + } + self.textInputNode?.resignFirstResponder() + self.applyCurrentInputMode(reload: false) return true } - + public func baseHeight() -> CGFloat { return 45.0 } @@ -571,17 +692,21 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + + deinit { + self.inputMediaNodeDataDisposable?.dispose() + } + public func loadTextInputNodeIfNeeded() { if self.textInputNode == nil { self.loadTextInputNode() } } - + private func loadTextInputNode() { let textInputNode = CaptionEditableTextNode() textInputNode.initialPrimaryLanguage = self.presentationInterfaceState?.interfaceState.inputLanguage - + var textColor: UIColor = .black var tintColor: UIColor = .blue var baseFontSize: CGFloat = 17.0 @@ -591,17 +716,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS tintColor = presentationInterfaceState.theme.list.itemAccentColor baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) keyboardAppearance = presentationInterfaceState.theme.rootController.keyboardColor.keyboardAppearance - + textInputNode.textView.theme = makeTextInputTheme(context: self.context, interfaceState: presentationInterfaceState) } - + let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineSpacing = 1.0 paragraphStyle.lineHeightMultiple = 1.0 paragraphStyle.paragraphSpacing = 1.0 paragraphStyle.maximumLineHeight = 20.0 paragraphStyle.minimumLineHeight = 20.0 - + textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(max(minInputFontSize, baseFontSize)), NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.paragraphStyle: paragraphStyle] textInputNode.clipsToBounds = false textInputNode.textView.clipsToBounds = false @@ -613,27 +738,27 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self.textInputContainer.addSubnode(textInputNode) textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true self.textInputNode = textInputNode - + textInputNode.textView.inputAssistantItem.leadingBarButtonGroups = [] textInputNode.textView.inputAssistantItem.trailingBarButtonGroups = [] - + if let presentationInterfaceState = self.presentationInterfaceState { refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState, glass: self.glass) } - + if !self.textInputContainer.bounds.size.width.isZero { let textInputFrame = self.textInputContainer.frame - + textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) textInputNode.view.layoutIfNeeded() textInputNode.textView.updateLayout(size: textInputNode.bounds.size) self.updateSpoiler() } - + self.textInputBackgroundNode.isUserInteractionEnabled = false self.textInputBackgroundNode.view.removeGestureRecognizer(self.textInputBackgroundNode.view.gestureRecognizers![0]) - + let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))) recognizer.touchDown = { [weak self] in if let strongSelf = self { @@ -644,7 +769,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS guard let strongSelf = self, let textInputNode = strongSelf.textInputNode else { return true } - + if textInputNode.textView.isFirstResponder { return true } else { @@ -652,48 +777,49 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } textInputNode.view.addGestureRecognizer(recognizer) - + textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string + self.applyCurrentInputMode(reload: false) } - + private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat { let textFieldInsets = self.textFieldInsets(metrics: metrics) return max(self.glass ? 40.0 : 33.0, maxHeight - (textFieldInsets.top + textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom)) } - + private func calculateTextFieldMetrics(width: CGFloat, maxHeight: CGFloat, metrics: LayoutMetrics) -> (accessoryButtonsWidth: CGFloat, textFieldHeight: CGFloat) { var textFieldInsets = self.textFieldInsets(metrics: metrics) if self.actionButtons.frame.width > 44.0 { textFieldInsets.right = self.actionButtons.frame.width - 6.0 } let fieldMaxHeight = textFieldMaxHeight(maxHeight, metrics: metrics) - + var textFieldMinHeight: CGFloat = 35.0 var textFieldRealInsets = UIEdgeInsets() if let presentationInterfaceState = self.presentationInterfaceState { textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, glass: self.glass, metrics: metrics) textFieldRealInsets = calculateTextFieldRealInsets(presentationInterfaceState, glass: self.glass) } - + let textFieldHeight: CGFloat if let textInputNode = self.textInputNode { let maxTextWidth = width - textFieldInsets.left - textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - + let measuredHeight = textInputNode.textHeightForWidth(maxTextWidth, rightInset: textFieldRealInsets.right) let unboundTextFieldHeight = max(textFieldMinHeight, ceil(measuredHeight)) - + let maxNumberOfLines = min(12, (Int(fieldMaxHeight - 11.0) - 33) / 22) - + let updatedMaxHeight = (CGFloat(maxNumberOfLines) * (22.0 + 2.0) + 10.0) - + textFieldHeight = max(textFieldMinHeight, min(updatedMaxHeight, unboundTextFieldHeight)) } else { textFieldHeight = textFieldMinHeight } - + return (0.0, textFieldHeight) } - + private func textFieldInsets(metrics: LayoutMetrics) -> UIEdgeInsets { var insets = UIEdgeInsets(top: 6.0, left: 6.0, bottom: 6.0, right: 42.0) if self.glass { @@ -707,13 +833,13 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } return insets } - + private func panelHeight(textFieldHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat { let textFieldInsets = self.textFieldInsets(metrics: metrics) let result = textFieldHeight + textFieldInsets.top + textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom return result } - + func minimalHeight(interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat { let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, glass: self.glass, metrics: metrics) var minimalHeight: CGFloat = 14.0 + textFieldMinHeight @@ -722,7 +848,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } return minimalHeight } - + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { guard self.isUserInteractionEnabled else { return nil @@ -736,38 +862,57 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS if !self.inputModeView.isHidden, let result = self.inputModeView.hitTest(self.view.convert(point, to: self.inputModeView), with: event) { return result } - - return super.hitTest(point, with: event) + if let inputMediaNode = self.inputMediaNode, let result = inputMediaNode.view.hitTest(self.view.convert(point, to: inputMediaNode.view), with: event) { + return result + } + let result = super.hitTest(point, with: event) + if result === self.view { + return nil + } + return result } - + public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: 0.0, additionalSideInsets: additionalSideInsets, textFieldMaxHeight: maxHeight, availableHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) + } + + public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: keyboardHeight, additionalSideInsets: additionalSideInsets, textFieldMaxHeight: maxHeight, availableHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) + } + + public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, textFieldMaxHeight: CGFloat, availableHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { let hadLayout = self.validLayout != nil - let previousAdditionalSideInsets = self.validLayout?.3 - self.validLayout = (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, isSecondary) - + let previousLayout = self.validLayout + self.validLayout = (width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: keyboardHeight, additionalSideInsets: additionalSideInsets, textFieldMaxHeight: textFieldMaxHeight, availableHeight: availableHeight, metrics: metrics, isSecondary: isSecondary) + + let previousAdditionalSideInsets = previousLayout?.additionalSideInsets let leftInset = leftInset + 8.0 let rightInset = rightInset + 8.0 - + var transition = transition - if let previousAdditionalSideInsets = previousAdditionalSideInsets, previousAdditionalSideInsets.right != additionalSideInsets.right { + if keyboardHeight.isZero, let previousKeyboardHeight = previousLayout?.keyboardHeight, previousKeyboardHeight > 0.0, !transition.isAnimated { + transition = .animated(duration: 0.4, curve: .spring) + } + if let previousAdditionalSideInsets, previousAdditionalSideInsets.right != additionalSideInsets.right { if case .animated = transition { transition = .animated(duration: 0.2, curve: .easeInOut) } } - + if self.presentationInterfaceState != interfaceState || !hadLayout { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState - + self.inputMediaInteraction?.forceTheme = interfaceState.theme + let themeUpdated = previousState?.theme !== interfaceState.theme - + var updateSendButtonIcon = false if (previousState?.interfaceState.editMessage != nil) != (interfaceState.interfaceState.editMessage != nil) { updateSendButtonIcon = true } if self.theme !== interfaceState.theme { updateSendButtonIcon = true - + if self.theme == nil || !self.theme!.chat.inputPanel.inputTextColor.isEqual(interfaceState.theme.chat.inputPanel.inputTextColor) { let textColor = interfaceState.theme.chat.inputPanel.inputTextColor let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize) @@ -775,20 +920,20 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS if let textInputNode = self.textInputNode { if let text = textInputNode.attributedText { let selectedRange = textInputNode.selectedRange - let textRange = NSMakeRange(0, (text.string as NSString).length) + let textRange = NSMakeRange(0, (text.string as NSString).length) let updatedText = NSMutableAttributedString(attributedString: text) updatedText.removeAttribute(.foregroundColor, range: textRange) updatedText.addAttribute(.foregroundColor, value: textColor, range: textRange) - + textInputNode.attributedText = updatedText textInputNode.selectedRange = selectedRange } textInputNode.textView.typingAttributes = [NSAttributedString.Key.font: Font.regular(baseFontSize), NSAttributedString.Key.foregroundColor: textColor] - + self.updateSpoiler() } } - + let keyboardAppearance = interfaceState.theme.rootController.keyboardColor.keyboardAppearance if let textInputNode = self.textInputNode, textInputNode.keyboardAppearance != keyboardAppearance, textInputNode.isFirstResponder() { if textInputNode.isCurrentlyEmoji() { @@ -797,36 +942,32 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } textInputNode.keyboardAppearance = keyboardAppearance } - + self.theme = interfaceState.theme - self.actionButtons.updateTheme(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) - + let textFieldMinHeight = calclulateTextFieldMinHeight(interfaceState, glass: self.glass, metrics: metrics) let minimalInputHeight: CGFloat = 2.0 + textFieldMinHeight - + let backgroundColor: UIColor if case let .color(color) = interfaceState.chatWallpaper, UIColor(rgb: color).isEqual(interfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) { backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper } else { backgroundColor = interfaceState.theme.chat.inputPanel.panelBackgroundColor } - + self.textInputBackgroundImageNode.image = textInputBackgroundImage(backgroundColor: backgroundColor, inputBackgroundColor: nil, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight, caption: self.isCaption) self.transparentTextInputBackgroundImage = textInputBackgroundImage(backgroundColor: nil, inputBackgroundColor: interfaceState.theme.chat.inputPanel.inputBackgroundColor, strokeColor: interfaceState.theme.chat.inputPanel.inputStrokeColor, diameter: minimalInputHeight, caption: self.isCaption) self.textInputContainerBackgroundNode.image = generateStretchableFilledCircleImage(diameter: minimalInputHeight, color: interfaceState.theme.chat.inputPanel.inputBackgroundColor) - } else { - if self.strings !== interfaceState.strings { - self.strings = interfaceState.strings - self.inputMenu.updateStrings(interfaceState.strings) - } + } else if self.strings !== interfaceState.strings { + self.strings = interfaceState.strings + self.inputMenu.updateStrings(interfaceState.strings) } - + if themeUpdated || !self.initializedPlaceholder { self.initializedPlaceholder = true - + let placeholder = self.isCaption || self.isAttachment ? interfaceState.strings.MediaPicker_AddCaption : interfaceState.strings.Conversation_InputTextPlaceholder - if self.currentPlaceholder != placeholder || themeUpdated { self.currentPlaceholder = placeholder let baseFontSize = max(minInputFontSize, interfaceState.fontSize.baseDisplaySize) @@ -843,74 +984,76 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize) } } - + let sendButtonHasApplyIcon = self.isCaption || interfaceState.interfaceState.editMessage != nil - - if updateSendButtonIcon { - if !self.actionButtons.animatingSendButton { - let imageNode = self.actionButtons.sendButton.imageNode - - if transition.isAnimated && !self.actionButtons.sendButton.alpha.isZero && self.actionButtons.sendButton.layer.animation(forKey: "opacity") == nil, let previousImage = imageNode.image { - let tempView = UIImageView(image: previousImage) - self.actionButtons.sendButton.view.addSubview(tempView) - tempView.frame = imageNode.frame - tempView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tempView] _ in - tempView?.removeFromSuperview() - }) - tempView.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, removeOnCompletion: false) - - imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - imageNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) - } - self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon - if self.actionButtons.sendButtonHasApplyIcon { - self.actionButtons.setImage(PresentationResourcesChat.chatInputPanelApplyIconImage(interfaceState.theme)) - } else { - self.actionButtons.setImage(PresentationResourcesChat.chatInputPanelSendIconImage(interfaceState.theme)) - } + if updateSendButtonIcon, !self.actionButtons.animatingSendButton { + let imageNode = self.actionButtons.sendButton.imageNode + + if transition.isAnimated && !self.actionButtons.sendButton.alpha.isZero && self.actionButtons.sendButton.layer.animation(forKey: "opacity") == nil, let previousImage = imageNode.image { + let tempView = UIImageView(image: previousImage) + self.actionButtons.sendButton.view.addSubview(tempView) + tempView.frame = imageNode.frame + tempView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak tempView] _ in + tempView?.removeFromSuperview() + }) + tempView.layer.animateScale(from: 1.0, to: 0.2, duration: 0.2, removeOnCompletion: false) + + imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + imageNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.2) + } + self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon + if self.actionButtons.sendButtonHasApplyIcon { + self.actionButtons.setImage(PresentationResourcesChat.chatInputPanelApplyIconImage(interfaceState.theme)) + } else { + self.actionButtons.setImage(PresentationResourcesChat.chatInputPanelSendIconImage(interfaceState.theme)) } } } - + + let isLandscape = width > availableHeight + let deviceMetrics = DeviceMetrics(screenSize: CGSize(width: width, height: availableHeight), scale: UIScreen.main.scale, statusBarHeight: 0.0, onScreenNavigationHeight: nil) + let standardInputHeight = deviceMetrics.standardInputHeight(inLandscape: isLandscape) + if keyboardHeight > 0.0 || !self.isFocused { + self.isTransitioningToTextKeyboard = false + } + var textFieldMinHeight: CGFloat = self.glass ? 40.0 : 33.0 if let presentationInterfaceState = self.presentationInterfaceState { textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, glass: self.glass, metrics: metrics) } let minimalHeight: CGFloat = 14.0 + textFieldMinHeight - + let baseWidth = width - leftInset - rightInset - let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: maxHeight, metrics: metrics) - var panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) - - self.updateCounterTextNode(transition: transition) - + let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: baseWidth, maxHeight: textFieldMaxHeight, metrics: metrics) + var panelContentHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) + var inputHasText = false if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 { inputHasText = true } - + var textFieldInsets = self.textFieldInsets(metrics: metrics) if additionalSideInsets.right > 0.0 { textFieldInsets.right += additionalSideInsets.right / 3.0 } - + var textInputViewRealInsets = UIEdgeInsets() if let presentationInterfaceState = self.presentationInterfaceState { textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState, glass: self.glass) } - + if self.isCaption { if self.isFocused { self.oneLineNode.textNode.alpha = 0.0 self.oneLineDustNode?.alpha = 0.0 self.textInputNode?.alpha = 1.0 - + transition.updateAlpha(node: self.actionButtons, alpha: 1.0) transition.updateTransformScale(node: self.actionButtons, scale: 1.0) transition.updateAlpha(node: self.textInputBackgroundImageNode, alpha: 1.0) } else { - panelHeight = minimalHeight - + panelContentHeight = minimalHeight + transition.updateAlpha(node: self.oneLineNode.textNode, alpha: inputHasText ? 1.0 : 0.0) if let oneLineDustNode = self.oneLineDustNode { transition.updateAlpha(node: oneLineDustNode, alpha: inputHasText ? 1.0 : 0.0) @@ -918,12 +1061,142 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS if let textInputNode = self.textInputNode { transition.updateAlpha(node: textInputNode, alpha: inputHasText ? 0.0 : 1.0) } - + transition.updateAlpha(node: self.actionButtons, alpha: 0.0) transition.updateTransformScale(node: self.actionButtons, scale: 0.001) transition.updateAlpha(node: self.textInputBackgroundImageNode, alpha: inputHasText ? 1.0 : 0.0) } + } + let inputPanelHeight = panelContentHeight + (self.glass ? 11.0 : 0.0) + var totalHeight = inputPanelHeight + var inputMediaHeight: CGFloat = 0.0 + self.currentAdditionalInputHeight = 0.0 + var inputMediaNodeForLayout: ChatEntityKeyboardInputNode? + var isNewInputMediaNode = false + + if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { + let inputMediaNode: ChatEntityKeyboardInputNode + if let current = self.inputMediaNode { + inputMediaNode = current + } else { + isNewInputMediaNode = true + inputMediaNode = ChatEntityKeyboardInputNode( + context: self.context, + currentInputData: inputData, + updatedInputData: self.inputMediaNodeDataPromise.get(), + defaultToEmojiTab: true, + opaqueTopPanelBackground: false, + useOpaqueTheme: false, + interaction: self.inputMediaInteraction, + chatPeerId: nil, + stateContext: self.inputMediaNodeStateContext + ) + inputMediaNode.clipsToBounds = true + inputMediaNode.externalTopPanelContainerImpl = nil + inputMediaNode.useExternalSearchContainer = true + self.inputMediaNode = inputMediaNode + } + + if inputMediaNode.view.superview == nil { + self.view.addSubview(inputMediaNode.view) + } + inputMediaNodeForLayout = inputMediaNode + + let heightAndOverflow = inputMediaNode.updateLayout( + width: width, + leftInset: 0.0, + rightInset: 0.0, + bottomInset: bottomInset, + standardInputHeight: standardInputHeight, + inputHeight: 0.0, + maximumHeight: availableHeight, + inputPanelHeight: 0.0, + transition: .immediate, + interfaceState: interfaceState, + layoutMetrics: metrics, + deviceMetrics: deviceMetrics, + isVisible: true, + isExpanded: false + ) + inputMediaHeight = heightAndOverflow.0 + self.currentAdditionalInputHeight = inputMediaHeight + totalHeight += inputMediaHeight + } else if let inputMediaNode = self.inputMediaNode { + self.inputMediaNode = nil + + if transition.isAnimated { + var dismissingInputHeight = keyboardHeight + if self.isTransitioningToTextKeyboard && dismissingInputHeight.isZero && self.isFocused { + dismissingInputHeight = max(dismissingInputHeight, standardInputHeight) + } + let targetOriginY: CGFloat + if self.usesContainerLayout { + if dismissingInputHeight > 0.0 { + targetOriginY = availableHeight - dismissingInputHeight + } else { + targetOriginY = availableHeight + } + } else if dismissingInputHeight > 0.0 { + targetOriginY = inputPanelHeight + } else { + targetOriginY = inputPanelHeight + inputMediaNode.frame.height + } + let targetFrame = CGRect(origin: CGPoint(x: inputMediaNode.frame.minX, y: targetOriginY), size: inputMediaNode.frame.size) + transition.updateFrame(view: inputMediaNode.view, frame: targetFrame) + inputMediaNode.view.layer.animateAlpha(from: inputMediaNode.view.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in + inputMediaNode?.view.removeFromSuperview() + }) + } else { + inputMediaNode.view.removeFromSuperview() + } + } + + var retainedInputHeight = keyboardHeight + var shouldRetainHiddenInputHeight = false + if self.isFocused { + if case .emoji = self.currentInputMode { + retainedInputHeight = max(retainedInputHeight, standardInputHeight) + shouldRetainHiddenInputHeight = true + } else if self.isTransitioningToTextKeyboard && retainedInputHeight.isZero { + retainedInputHeight = max(retainedInputHeight, standardInputHeight) + shouldRetainHiddenInputHeight = true + } + } + if self.currentAdditionalInputHeight.isZero && retainedInputHeight > 0.0 && shouldRetainHiddenInputHeight { + self.currentAdditionalInputHeight = retainedInputHeight + totalHeight += retainedInputHeight + } + + let isLandscapePhone = width > availableHeight && UIDevice.current.userInterfaceIdiom != .pad + let collapsedCaptionTopInset = self.currentSafeAreaInset.top + 48.0 + let expandedCaptionTopInset = self.currentSafeAreaInset.top + 8.0 + + var panelOriginY: CGFloat = 0.0 + var inputMediaFrame = CGRect(origin: CGPoint(x: 0.0, y: inputPanelHeight), size: CGSize(width: width, height: inputMediaHeight)) + if self.usesContainerLayout { + if isLandscapePhone { + panelOriginY = availableHeight + 16.0 + inputMediaFrame.origin.y = availableHeight + 16.0 + } else if case .emoji = self.currentInputMode { + inputMediaFrame.origin.y = availableHeight - inputMediaHeight + if self.currentIsCaptionAbove { + panelOriginY = expandedCaptionTopInset + } else { + panelOriginY = inputMediaFrame.minY - inputPanelHeight + } + } else { + if self.currentIsCaptionAbove { + panelOriginY = (retainedInputHeight > 0.0 ? expandedCaptionTopInset : collapsedCaptionTopInset) + } else { + let bottomOffset = max(self.currentContainerBottomInset, retainedInputHeight) + panelOriginY = availableHeight - inputPanelHeight - bottomOffset + } + inputMediaFrame.origin.y = availableHeight + } + } + + if self.isCaption { let makeOneLineLayout = TextNodeWithEntities.asyncLayout(self.oneLineNode) let (oneLineLayout, oneLineApply) = makeOneLineLayout(TextNodeLayoutArguments( attributedString: self.oneLineNodeAttributedText, @@ -942,8 +1215,14 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS displaySpoilers: false, displayEmbeddedItemsUnderSpoilers: false )) - - let oneLineFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: oneLineLayout.size) + + let oneLineFrame = CGRect( + origin: CGPoint( + x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, + y: panelOriginY + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel + ), + size: oneLineLayout.size + ) self.oneLineNode.textNode.frame = oneLineFrame let _ = oneLineApply(TextNodeWithEntities.Arguments( context: self.context, @@ -952,18 +1231,33 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS placeholderColor: self.presentationInterfaceState?.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12) ?? .lightGray, attemptSynchronous: false )) - self.updateOneLineSpoiler() } + self.textPlaceholderNode.isHidden = inputHasText - - let additionalRightInset = self.updateFieldAndButtonsLayout(inputHasText: inputHasText, panelHeight: panelHeight, transition: transition) - - let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right - additionalRightInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) - transition.updateFrame(node: self.textInputContainer, frame: textInputFrame) - + let textInputFrame = CGRect( + x: leftInset + textFieldInsets.left, + y: panelOriginY + textFieldInsets.top, + width: baseWidth - textFieldInsets.left - textFieldInsets.right, + height: panelContentHeight - textFieldInsets.top - textFieldInsets.bottom + ) + let additionalRightInset = self.updateFieldAndButtonsLayout(inputHasText: inputHasText, panelHeight: panelContentHeight, panelOriginY: panelOriginY, textInputFrame: textInputFrame, transition: transition) + let updatedTextInputFrame = CGRect( + x: textInputFrame.minX, + y: textInputFrame.minY, + width: baseWidth - textFieldInsets.left - textFieldInsets.right - additionalRightInset, + height: textInputFrame.height + ) + transition.updateFrame(node: self.textInputContainer, frame: updatedTextInputFrame) + if let textInputNode = self.textInputNode { - let textFieldFrame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - textInputViewInternalInsets.bottom)) + let textFieldFrame = CGRect( + origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), + size: CGSize( + width: updatedTextInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), + height: updatedTextInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom + ) + ) let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size if let presentationInterfaceState = self.presentationInterfaceState { textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState, glass: self.glass) @@ -973,33 +1267,42 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS textInputNode.layout() } } - + + self.updateCounterTextNode(transition: transition, panelHeight: panelContentHeight, panelOriginY: panelOriginY) self.actionButtons.updateAccessibility() - - if self.glass { - panelHeight += 11.0 + + if let inputMediaNode = inputMediaNodeForLayout { + if isNewInputMediaNode && transition.isAnimated { + inputMediaNode.view.frame = inputMediaFrame.offsetBy(dx: 0.0, dy: inputMediaHeight) + inputMediaNode.view.alpha = 0.0 + inputMediaNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + inputMediaNode.view.alpha = 1.0 + transition.updateFrame(view: inputMediaNode.view, frame: inputMediaFrame) } - - return panelHeight + + self.currentHeight = totalHeight + return totalHeight } - - private func updateFieldAndButtonsLayout(inputHasText: Bool, panelHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { - guard let (width, leftInsetValue, rightInsetValue, additionalSideInsets, _, metrics, _) = self.validLayout else { + + private func updateFieldAndButtonsLayout(inputHasText: Bool, panelHeight: CGFloat, panelOriginY: CGFloat, textInputFrame: CGRect, transition: ContainedViewLayoutTransition) -> CGFloat { + guard let layout = self.validLayout else { return 0.0 } - - let leftInset = leftInsetValue + 8.0 - let rightInset = rightInsetValue + 8.0 - + + let width = layout.width + let leftInset = layout.leftInset + 8.0 + let rightInset = layout.rightInset + 8.0 + var textFieldMinHeight: CGFloat = self.glass ? 40.0 : 33.0 if let presentationInterfaceState = self.presentationInterfaceState { - textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, glass: self.glass, metrics: metrics) + textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, glass: self.glass, metrics: layout.metrics) } var minimalHeight: CGFloat = textFieldMinHeight if !self.glass { minimalHeight += 14.0 } - + var panelHeight = panelHeight var composeButtonsOffset: CGFloat = 0.0 if self.isCaption { @@ -1010,15 +1313,14 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS panelHeight = minimalHeight } } - + let baseWidth = width - leftInset - rightInset - let textInputFrame = self.textInputContainer.frame - - var textFieldInsets = self.textFieldInsets(metrics: metrics) - if additionalSideInsets.right > 0.0 { - textFieldInsets.right += additionalSideInsets.right / 3.0 + + var textFieldInsets = self.textFieldInsets(metrics: layout.metrics) + if layout.additionalSideInsets.right > 0.0 { + textFieldInsets.right += layout.additionalSideInsets.right / 3.0 } - + var isPaidMessage = false var textBackgroundInset: CGFloat = 0.0 let actionButtonsSize: CGSize @@ -1039,16 +1341,16 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } else { actionButtonsSize = CGSize(width: 44.0, height: minimalHeight) } - + let actionButtonsOriginOffset: CGFloat = self.glass ? -6.0 : 0.0 - let actionButtonsFrame = CGRect(origin: CGPoint(x: width - rightInset - actionButtonsSize.width + 1.0 - UIScreenPixel + composeButtonsOffset + actionButtonsOriginOffset, y: panelHeight - minimalHeight - 1.0), size: actionButtonsSize) + let actionButtonsFrame = CGRect(origin: CGPoint(x: width - rightInset - actionButtonsSize.width + 1.0 - UIScreenPixel + composeButtonsOffset + actionButtonsOriginOffset, y: panelOriginY + panelHeight - minimalHeight - 1.0), size: actionButtonsSize) transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame) - + let textInputHeight = panelHeight - textFieldInsets.top - textFieldInsets.bottom let textInputBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset - textBackgroundInset, height: textInputHeight)) transition.updateFrame(node: self.textInputContainerBackgroundNode, frame: textInputBackgroundFrame) - - transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset - textBackgroundInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)) + + transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: CGRect(x: leftInset + textFieldInsets.left, y: panelOriginY + textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset - textBackgroundInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)) transition.updateFrame(layer: self.textInputBackgroundImageNode.layer, frame: CGRect(x: 0.0, y: 0.0, width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset - textBackgroundInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)) if self.isAIEnabled { @@ -1109,7 +1411,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS var textInputViewRealInsets = UIEdgeInsets() if let presentationInterfaceState = self.presentationInterfaceState { textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState, glass: self.glass) - + var colors: [String: UIColor] = [:] let colorKeys: [String] = [ "__allcolors__" @@ -1120,7 +1422,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } let animationComponent = LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( - name: self.textInputNode?.textView.inputView == nil ? "input_anim_smileToKey" : "input_anim_keyToSmile", + name: self.currentInputMode == .text ? "input_anim_smileToKey" : "input_anim_keyToSmile", mode: .still(position: .begin) ), colors: colors, @@ -1140,20 +1442,20 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS if self.glass { inputNodeOffset = CGPoint(x: -6.0, y: -4.0) } - transition.updateFrame(view: self.inputModeView, frame: CGRect(origin: CGPoint(x: textInputBackgroundFrame.maxX - inputNodeSize.width + inputNodeOffset.x, y: textInputBackgroundFrame.maxY - inputNodeSize.height + inputNodeOffset.y), size: inputNodeSize)) + transition.updateFrame(view: self.inputModeView, frame: CGRect(origin: CGPoint(x: textInputBackgroundFrame.maxX - inputNodeSize.width + inputNodeOffset.x, y: panelOriginY + textInputBackgroundFrame.maxY - inputNodeSize.height + inputNodeOffset.y), size: inputNodeSize)) } - + let placeholderFrame: CGRect if self.isCaption && !self.isFocused { - placeholderFrame = CGRect(origin: CGPoint(x: textInputFrame.minX + floorToScreenPixels((textInputBackgroundFrame.width - self.textPlaceholderNode.frame.width) / 2.0), y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size) + placeholderFrame = CGRect(origin: CGPoint(x: textInputFrame.minX + floorToScreenPixels((textInputBackgroundFrame.width - self.textPlaceholderNode.frame.width) / 2.0), y: panelOriginY + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size) } else { - placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size) + placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: panelOriginY + textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size) } transition.updateFrame(node: self.textPlaceholderNode, frame: placeholderFrame) - + return isPaidMessage ? textBackgroundInset : 0.0 } - + private var skipUpdate = false public func chatInputTextNodeDidUpdateText() { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { @@ -1162,24 +1464,22 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) }) refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) - + self.updateSpoiler() - + let inputTextState = self.inputTextState - + self.skipUpdate = true - + self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) self.interfaceInteraction?.updateInputLanguage({ _ in return textInputNode.textInputMode?.primaryLanguage }) if self.isCaption, let presentationInterfaceState = self.presentationInterfaceState { self.presentationInterfaceState = presentationInterfaceState.updatedInterfaceState({ return $0.withUpdatedComposeInputState(inputTextState) }) - + } self.updateTextNodeText(animated: true) - - self.updateCounterTextNode(transition: .immediate) if let aiButton = self.aiButton { var aiButtonAlpha: CGFloat = self.heightDependentAiButtonAlpha @@ -1199,17 +1499,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS @objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { self.chatInputTextNodeDidUpdateText() } - + private func updateSpoiler() { guard let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else { return } - + let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor - + var rects: [CGRect] = [] var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = [] - + if let attributedText = textInputNode.attributedText { let beginning = textInputNode.textView.beginningOfDocument attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in @@ -1222,10 +1522,10 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } } - + var startIndex: Int? var currentIndex: Int? - + let nsString = (attributedText.string as NSString) nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil { @@ -1239,14 +1539,14 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } currentIndex = range.location + range.length } - + if let currentStartIndex = startIndex, let currentIndex = currentIndex { startIndex = nil let endIndex = currentIndex addSpoiler(startIndex: currentStartIndex, endIndex: endIndex) } } - + if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute { if let start = textInputNode.textView.position(from: beginning, offset: range.location), let end = textInputNode.textView.position(from: start, offset: range.length), let textRange = textInputNode.textView.textRange(from: start, to: end) { let textRects = textInputNode.textView.selectionRects(for: textRange) @@ -1258,7 +1558,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } }) } - + if !rects.isEmpty { let dustNode: InvisibleInkDustNode if let current = self.dustNode { @@ -1276,7 +1576,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS dustNode.removeFromSupernode() self.dustNode = nil } - + if !customEmojiRects.isEmpty { let customEmojiContainerView: CustomEmojiContainerView if let current = self.customEmojiContainerView { @@ -1292,21 +1592,21 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS textInputNode.textView.addSubview(customEmojiContainerView) self.customEmojiContainerView = customEmojiContainerView } - + customEmojiContainerView.update(emojiRects: customEmojiRects) } else if let customEmojiContainerView = self.customEmojiContainerView { customEmojiContainerView.removeFromSuperview() self.customEmojiContainerView = nil } } - + private func updateSpoilersRevealed(animated: Bool = true) { guard let textInputNode = self.textInputNode else { return } - + let selectionRange = textInputNode.textView.selectedRange - + var revealed = false if let attributedText = textInputNode.attributedText { attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in @@ -1317,12 +1617,12 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } }) } - + guard self.spoilersRevealed != revealed else { return } self.spoilersRevealed = revealed - + if revealed { self.updateInternalSpoilersRevealed(true, animated: animated) } else { @@ -1331,26 +1631,26 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS }) } } - + private func updateInternalSpoilersRevealed(_ revealed: Bool, animated: Bool) { guard self.spoilersRevealed == revealed, let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState else { return } - + let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) - + textInputNode.textView.isScrollEnabled = false - + refreshChatTextInputAttributes(context: self.context, textView: textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) }) - + textInputNode.attributedText = textAttributedStringForStateText(context: self.context, stateText: self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) }) - + if textInputNode.textView.subviews.count > 1, animated { let containerView = textInputNode.textView.subviews[1] if let canvasView = containerView.subviews.first { @@ -1371,7 +1671,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS Queue.mainQueue().after(0.1) { textInputNode.textView.isScrollEnabled = true } - + if animated { if revealed { let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) @@ -1388,8 +1688,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS dustNode.alpha = revealed ? 0.0 : 1.0 } } - - private func updateCounterTextNode(transition: ContainedViewLayoutTransition) { + + private func updateCounterTextNode(transition: ContainedViewLayoutTransition, panelHeight: CGFloat, panelOriginY: CGFloat) { let inputTextMaxLength: Int32? if let maxCaptionLength = self.maxCaptionLength { inputTextMaxLength = maxCaptionLength @@ -1399,123 +1699,146 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let inputTextMaxLength = inputTextMaxLength { let textCount = Int32(textInputNode.textView.text.count) let counterColor: UIColor = textCount > inputTextMaxLength ? presentationInterfaceState.theme.chat.inputPanel.panelControlDestructiveColor : presentationInterfaceState.theme.chat.inputPanel.panelControlColor - + let remainingCount = max(-999, inputTextMaxLength - textCount) let counterText = remainingCount >= 5 ? "" : "\(remainingCount)" self.counterTextNode.attributedText = NSAttributedString(string: counterText, font: counterFont, textColor: counterColor) } else { self.counterTextNode.attributedText = NSAttributedString(string: "", font: counterFont, textColor: .black) } - - if let (width, leftInset, rightInset, _, maxHeight, metrics, _) = self.validLayout { + + if let layout = self.validLayout { let composeButtonsOffset: CGFloat = 0.0 - - let leftInset = leftInset + 8.0 - let rightInset = rightInset + 8.0 - - let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset, maxHeight: maxHeight, metrics: metrics) - let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) + + let rightInset = layout.rightInset + 8.0 var textFieldMinHeight: CGFloat = 33.0 if let presentationInterfaceState = self.presentationInterfaceState { - textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, glass: self.glass, metrics: metrics) + textFieldMinHeight = calclulateTextFieldMinHeight(presentationInterfaceState, glass: self.glass, metrics: layout.metrics) } let minimalHeight: CGFloat = 14.0 + textFieldMinHeight - + let counterSize = self.counterTextNode.updateLayout(CGSize(width: 44.0, height: 44.0)) - let actionButtonsOriginX = width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset - let counterFrame = CGRect(origin: CGPoint(x: actionButtonsOriginX, y: panelHeight - minimalHeight - counterSize.height + 3.0), size: CGSize(width: width - actionButtonsOriginX - rightInset, height: counterSize.height)) + let actionButtonsOriginX = layout.width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset + let counterFrame = CGRect(origin: CGPoint(x: actionButtonsOriginX, y: panelOriginY + panelHeight - minimalHeight - counterSize.height + 3.0), size: CGSize(width: layout.width - actionButtonsOriginX - rightInset, height: counterSize.height)) transition.updateFrame(node: self.counterTextNode, frame: counterFrame) } } - + private func toggleInputMode() { self.loadTextInputNodeIfNeeded() - + guard let textInputNode = self.textInputNode else { return } - - var shouldHaveInputView = false - if textInputNode.textView.isFirstResponder { - if textInputNode.textView.inputView == nil { - shouldHaveInputView = true + + switch self.currentInputMode { + case .text: + self.isTransitioningToTextKeyboard = false + self.currentInputMode = .emoji + self.applyCurrentInputMode(reload: textInputNode.textView.isFirstResponder) + if !textInputNode.textView.isFirstResponder { + textInputNode.textView.becomeFirstResponder() } - } else { - shouldHaveInputView = true - } - - if shouldHaveInputView { - let inputView = self.makeEntityInputView() - inputView?.insertText = { [weak self] text in - guard let strongSelf = self else { - return - } - - strongSelf.interfaceInteraction?.updateTextInputStateAndMode { textInputState, inputMode in - let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) - - let range = textInputState.selectionRange - inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: text) - - let selectionPosition = range.lowerBound + (text.string as NSString).length - - return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode) - } - } - inputView?.deleteBackwards = { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.textInputNode?.textView.deleteBackward() - } - inputView?.switchToKeyboard = { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.toggleInputMode() - } - inputView?.presentController = { [weak self] c in - guard let strongSelf = self else { - return - } - strongSelf.presentController(c) - } - - textInputNode.textView.inputView = inputView - } else { - textInputNode.textView.inputView = nil - } - - if textInputNode.textView.isFirstResponder { - textInputNode.textView.reloadInputViews() - } else { - textInputNode.textView.becomeFirstResponder() + self.requestRelayout(animated: true) + case .emoji: + self.activateInput() } } - + + private func requestRelayout(animated: Bool) { + self.updateHeight(animated) + self.heightUpdated?(animated) + } + + private func applyCurrentInputMode(reload: Bool) { + guard let textInputNode = self.textInputNode else { + return + } + + switch self.currentInputMode { + case .text: + if textInputNode.textView.inputView != nil { + textInputNode.textView.inputView = nil + if reload && textInputNode.textView.isFirstResponder { + textInputNode.textView.reloadInputViews() + } + } + case .emoji: + if !(textInputNode.textView.inputView is EmptyInputView) { + textInputNode.textView.inputView = EmptyInputView() + if reload && textInputNode.textView.isFirstResponder { + textInputNode.textView.reloadInputViews() + } + } + } + } + + private func insertTextFromInputMedia(_ text: NSAttributedString) { + self.loadTextInputNodeIfNeeded() + + guard let textInputNode = self.textInputNode else { + return + } + + let attributedText = NSMutableAttributedString(attributedString: textInputNode.attributedText ?? NSAttributedString()) + let range = textInputNode.selectedRange + attributedText.replaceCharacters(in: range, with: text) + let selectionPosition = range.lowerBound + text.length + textInputNode.attributedText = attributedText + textInputNode.selectedRange = NSRange(location: selectionPosition, length: 0) + self.chatInputTextNodeDidUpdateText() + } + + private func deleteBackwardsFromInputMedia() { + self.loadTextInputNodeIfNeeded() + + guard let textInputNode = self.textInputNode else { + return + } + if textInputNode.textView.isFirstResponder { + textInputNode.textView.deleteBackward() + return + } + + let attributedText = NSMutableAttributedString(attributedString: textInputNode.attributedText ?? NSAttributedString()) + let range = textInputNode.selectedRange + if range.length > 0 { + attributedText.deleteCharacters(in: range) + textInputNode.attributedText = attributedText + textInputNode.selectedRange = NSRange(location: range.location, length: 0) + self.chatInputTextNodeDidUpdateText() + } else if range.location > 0 { + let deleteRange = NSRange(location: range.location - 1, length: 1) + attributedText.deleteCharacters(in: deleteRange) + textInputNode.attributedText = attributedText + textInputNode.selectedRange = NSRange(location: deleteRange.location, length: 0) + self.chatInputTextNodeDidUpdateText() + } + } + private func updateTextNodeText(animated: Bool) { var inputHasText = false if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 { inputHasText = true } - + if let presentationInterfaceState = self.presentationInterfaceState { self.textPlaceholderNode.isHidden = inputHasText - + let textColor = presentationInterfaceState.theme.chat.inputPanel.inputTextColor let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) let textFont = Font.regular(baseFontSize) let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor - + let attributedText = textAttributedStringForStateText(context: self.context, stateText: self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false, availableEmojis: Set(self.context.animatedEmojiStickersValue.keys), emojiViewProvider: self.emojiViewProvider, makeCollapsedQuoteAttachment: { text, attributes in return ChatInputTextCollapsedQuoteAttachmentImpl(text: text, attributes: attributes) }) - + let range = (attributedText.string as NSString).range(of: "\n") if range.location != NSNotFound { let trimmedText = NSMutableAttributedString(attributedString: attributedText.attributedSubstring(from: NSMakeRange(0, range.location))) trimmedText.append(NSAttributedString(string: "\u{2026}", font: textFont, textColor: textColor)) - + self.oneLineNodeAttributedText = trimmedText } else { self.oneLineNodeAttributedText = attributedText @@ -1523,24 +1846,45 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } else { self.oneLineNodeAttributedText = nil } - + let panelHeight = self.updateTextHeight(animated: animated) - if self.isAttachment, let panelHeight = panelHeight { - let _ = self.updateFieldAndButtonsLayout(inputHasText: inputHasText, panelHeight: panelHeight, transition: .animated(duration: 0.2, curve: .easeInOut)) + if self.isAttachment, let panelHeight = panelHeight, let layout = self.validLayout { + let leftInset = layout.leftInset + 8.0 + let rightInset = layout.rightInset + 8.0 + let baseWidth = layout.width - leftInset - rightInset + + var textFieldInsets = self.textFieldInsets(metrics: layout.metrics) + if layout.additionalSideInsets.right > 0.0 { + textFieldInsets.right += layout.additionalSideInsets.right / 3.0 + } + + let textInputFrame = CGRect( + x: leftInset + textFieldInsets.left, + y: textFieldInsets.top, + width: baseWidth - textFieldInsets.left - textFieldInsets.right, + height: panelHeight - textFieldInsets.top - textFieldInsets.bottom + ) + let _ = self.updateFieldAndButtonsLayout( + inputHasText: inputHasText, + panelHeight: panelHeight, + panelOriginY: 0.0, + textInputFrame: textInputFrame, + transition: .animated(duration: 0.2, curve: .easeInOut) + ) } } - + private func updateOneLineSpoiler() { if let textLayout = self.oneLineNode.textNode.cachedLayout, !textLayout.spoilers.isEmpty { if self.oneLineDustNode == nil { let oneLineDustNode = InvisibleInkDustNode(textNode: nil, enableAnimations: self.context.sharedContext.energyUsageSettings.fullTranslucency) self.oneLineDustNode = oneLineDustNode self.oneLineNode.textNode.supernode?.insertSubnode(oneLineDustNode, aboveSubnode: self.oneLineNode.textNode) - + } if let oneLineDustNode = self.oneLineDustNode { let textFrame = self.oneLineNode.textNode.frame.insetBy(dx: 0.0, dy: -3.0) - + oneLineDustNode.update(size: textFrame.size, color: .white, textColor: .white, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 0.0, dy: 3.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 0.0, dy: 3.0) }) oneLineDustNode.frame = textFrame } @@ -1551,39 +1895,40 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } } - + private func updateTextHeight(animated: Bool) -> CGFloat? { - if let (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, _) = self.validLayout { - let leftInset = leftInset + 8.0 - let rightInset = rightInset + 8.0 - - let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: width - leftInset - rightInset - additionalSideInsets.right, maxHeight: maxHeight, metrics: metrics) - let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) - if !self.bounds.size.height.isEqual(to: panelHeight) { + if let layout = self.validLayout { + let leftInset = layout.leftInset + 8.0 + let rightInset = layout.rightInset + 8.0 + + let (_, textFieldHeight) = self.calculateTextFieldMetrics(width: layout.width - leftInset - rightInset - layout.additionalSideInsets.right, maxHeight: layout.textFieldMaxHeight, metrics: layout.metrics) + let panelContentHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: layout.metrics) + let totalHeight = panelContentHeight + (self.glass ? 11.0 : 0.0) + self.currentAdditionalInputHeight + if self.currentHeight != totalHeight { self.updateHeight(animated) self.heightUpdated?(animated) } - return panelHeight + return panelContentHeight } else { return nil } } - + public func chatInputTextNodeShouldReturn(modifierFlags: UIKeyModifierFlags) -> Bool { if self.actionButtons.sendButton.supernode != nil && !self.actionButtons.sendButton.isHidden && !self.actionButtons.sendButton.alpha.isZero { self.sendButtonPressed() } return false } - + @objc public func editableTextNodeShouldReturn(_ editableTextNode: ASEditableTextNode) -> Bool { return self.chatInputTextNodeShouldReturn(modifierFlags: []) } - + private func applyUpdateSendButtonIcon() { if let interfaceState = self.presentationInterfaceState { let sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil - + if sendButtonHasApplyIcon != self.actionButtons.sendButtonHasApplyIcon { self.actionButtons.sendButtonHasApplyIcon = sendButtonHasApplyIcon if self.actionButtons.sendButtonHasApplyIcon { @@ -1598,7 +1943,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } } - + public func chatInputTextNodeDidChangeSelection(dueToEditing: Bool) { if !dueToEditing && !self.updatingInputState { let inputTextState = self.inputTextState @@ -1606,58 +1951,63 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS self.interfaceInteraction?.updateTextInputStateAndMode({ _, inputMode in return (inputTextState, inputMode) }) self.skipUpdate = false } - + if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState { if case .format = self.inputMenu.state { self.inputMenu.hide() } - + let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) refreshChatTextInputTypingAttributes(textInputNode.textView, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) - + self.updateSpoilersRevealed() } } - + @objc public func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { self.chatInputTextNodeDidChangeSelection(dueToEditing: dueToEditing) } - + public func chatInputTextNodeDidBeginEditing() { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in return (.text, state.keyboardButtonsMessage?.id) }) self.inputMenu.activate() - + self.focusUpdated?(true) - - if self.isCaption, let (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, isSecondary) = self.validLayout, let presentationInterfaceState = self.presentationInterfaceState { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: 0.0, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .animated(duration: 0.3, curve: .easeInOut), interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: false) + + if self.isCaption || self.currentInputMode != .text { + self.requestRelayout(animated: true) } } - + @objc public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { self.chatInputTextNodeDidBeginEditing() } - + public func chatInputTextNodeDidFinishEditing() { guard let editableTextNode = self.textInputNode else { return } + let shouldUpdateLayout = self.isCaption || self.currentInputMode != .text self.storedInputLanguage = editableTextNode.textInputMode?.primaryLanguage self.inputMenu.deactivate() - + self.focusUpdated?(false) - - if self.isCaption, let (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, isSecondary) = self.validLayout, let presentationInterfaceState = self.presentationInterfaceState { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: 0.0, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .animated(duration: 0.3, curve: .easeInOut), interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: false) + + if self.currentInputMode != .text { + self.currentInputMode = .text + self.applyCurrentInputMode(reload: false) + } + if shouldUpdateLayout { + self.requestRelayout(animated: true) } } - + public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { self.chatInputTextNodeDidFinishEditing() } - + public func editableTextNodeTarget(forAction action: Selector) -> ASEditableTextNodeTargetForAction? { if action == makeSelectorFromString("_accessibilitySpeak:") { if case .format = self.inputMenu.state { @@ -1703,17 +2053,17 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } return nil } - + @available(iOS 13.0, *) public func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { guard let editableTextNode = self.textInputNode else { return UIMenu(children: suggestedActions) } - + var actions = suggestedActions - + if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 { - + } else { var children: [UIAction] = [ UIAction(title: self.strings?.TextFormat_Bold ?? "Bold", image: nil) { [weak self] (action) in @@ -1747,12 +2097,12 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } ] - + var hasSpoilers = true - if self.presentationInterfaceState?.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat { + if self.presentationInterfaceState?.chatLocation.peerId?.isSecretChat == true { hasSpoilers = false } - + if hasSpoilers { children.insert(UIAction(title: self.strings?.TextFormat_Quote ?? "Quote", image: nil) { [weak self] (action) in if let strongSelf = self { @@ -1770,18 +2120,18 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } }) } - + let formatMenu = UIMenu(title: self.strings?.TextFormat_Format ?? "Format", image: nil, children: children) actions.insert(formatMenu, at: 3) } return UIMenu(children: actions) } - + @available(iOS 16.0, *) public func editableTextNodeMenu(_ editableTextNode: ASEditableTextNode, forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu { return self.chatInputTextNodeMenu(forTextRange: textRange, suggestedActions: suggestedActions) } - + private var currentSpeechHolder: SpeechSynthesizerHolder? @objc func _accessibilitySpeak(_ sender: Any) { var text = "" @@ -1804,34 +2154,34 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS UIMenuController.shared.update() } } - + @objc func _showTextStyleOptions(_ sender: Any) { if let textInputNode = self.textInputNode { self.inputMenu.format(view: textInputNode.view, rect: textInputNode.selectionRect.offsetBy(dx: 0.0, dy: -textInputNode.textView.contentOffset.y).insetBy(dx: 0.0, dy: -1.0)) } } - + @objc func formatAttributesBold(_ sender: Any) { self.inputMenu.back() self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.bold, value: nil), inputMode) } } - + @objc func formatAttributesItalic(_ sender: Any) { self.inputMenu.back() self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.italic, value: nil), inputMode) } } - + @objc func formatAttributesMonospace(_ sender: Any) { self.inputMenu.back() self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.monospace, value: nil), inputMode) } } - + private var imitateFocus = false @objc func formatAttributesLink(_ sender: Any) { self.inputMenu.back() @@ -1840,24 +2190,24 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } self.interfaceInteraction?.openLinkEditing() } - + @objc func formatAttributesStrikethrough(_ sender: Any) { self.inputMenu.back() self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.strikethrough, value: nil), inputMode) } } - + @objc func formatAttributesUnderline(_ sender: Any) { self.inputMenu.back() self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.underline, value: nil), inputMode) } } - + @objc func formatAttributesSpoiler(_ sender: Any) { self.inputMenu.back() - + var animated = false if let attributedText = self.textInputNode?.attributedText { attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in @@ -1866,35 +2216,35 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } }) } - + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.spoiler, value: nil), inputMode) } - + self.updateSpoilersRevealed(animated: animated) } - + @objc func formatAttributesQuote(_ sender: Any) { self.inputMenu.back() - + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .quote, isCollapsed: false)), inputMode) } } - + @objc func formatAttributesCodeBlock(_ sender: Any) { self.inputMenu.back() - + self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in return (chatTextInputAddFormattingAttribute(current, attribute: ChatTextInputAttributes.block, value: ChatTextInputTextQuoteAttribute(kind: .code(language: nil), isCollapsed: false)), inputMode) } } - + public func chatInputTextNode(shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { guard let editableTextNode = self.textInputNode else { return true } - + var cleanText = text let removeSequences: [String] = ["\u{202d}", "\u{202c}"] for sequence in removeSequences { @@ -1906,7 +2256,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } } } - + if cleanText != text { let string = NSMutableAttributedString(attributedString: editableTextNode.attributedText ?? NSAttributedString()) var textColor: UIColor = .black @@ -1928,11 +2278,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } return true } - + @objc public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { return self.chatInputTextNode(shouldChangeTextIn: range, replacementText: text) } - + public func chatInputTextNodeShouldCopy() -> Bool { self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in storeInputTextInPasteboard(current.inputText.attributedSubstring(from: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count))) @@ -1940,21 +2290,21 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } return false } - + @objc public func editableTextNodeShouldCopy(_ editableTextNode: ASEditableTextNode) -> Bool { return self.chatInputTextNodeShouldCopy() } - + public func chatInputTextNodeShouldPaste() -> Bool { let pasteboard = UIPasteboard.general - + var attributedString: NSAttributedString? if let data = pasteboard.data(forPasteboardType: kUTTypeRTF as String) { attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtf) } else if let data = pasteboard.data(forPasteboardType: "com.apple.flat-rtfd") { attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtfd) } - + if let attributedString = attributedString { self.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in if let inputText = current.inputText.mutableCopy() as? NSMutableAttributedString { @@ -1969,22 +2319,22 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } return true } - + public func chatInputTextNodeShouldRespondToAction(action: Selector) -> Bool { return true } - + public func chatInputTextNodeTargetForAction(action: Selector) -> ChatInputTextNode.TargetForAction? { return nil } - + @objc public func editableTextNodeShouldPaste(_ editableTextNode: ASEditableTextNode) -> Bool { return self.chatInputTextNodeShouldPaste() } - + public func chatInputTextNodeBackspaceWhileEmpty() { } - + @objc private func aiButtonPressed() { self.invokeAICompose?() } @@ -2014,31 +2364,38 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } self.sendMessage(.generic, nil) } - + @objc func textInputBackgroundViewTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.ensureFocused() } } - + public var isFocused: Bool { if self.imitateFocus { return true } return self.textInputNode?.isFirstResponder() ?? false } - + public func ensureUnfocused() { + self.isTransitioningToTextKeyboard = false self.textInputNode?.resignFirstResponder() + if self.currentInputMode != .text { + self.currentInputMode = .text + self.requestRelayout(animated: true) + } + self.applyCurrentInputMode(reload: false) } - + public func ensureFocused() { self.imitateFocus = false - + if self.textInputNode == nil { self.loadTextInputNode() } - + + self.applyCurrentInputMode(reload: false) self.textInputNode?.becomeFirstResponder() } diff --git a/submodules/AttachmentUI/Sources/AttachmentContainer.swift b/submodules/AttachmentUI/Sources/AttachmentContainer.swift index 88fd1b7b67..25d2bf0d7d 100644 --- a/submodules/AttachmentUI/Sources/AttachmentContainer.swift +++ b/submodules/AttachmentUI/Sources/AttachmentContainer.swift @@ -479,7 +479,9 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { if self.isDismissed { return } - self.bottomClipNode.cornerRadius = layout.deviceMetrics.screenCornerRadius - 2.0 + + let containerCornerRadius = max(24.0, layout.deviceMetrics.screenCornerRadius) + self.bottomClipNode.cornerRadius = containerCornerRadius - 2.0 self.isUpdatingState = true diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index bb6cf603c6..ac85742469 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import ComponentFlow import TelegramCore import TelegramPresentationData @@ -35,7 +34,7 @@ public enum AttachmentButtonType: Equatable { case emoji case audio case standalone - + public var key: String { switch self { case .gallery: @@ -66,7 +65,7 @@ public enum AttachmentButtonType: Equatable { return "standalone" } } - + public static func ==(lhs: AttachmentButtonType, rhs: AttachmentButtonType) -> Bool { switch lhs { case .gallery: @@ -164,71 +163,71 @@ public protocol AttachmentContainable: ViewController, MinimizableController { var isInnerPanGestureEnabled: (() -> Bool)? { get } var mediaPickerContext: AttachmentMediaPickerContext? { get } var getCurrentSendMessageContextMediaPreview: (() -> ChatSendMessageContextScreenMediaPreview?)? { get } - + func isContainerPanningUpdated(_ panning: Bool) - + func resetForReuse() func prepareForReuse() - + func requestDismiss(completion: @escaping () -> Void) func shouldDismissImmediately() -> Bool - + func beforeMaximize(navigationController: NavigationController, completion: @escaping () -> Void) } public extension AttachmentContainable { func isContainerPanningUpdated(_ panning: Bool) { - + } - + func resetForReuse() { - + } - + func prepareForReuse() { - + } - + func requestDismiss(completion: @escaping () -> Void) { completion() } - + func shouldDismissImmediately() -> Bool { return true } - + func beforeMaximize(navigationController: NavigationController, completion: @escaping () -> Void) { completion() } - + var minimizedBounds: CGRect? { return nil } - + var isFullscreen: Bool { return false } - + var minimizedTopEdgeOffset: CGFloat? { return nil } - + var minimizedIcon: UIImage? { return nil } - + var minimizedProgress: Float? { return nil } - + var isPanGestureEnabled: (() -> Bool)? { return nil } - + var isInnerPanGestureEnabled: (() -> Bool)? { return nil } - + var getCurrentSendMessageContextMediaPreview: (() -> ChatSendMessageContextScreenMediaPreview?)? { return nil } @@ -248,25 +247,25 @@ public enum AttachmentMediaPickerAttachmentMode { public protocol AttachmentMediaPickerContext { var selectionCount: Signal { get } var caption: Signal { get } - + var hasCaption: Bool { get } var captionIsAboveMedia: Signal { get } func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void - + var canMakePaidContent: Bool { get } var price: Int64? { get } func setPrice(_ price: Int64) -> Void - + var hasTimers: Bool { get } - + var loadingProgress: Signal { get } var mainButtonState: Signal { get } var secondaryButtonState: Signal { get } var bottomPanelBackgroundColor: Signal { get } - + func mainButtonAction() func secondaryButtonAction() - + func setCaption(_ caption: NSAttributedString) func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) @@ -276,19 +275,19 @@ public extension AttachmentMediaPickerContext { var selectionCount: Signal { return .single(0) } - + var caption: Signal { return .single(nil) } - + var captionIsAboveMedia: Signal { return .single(false) } - + var hasCaption: Bool { return false } - + func setCaptionIsAboveMedia(_ captionIsAboveMedia: Bool) -> Void { } @@ -299,42 +298,42 @@ public extension AttachmentMediaPickerContext { var price: Int64? { return nil } - + func setPrice(_ price: Int64) -> Void { } - + var hasTimers: Bool { return false } - + var loadingProgress: Signal { return .single(nil) } - + var mainButtonState: Signal { return .single(nil) } - + var secondaryButtonState: Signal { return .single(nil) } - + var bottomPanelBackgroundColor: Signal { return .single(nil) } - + func setCaption(_ caption: NSAttributedString) { } - + func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode, parameters: ChatSendMessageActionSheetController.SendParameters?) { } - + func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { } - + func mainButtonAction() { } - + func secondaryButtonAction() { } } @@ -346,7 +345,7 @@ private func generateShadowImage(glass: Bool) -> UIImage? { let side = innerSide + middle + shadowInset * 2.0 return generateImage(CGSize(width: side, height: side), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - + context.saveGState() context.setShadow(offset: CGSize(), blur: glass ? 80.0 : 60.0, color: UIColor(white: 0.0, alpha: glass ? 0.3 : 0.4).cgColor) @@ -356,9 +355,9 @@ private func generateShadowImage(glass: Bool) -> UIImage? { context.addPath(firstPath) context.addPath(secondPath) context.fillPath() - + context.restoreGState() - + context.setBlendMode(.clear) context.addPath(firstPath) context.addPath(secondPath) @@ -367,9 +366,9 @@ private func generateShadowImage(glass: Bool) -> UIImage? { let path = UIBezierPath(roundedRect: CGRect(x: shadowInset, y: shadowInset, width: innerSide, height: innerSide), cornerRadius: 10.0).cgPath context.addPath(path) context.fillPath() - + context.restoreGState() - + context.setBlendMode(.clear) context.addPath(path) context.fillPath() @@ -380,9 +379,9 @@ private func generateShadowImage(glass: Bool) -> UIImage? { private func generateMaskImage(glass: Bool) -> UIImage? { return generateImage(CGSize(width: 390.0, height: 220.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - + context.setFillColor(UIColor.white.cgColor) - + if glass { let firstPath = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: 390.0, height: 209.0), cornerRadius: 62.0).cgPath let secondPath = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: 390.0, height: 130.0), cornerRadius: 38.0).cgPath @@ -403,7 +402,7 @@ public class AttachmentController: ViewController, MinimizableController { case glass case legacy } - + private let context: AccountContext private let updatedPresentationData: (initial: PresentationData, signal: Signal)? private let style: Style @@ -414,12 +413,12 @@ public class AttachmentController: ViewController, MinimizableController { private let fromMenu: Bool private var hasTextInput: Bool private let isFullSize: Bool - private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? + private let customEmojiAvailable: Bool public var animateAppearance: Bool = false - + public var willDismiss: () -> Void = {} public var didDismiss: () -> Void = {} - + public var mediaPickerContext: AttachmentMediaPickerContext? { get { return self.node.mediaPickerContext @@ -428,49 +427,48 @@ public class AttachmentController: ViewController, MinimizableController { self.node.mediaPickerContext = newValue } } - + private let _ready = Promise() override public var ready: Promise { return self._ready } - + public private(set) var minimizedTopEdgeOffset: CGFloat? public private(set) var minimizedBounds: CGRect? public var minimizedIcon: UIImage? { return self.mainController.minimizedIcon } - + public var isFullscreen: Bool { return self.mainController.isFullscreen } - + public weak var attachmentButton: UIView? - + private final class Node: ASDisplayNode { private weak var controller: AttachmentController? fileprivate let dim: ASDisplayNode private let shadowNode: ASImageNode fileprivate let container: AttachmentContainer - private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? let panel: AttachmentPanel - + fileprivate var currentType: AttachmentButtonType? fileprivate var currentControllers: [AttachmentContainable] = [] - + private var validLayout: ContainerViewLayout? private var modalProgress: CGFloat = 0.0 fileprivate var isDismissing = false - + private let captionDisposable = MetaDisposable() private let mediaSelectionCountDisposable = MetaDisposable() - + private let loadingProgressDisposable = MetaDisposable() private let mainButtonStateDisposable = MetaDisposable() private let secondaryButtonStateDisposable = MetaDisposable() private let bottomPanelBackgroundColorDisposable = MetaDisposable() - + private var selectionCount: Int = 0 - + var mediaPickerContext: AttachmentMediaPickerContext? { didSet { if let mediaPickerContext = self.mediaPickerContext { @@ -553,37 +551,36 @@ public class AttachmentController: ViewController, MinimizableController { } } } - + private let wrapperNode: ASDisplayNode - + private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - + private var isMinimizing = false - - init(controller: AttachmentController, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { + + init(controller: AttachmentController) { self.controller = controller - self.makeEntityInputView = makeEntityInputView - + if let updatedPresentationData = controller.updatedPresentationData { self.presentationData = updatedPresentationData.initial } else { self.presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } } - + self.dim = ASDisplayNode() self.dim.alpha = 0.0 self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) - + self.shadowNode = ASImageNode() self.shadowNode.isUserInteractionEnabled = false - + self.wrapperNode = ASDisplayNode() self.wrapperNode.clipsToBounds = true - + self.container = AttachmentContainer(presentationData: self.presentationData, isFullSize: controller.isFullSize, glass: controller._hasGlassStyle, hasPill: controller.style == .glass) self.container.canHaveKeyboardFocus = true - + let panelStyle: AttachmentPanel.Style switch controller.style { case .glass: @@ -591,37 +588,37 @@ public class AttachmentController: ViewController, MinimizableController { case .legacy: panelStyle = .legacy } - - self.panel = AttachmentPanel(controller: controller, style: panelStyle, context: controller.context, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, updatedPresentationData: controller.updatedPresentationData, makeEntityInputView: makeEntityInputView) + + self.panel = AttachmentPanel(controller: controller, style: panelStyle, context: controller.context, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, customEmojiAvailable: controller.customEmojiAvailable, updatedPresentationData: controller.updatedPresentationData) self.panel.fromMenu = controller.fromMenu self.panel.isStandalone = controller.isStandalone - + super.init() - + self.clipsToBounds = false - + self.addSubnode(self.dim) self.addSubnode(self.shadowNode) self.addSubnode(self.wrapperNode) - + self.container.controllerRemoved = { [weak self] controller in if let strongSelf = self, let layout = strongSelf.validLayout, !strongSelf.isDismissing { strongSelf.currentControllers = strongSelf.currentControllers.filter { $0 !== controller } strongSelf.containerLayoutUpdated(layout, transition: .immediate) } } - + self.container.updateModalProgress = { [weak self] progress, topInset, bounds, transition in if let strongSelf = self, let controller = strongSelf.controller, let layout = strongSelf.validLayout, !strongSelf.isDismissing { var transition = transition if strongSelf.container.supernode == nil { transition = .animated(duration: 0.5, curve: .spring) } - + strongSelf.modalProgress = progress strongSelf.controller?.minimizedTopEdgeOffset = topInset strongSelf.controller?.minimizedBounds = bounds - + if !strongSelf.isMinimizing { if controller.style != .glass || strongSelf.didMaximizeOnce { strongSelf.controller?.updateModalStyleOverlayTransitionFactor(progress, transition: transition) @@ -635,7 +632,7 @@ public class AttachmentController: ViewController, MinimizableController { strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.5, curve: .spring)) } } - + self.container.interactivelyDismissed = { [weak self] velocity in guard let self, let controller = self.controller, let layout = self.validLayout else { return true @@ -646,7 +643,7 @@ public class AttachmentController: ViewController, MinimizableController { delta -= minimizedTopEdgeOffset } let initialVelocity: CGFloat = delta > 0.0 ? velocity / delta : 0.0 - + if controller.shouldMinimizeOnSwipe?(self.currentType) == true { self.minimize(damping: damping, initialVelocity: initialVelocity) return false @@ -658,13 +655,13 @@ public class AttachmentController: ViewController, MinimizableController { } return true } - + self.container.isPanningUpdated = { [weak self] value in if let strongSelf = self, let currentController = strongSelf.currentControllers.last, !value { currentController.isContainerPanningUpdated(value) } } - + self.container.isPanGestureEnabled = { [weak self] in guard let self, let currentController = self.currentControllers.last else { return true @@ -675,7 +672,7 @@ public class AttachmentController: ViewController, MinimizableController { return true } } - + self.container.isInnerPanGestureEnabled = { [weak self] in guard let self, let currentController = self.currentControllers.last else { return true @@ -686,7 +683,7 @@ public class AttachmentController: ViewController, MinimizableController { return true } } - + self.container.shouldCancelPanGesture = { [weak self] in if let strongSelf = self, let currentController = strongSelf.currentControllers.last { if !currentController.shouldDismissImmediately() { @@ -698,7 +695,7 @@ public class AttachmentController: ViewController, MinimizableController { return false } } - + self.container.requestDismiss = { [weak self] in if let strongSelf = self, let currentController = strongSelf.currentControllers.last { currentController.requestDismiss { [weak self] in @@ -708,7 +705,7 @@ public class AttachmentController: ViewController, MinimizableController { } } } - + self.panel.selectionChanged = { [weak self] type in if let strongSelf = self { return strongSelf.switchToController(type) @@ -716,25 +713,25 @@ public class AttachmentController: ViewController, MinimizableController { return false } } - + self.panel.longPressed = { [weak self] _ in if let strongSelf = self, let currentController = strongSelf.currentControllers.last { currentController.longTapWithTabBar?() } } - + self.panel.beganTextEditing = { [weak self] in if let strongSelf = self { strongSelf.container.update(isExpanded: true, transition: .animated(duration: 0.5, curve: .spring)) } } - + self.panel.textUpdated = { [weak self] text in if let strongSelf = self { strongSelf.mediaPickerContext?.setCaption(text) } } - + self.panel.sendMessagePressed = { [weak self] mode, parameters in if let strongSelf = self { switch mode { @@ -754,14 +751,14 @@ public class AttachmentController: ViewController, MinimizableController { guard let self, let controller = self.controller, let mediaPickerContext = self.mediaPickerContext else { return } - + guard let caption = await mediaPickerContext.caption.get() else { return } if caption.length == 0 { return } - + let textProcessingScreen = await controller.context.sharedContext.makeTextProcessingScreen( context: controller.context, theme: self.presentationData.theme, @@ -790,45 +787,51 @@ public class AttachmentController: ViewController, MinimizableController { self.controller?.push(textProcessingScreen) } } - + self.panel.onMainButtonPressed = { [weak self] in if let strongSelf = self { strongSelf.mediaPickerContext?.mainButtonAction() } } - + self.panel.onSecondaryButtonPressed = { [weak self] in if let strongSelf = self { strongSelf.mediaPickerContext?.secondaryButtonAction() } } - + self.panel.requestLayout = { [weak self] in if let strongSelf = self, let layout = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.2, curve: .easeInOut)) } } - + self.panel.present = { [weak self] c in if let strongSelf = self { strongSelf.controller?.present(c, in: .window(.root)) } } - + self.panel.presentInGlobalOverlay = { [weak self] c in if let strongSelf = self { strongSelf.controller?.presentInGlobalOverlay(c, with: nil) } } - + self.panel.getNavigationController = { [weak self] in + guard let controller = self?.controller else { + return nil + } + return controller.navigationController as? NavigationController + } + self.panel.getCurrentSendMessageContextMediaPreview = { [weak self] in guard let self, let currentController = self.currentControllers.last else { return nil } - + return currentController.getCurrentSendMessageContextMediaPreview?() } - + if let updatedPresentationData = controller.updatedPresentationData { self.presentationDataDisposable = (updatedPresentationData.signal |> deliverOnMainQueue).start(next: { [weak self] presentationData in @@ -840,7 +843,7 @@ public class AttachmentController: ViewController, MinimizableController { }) } } - + deinit { self.captionDisposable.dispose() self.mediaSelectionCountDisposable.dispose() @@ -850,16 +853,16 @@ public class AttachmentController: ViewController, MinimizableController { self.bottomPanelBackgroundColorDisposable.dispose() self.presentationDataDisposable?.dispose() } - + private var inputContainerHeight: CGFloat? private var inputContainerNode: ASDisplayNode? override func didLoad() { super.didLoad() - + self.view.disablesInteractiveModalDismiss = true - + self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - + if let controller = self.controller { let _ = self.switchToController(controller.initialButton) if case let .app(bot) = controller.initialButton { @@ -884,14 +887,14 @@ public class AttachmentController: ViewController, MinimizableController { } } } - + if let (inputContainerHeight, inputContainerNode, _) = self.controller?.getInputContainerNode() { self.inputContainerHeight = inputContainerHeight self.inputContainerNode = inputContainerNode self.addSubnode(inputContainerNode) } } - + private var didMaximizeOnce = false fileprivate func minimize(damping: CGFloat? = nil, initialVelocity: CGFloat? = nil) { guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { @@ -911,25 +914,25 @@ public class AttachmentController: ViewController, MinimizableController { } return minimizedContainer }, animated: true) - + self.dim.isHidden = true - + self.isMinimizing = true self.container.update(isExpanded: true, force: true, transition: .immediate) self.isMinimizing = false - + Queue.mainQueue().after(0.45, { self.dim.isHidden = false }) } - + fileprivate func updateSelectionCount(_ count: Int, animated: Bool = true) { self.selectionCount = count if let layout = self.validLayout { self.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate) } } - + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { guard !self.isDismissing else { return @@ -948,7 +951,7 @@ public class AttachmentController: ViewController, MinimizableController { } } } - + func switchToController(_ type: AttachmentButtonType, animated: Bool = true) -> Bool { guard self.currentType != type else { if self.isAnimating { @@ -1001,13 +1004,13 @@ public class AttachmentController: ViewController, MinimizableController { strongSelf.updateIsPanelVisible(isVisible, transition: transition) } } - + controller.cancelPanGesture = { [weak self] in if let strongSelf = self { strongSelf.container.cancelPanGesture() } } - + controller.isContainerPanning = { [weak self] in if let strongSelf = self { return strongSelf.container.isPanning @@ -1015,7 +1018,7 @@ public class AttachmentController: ViewController, MinimizableController { return false } } - + controller.isContainerExpanded = { [weak self] in if let strongSelf = self { return strongSelf.container.isExpanded @@ -1023,14 +1026,14 @@ public class AttachmentController: ViewController, MinimizableController { return false } } - + let previousController = strongSelf.currentControllers.last strongSelf.currentControllers = [controller] - + if previousType != nil && animated { strongSelf.animateSwitchTransition(controller, previousController: previousController) } - + if let layout = strongSelf.validLayout { strongSelf.switchingController = true strongSelf.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.3, curve: .spring) : .immediate) @@ -1045,7 +1048,7 @@ public class AttachmentController: ViewController, MinimizableController { } return shouldSwitch } - + private func animateSwitchTransition(_ controller: AttachmentContainable, previousController: AttachmentContainable?) { guard let snapshotView = self.container.container.view.snapshotView(afterScreenUpdates: false) else { return @@ -1053,7 +1056,7 @@ public class AttachmentController: ViewController, MinimizableController { snapshotView.alpha = 0.0 snapshotView.frame = self.container.container.frame self.container.bottomClipNode.view.insertSubview(snapshotView, at: self.container.bottomClipNode.view.subviews.count) - + let _ = (controller.ready.get() |> filter { $0 @@ -1066,14 +1069,14 @@ public class AttachmentController: ViewController, MinimizableController { let _ = layout if case .compact = layout.metrics.widthClass { let offset = 10.0 - + let initialPosition = strongSelf.container.wrappingNode.layer.position let targetPosition = initialPosition.offsetBy(dx: 0.0, dy: offset) var startPosition = initialPosition if let presentation = strongSelf.container.wrappingNode.layer.presentation() { startPosition = presentation.position } - + strongSelf.container.wrappingNode.layer.animatePosition(from: startPosition, to: targetPosition, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in if let strongSelf = self, finished { strongSelf.container.wrappingNode.layer.animateSpring(from: NSValue(cgPoint: targetPosition), to: NSValue(cgPoint: initialPosition), keyPath: "position", duration: 0.3, delay: 0.0, initialVelocity: 0.0, damping: 70.0, removeOnCompletion: false, completion: { [weak self] finished in @@ -1084,26 +1087,26 @@ public class AttachmentController: ViewController, MinimizableController { } }) } - + snapshotView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.23, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() previousController?.resetForReuse() }) }) } - + private var isAnimating = false func animateIn() { guard let layout = self.validLayout, let controller = self.controller else { return } - + self.isAnimating = true if case .regular = layout.metrics.widthClass { if controller.animateAppearance { let targetPosition = self.position let startPosition = targetPosition.offsetBy(dx: 0.0, dy: layout.size.height) - + self.position = startPosition let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) transition.animateView(allowUserInteraction: true, { @@ -1117,25 +1120,25 @@ public class AttachmentController: ViewController, MinimizableController { ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 0.1) } else { ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) - + if case .glass = controller.style, let attachmentButton = controller.attachmentButton { let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .customSpring(damping: 110.0, initialVelocity: 1.1)) let cornersTransition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut) let scaleTransition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .customSpring(damping: 110.0, initialVelocity: 0.0)) - + let buttonTransition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut) - + let targetFrame = self.container.clipNode.view.convert(self.container.clipNode.view.bounds, to: self.view) - + let sourceButtonFrame = attachmentButton.convert(attachmentButton.bounds, to: self.view) let sourceButtonScale = sourceButtonFrame.width / targetFrame.width - + if let sourceGlassView = findParentGlassBackgroundView(attachmentButton), let glassParams = sourceGlassView.params { let containerView = ClipContainerView() containerView.update(bounds: CGRect(origin: CGPoint(x: 0.0, y: (targetFrame.height - targetFrame.width) * 0.5), size: CGSize(width: targetFrame.width, height: targetFrame.width)), topCornerRadius: targetFrame.width * 0.5, bottomCornerRadius: targetFrame.width * 0.5, boundsTransition: .immediate, cornersTransition: .immediate) containerView.frame = targetFrame self.view.addSubview(containerView) - + let localGlassView = GlassBackgroundView() localGlassView.update( size: targetFrame.size, @@ -1146,18 +1149,18 @@ public class AttachmentController: ViewController, MinimizableController { ) localGlassView.frame = CGRect(origin: .zero, size: targetFrame.size) containerView.contentView.addSubview(localGlassView) - + let initialContainerBounds = self.container.bounds let initialContainerFrame = self.container.frame let initialBottomClipRadius = self.container.bottomClipNode.cornerRadius - + let clipInnerFrame = self.container.container.view.convert(self.container.container.view.bounds, to: self.container.view) self.container.bounds = CGRect(origin: .zero, size: self.container.bounds.size) self.container.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((targetFrame.width - self.container.frame.width) / 2.0), y: -clipInnerFrame.minY), size: self.container.frame.size) self.container.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.container.bottomClipNode.cornerRadius = 0.0 containerView.contentView.addSubnode(self.container) - + let buttonIcon = GlassBackgroundView.ContentImageView() let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } buttonIcon.image = PresentationResourcesChat.chatInputPanelAttachmentButtonImage(presentationData.theme) @@ -1169,8 +1172,9 @@ public class AttachmentController: ViewController, MinimizableController { } localGlassView.contentView.addSubview(buttonIcon) ComponentTransition(buttonTransition).animateBlur(layer: buttonIcon.layer, fromRadius: 0.0, toRadius: 10.0) - - containerView.update(bounds: CGRect(origin: .zero, size: targetFrame.size), topCornerRadius: 38.0, bottomCornerRadius: layout.deviceMetrics.screenCornerRadius - 2.0, boundsTransition: scaleTransition, cornersTransition: cornersTransition) + + let containerCornerRadius = max(24.0, layout.deviceMetrics.screenCornerRadius) + containerView.update(bounds: CGRect(origin: .zero, size: targetFrame.size), topCornerRadius: 38.0, bottomCornerRadius: containerCornerRadius - 2.0, boundsTransition: scaleTransition, cornersTransition: cornersTransition) scaleTransition.animateBounds(layer: containerView.layer, from: CGRect(origin: .zero, size: CGSize(width: targetFrame.width, height: targetFrame.width))) scaleTransition.animateTransformScale(view: containerView, from: sourceButtonScale) positionTransition.animatePosition(layer: containerView.layer, from: sourceButtonFrame.center, to: containerView.center, completion: { _ in @@ -1187,7 +1191,7 @@ public class AttachmentController: ViewController, MinimizableController { } else { let targetPosition = self.container.position let startPosition = targetPosition.offsetBy(dx: 0.0, dy: layout.size.height) - + self.container.position = startPosition let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) transition.animateView(allowUserInteraction: true, { @@ -1198,19 +1202,19 @@ public class AttachmentController: ViewController, MinimizableController { } } } - + func animateOut(damping: CGFloat? = nil, initialVelocity: CGFloat? = nil, completion: @escaping () -> Void = {}) { guard let controller = self.controller else { return } self.isDismissing = true - + guard let layout = self.validLayout else { return } - + self.isAnimating = true - + switch layout.metrics.widthClass { case .regular: self.layer.allowsGroupOpacity = true @@ -1222,25 +1226,27 @@ public class AttachmentController: ViewController, MinimizableController { case .compact: let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) - + if case .glass = controller.style, let attachmentButton = controller.attachmentButton { let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .customSpring(damping: 110.0, initialVelocity: initialVelocity ?? 0.0)) let cornersTransition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity ?? 0.0)) let scaleTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity ?? 0.0)) - + let buttonTransition = ContainedViewLayoutTransition.animated(duration: 0.22, curve: .easeInOut) let initialFrame = self.container.clipNode.view.convert(self.container.clipNode.view.bounds, to: self.view) - + let targetButtonFrame = attachmentButton.convert(attachmentButton.bounds, to: self.view) let targetButtonScale = targetButtonFrame.width / initialFrame.width - + if let sourceGlassView = findParentGlassBackgroundView(attachmentButton), let glassParams = sourceGlassView.params { let containerView = ClipContainerView() containerView.frame = initialFrame - containerView.update(bounds: CGRect(origin: .zero, size: initialFrame.size), topCornerRadius: 38.0, bottomCornerRadius: layout.deviceMetrics.screenCornerRadius - 2.0, boundsTransition: .immediate, cornersTransition: .immediate) - self.view.addSubview(containerView) + let containerCornerRadius = max(24.0, layout.deviceMetrics.screenCornerRadius) + containerView.update(bounds: CGRect(origin: .zero, size: initialFrame.size), topCornerRadius: 38.0, bottomCornerRadius: containerCornerRadius - 2.0, boundsTransition: .immediate, cornersTransition: .immediate) + self.view.addSubview(containerView) + let localGlassView = GlassBackgroundView() localGlassView.update( size: initialFrame.size, @@ -1251,14 +1257,14 @@ public class AttachmentController: ViewController, MinimizableController { ) localGlassView.frame = CGRect(origin: .zero, size: initialFrame.size) containerView.contentView.addSubview(localGlassView) - + let clipInnerFrame = self.container.container.view.convert(self.container.container.view.bounds, to: self.container.view) self.container.bounds = CGRect(origin: .zero, size: self.container.bounds.size) self.container.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((initialFrame.width - self.container.frame.width) / 2.0), y: -clipInnerFrame.minY), size: self.container.frame.size) self.container.isUserInteractionEnabled = false self.container.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false) containerView.contentView.addSubnode(self.container) - + let buttonIcon = GlassBackgroundView.ContentImageView() let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } buttonIcon.image = PresentationResourcesChat.chatInputPanelAttachmentButtonImage(presentationData.theme) @@ -1271,19 +1277,19 @@ public class AttachmentController: ViewController, MinimizableController { localGlassView.contentView.addSubview(buttonIcon) ComponentTransition(buttonTransition).animateBlur(layer: buttonIcon.layer, fromRadius: 10.0, toRadius: 0.0) ComponentTransition(buttonTransition).animateBlur(layer: sourceGlassView.contentView.layer, fromRadius: 10.0, toRadius: 0.0) - + containerView.update(bounds: CGRect(origin: CGPoint(x: 0.0, y: (initialFrame.height - initialFrame.width) * 0.5), size: CGSize(width: initialFrame.width, height: initialFrame.width)), topCornerRadius: initialFrame.width * 0.5, bottomCornerRadius: initialFrame.width * 0.5, boundsTransition: scaleTransition, cornersTransition: cornersTransition) scaleTransition.updateBounds(layer: containerView.layer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: initialFrame.width, height: initialFrame.width))) - + scaleTransition.updateTransformScale(layer: containerView.layer, scale: targetButtonScale) positionTransition.updatePosition(layer: containerView.layer, position: targetButtonFrame.center, completion: { [weak self] _ in let _ = self?.container.dismiss(transition: .immediate, completion: completion) self?.isAnimating = false }) - + localGlassView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, delay: 0.2, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false) scaleTransition.animateTransformScale(view: sourceGlassView, from: 1.0 / targetButtonScale) - + positionTransition.animatePosition(layer: sourceGlassView.layer, from: self.view.convert(initialFrame.center, to: sourceGlassView.superview), to: sourceGlassView.center) } } else { @@ -1292,11 +1298,11 @@ public class AttachmentController: ViewController, MinimizableController { let _ = self?.container.dismiss(transition: .immediate, completion: completion) self?.isAnimating = false }) - + if controller.style != .glass || self.didMaximizeOnce { self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) } - + if controller.fromMenu && self.hasButton, let (_, _, getTransition) = controller.getInputContainerNode(), let inputTransition = getTransition() { self.panel.animateTransitionOut(inputTransition: inputTransition, dismissed: true, transition: positionTransition) self.containerLayoutUpdated(layout, transition: positionTransition) @@ -1304,11 +1310,11 @@ public class AttachmentController: ViewController, MinimizableController { } } } - + func scrollToTop() { self.currentControllers.last?.scrollToTop?() } - + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let controller = self.controller, controller.isInteractionDisabled() { return self.view @@ -1320,14 +1326,14 @@ public class AttachmentController: ViewController, MinimizableController { return result } } - + private var isUpdatingContainer = false private var switchingController = false - + private var hasButton = false - + private var isPanelVisible: Bool = true - + private func updateIsPanelVisible(_ isVisible: Bool, transition: ContainedViewLayoutTransition) { if self.isPanelVisible == isVisible { return @@ -1337,18 +1343,18 @@ public class AttachmentController: ViewController, MinimizableController { self.containerLayoutUpdated(layout, transition: transition) } } - + func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout - + guard let controller = self.controller else { return } - + transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 2.0))) - + let fromMenu = controller.fromMenu - + var containerLayout = layout let containerRect: CGRect if case .regular = layout.metrics.widthClass { @@ -1360,12 +1366,12 @@ public class AttachmentController: ViewController, MinimizableController { } else { let inputHeight = layout.inputHeight ?? 0.0 let availableHeight = layout.size.height - inputHeight - + let size = CGSize(width: 390.0, height: min(620.0, availableHeight)) - + let insets = layout.insets(options: [.input]) let masterWidth = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0)) - + let position: CGPoint let positionY = layout.size.height - size.height - insets.bottom - (inputHeight > 0.0 ? 0.0 : 54.0) if let sourceRect = controller.getSourceRect?() { @@ -1373,7 +1379,7 @@ public class AttachmentController: ViewController, MinimizableController { } else { position = CGPoint(x: masterWidth - 174.0, y: positionY) } - + if controller.isStandalone && !controller.forceSourceRect { var containerY = floorToScreenPixels((layout.size.height - size.height) / 2.0) if let inputHeight = layout.inputHeight, inputHeight > 88.0 { @@ -1386,7 +1392,7 @@ public class AttachmentController: ViewController, MinimizableController { containerLayout.size = containerRect.size containerLayout.intrinsicInsets.bottom = 12.0 containerLayout.inputHeight = nil - + if controller.isStandalone { self.wrapperNode.cornerRadius = 10.0 } else if self.wrapperNode.view.mask == nil { @@ -1395,11 +1401,11 @@ public class AttachmentController: ViewController, MinimizableController { maskView.contentMode = .scaleToFill self.wrapperNode.view.mask = maskView } - + if let maskView = self.wrapperNode.view.mask { transition.updateFrame(view: maskView, frame: CGRect(origin: CGPoint(), size: size)) } - + self.shadowNode.alpha = 1.0 if self.shadowNode.image == nil { self.shadowNode.image = generateShadowImage(glass: controller.style == .glass) @@ -1417,13 +1423,13 @@ public class AttachmentController: ViewController, MinimizableController { containerHeight = layout.size.height } containerRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: containerHeight)) - + self.wrapperNode.cornerRadius = 0.0 self.shadowNode.alpha = 0.0 - + self.wrapperNode.view.mask = nil } - + var containerInsets = containerLayout.intrinsicInsets var hasPanel = false let hasButton = self.panel.isButtonVisible && !self.isDismissing @@ -1434,7 +1440,7 @@ public class AttachmentController: ViewController, MinimizableController { if !self.isPanelVisible && !self.panel.hasMediaAccessoryPanel { hasPanel = false } - + var panelOffset: CGFloat = 0.0 if case .glass = controller.style { if layout.metrics.isTablet { @@ -1443,14 +1449,14 @@ public class AttachmentController: ViewController, MinimizableController { panelOffset = 8.0 } } - + let isEffecitvelyCollapsedUpdated = (self.selectionCount > 0) != (self.panel.isSelecting) let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, selectionCount: self.selectionCount, elevateProgress: !hasPanel && !hasButton, hideButtons: !self.isPanelVisible && self.panel.hasMediaAccessoryPanel, transition: transition) - + if hasPanel || hasButton { containerInsets.bottom = panelHeight + panelOffset } - + var panelTransition = transition if isEffecitvelyCollapsedUpdated { if case .glass = controller.style { @@ -1466,59 +1472,59 @@ public class AttachmentController: ViewController, MinimizableController { panelY = containerRect.height } panelTransition.updateFrame(node: self.panel, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY), size: CGSize(width: containerRect.width, height: panelHeight))) - + var shadowFrame = containerRect.insetBy(dx: -60.0, dy: -60.0) shadowFrame.size.height -= 12.0 transition.updateFrame(node: self.shadowNode, frame: shadowFrame) transition.updateFrame(node: self.wrapperNode, frame: containerRect) - + if !self.isUpdatingContainer && !self.isDismissing { self.isUpdatingContainer = true - + let containerTransition: ContainedViewLayoutTransition if self.container.supernode == nil { containerTransition = .immediate } else { containerTransition = transition } - + let controllers = self.currentControllers if !self.isAnimating { containerTransition.updateFrame(node: self.container, frame: CGRect(origin: CGPoint(), size: containerRect.size)) } - + let containerLayout = containerLayout.withUpdatedIntrinsicInsets(containerInsets) - + self.container.update(layout: containerLayout, controllers: controllers, coveredByModalTransition: 0.0, transition: self.switchingController ? .immediate : transition) - + if self.container.supernode == nil, !controllers.isEmpty && self.container.isReady && !self.isDismissing && !self.isAnimating { self.wrapperNode.addSubnode(self.container) - + if fromMenu, let _ = controller.getInputContainerNode() { self.addSubnode(self.panel) } else { self.container.addSubnode(self.panel) } - + self.animateIn() } - + self.isUpdatingContainer = false } } } - + public var requestController: (AttachmentButtonType, @escaping (AttachmentContainable?, AttachmentMediaPickerContext?) -> Void) -> Bool = { _, completion in completion(nil, nil) return true } - + public var getInputContainerNode: () -> (CGFloat, ASDisplayNode, () -> AttachmentController.InputPanelTransition?)? = { return nil } - + public var getSourceRect: (() -> CGRect?)? - + public var shouldMinimizeOnSwipe: ((AttachmentButtonType?) -> Bool)? - + public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, @@ -1530,7 +1536,8 @@ public class AttachmentController: ViewController, MinimizableController { fromMenu: Bool = false, hasTextInput: Bool = true, isFullSize: Bool = false, - makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil }) + makeEntityInputView: @escaping () -> UIView? = { return nil }, + customEmojiAvailable: Bool = true) { self.context = context self.updatedPresentationData = updatedPresentationData @@ -1542,35 +1549,35 @@ public class AttachmentController: ViewController, MinimizableController { self.fromMenu = fromMenu self.hasTextInput = hasTextInput self.isFullSize = isFullSize - self.makeEntityInputView = makeEntityInputView - + self.customEmojiAvailable = customEmojiAvailable + super.init(navigationBarPresentationData: nil) - + self._hasGlassStyle = true //style == .glass - + self.statusBar.statusBarStyle = .Ignore self.blocksBackgroundWhenInOverlay = true self.acceptsFocusWhenInOverlay = true - + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil) - + self.scrollToTop = { [weak self] in if let strongSelf = self { strongSelf.node.scrollToTop() } } } - + public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + public var forceSourceRect = false - + fileprivate var isStandalone: Bool { return self.buttons.contains(.standalone) || self.buttons.count == 1 } - + public func convertToStandalone() { guard self.buttons != [.standalone] else { return @@ -1582,37 +1589,37 @@ public class AttachmentController: ViewController, MinimizableController { self.hasTextInput = false self.requestLayout(transition: .immediate) } - + public func minimizeIfNeeded() { if self.shouldMinimizeOnSwipe?(self.node.currentType) == true { self.node.minimize() } } - + public func updateSelectionCount(_ count: Int) { self.node.updateSelectionCount(count, animated: false) } - + private var node: Node { return self.displayNode as! Node } - + open override func loadDisplayNode() { - self.displayNode = Node(controller: self, makeEntityInputView: self.makeEntityInputView) + self.displayNode = Node(controller: self) self.displayNodeDidLoad() } - + private var dismissedFlag = false public func _dismiss() { super.dismiss(animated: false, completion: {}) } - + public var ensureUnfocused = true - + public func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) { self.node.minimize() } - + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { if self.ensureUnfocused { self.view.endEditing(true) @@ -1638,31 +1645,31 @@ public class AttachmentController: ViewController, MinimizableController { self.node.container.removeFromSupernode() } } - + private func isInteractionDisabled() -> Bool { return false } - + public var isMinimized: Bool = false { didSet { self.mainController.isMinimized = self.isMinimized } } - + public var isMinimizable: Bool { return self.mainController.isMinimizable } - + public func shouldDismissImmediately() -> Bool { return self.mainController.shouldDismissImmediately() } - + private var validLayout: ContainerViewLayout? - + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { let previousSize = self.validLayout?.size super.containerLayoutUpdated(layout, transition: transition) - + self.validLayout = layout if let previousSize, previousSize != layout.size { Queue.mainQueue().after(0.1) { @@ -1671,11 +1678,11 @@ public class AttachmentController: ViewController, MinimizableController { } self.node.containerLayoutUpdated(layout, transition: transition) } - + public var mainController: AttachmentContainable { return self.node.currentControllers.first! } - + public final class InputPanelTransition { let inputNode: ASDisplayNode let accessoryPanelNode: ASDisplayNode? @@ -1703,7 +1710,7 @@ public class AttachmentController: ViewController, MinimizableController { self.prepareForDismiss = prepareForDismiss } } - + public static func preloadAttachBotIcons(context: AccountContext) -> DisposableSet { let disposableSet = DisposableSet() let _ = (context.engine.messages.attachMenuBots() @@ -1711,17 +1718,17 @@ public class AttachmentController: ViewController, MinimizableController { |> deliverOnMainQueue).startStandalone(next: { bots in for bot in bots { for (name, file) in bot.icons { - if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer._asPeer()) { + if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer) { if case .placeholder = name { let path = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation()) if !FileManager.default.fileExists(atPath: path) { let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let accountResource = context.account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedPreparedSvgRepresentation(), complete: false, fetch: true) - - let fetchedFullSize = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: file), reference: .media(media: .attachBot(peer: peer, media: file), resource: file.resource)) + + let fetchedFullSize = context.engine.resources.fetch(reference: .media(media: .attachBot(peer: peer, media: file), resource: file.resource), userLocation: .other, userContentType: MediaResourceUserContentType(file: file)) let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = accountResource.start() - + return ActionDisposable { fetchedFullSizeDisposable.dispose() fullSizeDisposable.dispose() @@ -1738,7 +1745,7 @@ public class AttachmentController: ViewController, MinimizableController { }) return disposableSet } - + public func makeContentSnapshotView() -> UIView? { let snapshotView = self.view.snapshotView(afterScreenUpdates: false) let navigationBarHeight = self.validLayout.flatMap { self.navigationLayout(layout: $0) }?.defaultContentHeight ?? 56.0 @@ -1766,30 +1773,30 @@ private func findParentGlassBackgroundView(_ view: UIView) -> GlassBackgroundVie private final class ClipContainerView: UIView { private let clipView = UIView() let contentView = UIView() - + override init(frame: CGRect) { super.init(frame: frame) - + self.clipView.clipsToBounds = true self.clipView.layer.cornerCurve = .continuous self.clipView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - + self.contentView.clipsToBounds = true self.contentView.layer.cornerCurve = .continuous self.contentView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - + self.addSubview(self.clipView) self.clipView.addSubview(self.contentView) } - + required init?(coder: NSCoder) { preconditionFailure() } - + func update(bounds: CGRect, topCornerRadius: CGFloat, bottomCornerRadius: CGFloat, boundsTransition: ContainedViewLayoutTransition, cornersTransition: ContainedViewLayoutTransition) { cornersTransition.updateCornerRadius(layer: self.clipView.layer, cornerRadius: topCornerRadius) cornersTransition.updateCornerRadius(layer: self.contentView.layer, cornerRadius: bottomCornerRadius) - + boundsTransition.updateFrame(view: self.clipView, frame: CGRect(origin: .zero, size: bounds.size)) boundsTransition.updatePosition(layer: self.contentView.layer, position: CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height * 0.5)) boundsTransition.updateBounds(layer: self.contentView.layer, bounds: bounds) diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index b824d68588..7c4e1b3f04 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -47,7 +47,7 @@ private final class IconComponent: Component { public let fileReference: FileMediaReference? public let animationName: String? public let tintColor: UIColor? - + public init(account: Account, name: String, fileReference: FileMediaReference?, animationName: String?, tintColor: UIColor?) { self.account = account self.name = name @@ -55,7 +55,7 @@ private final class IconComponent: Component { self.animationName = animationName self.tintColor = tintColor } - + public static func ==(lhs: IconComponent, rhs: IconComponent) -> Bool { if lhs.account !== rhs.account { return false @@ -74,23 +74,23 @@ private final class IconComponent: Component { } return false } - + public final class View: UIImageView { private var component: IconComponent? private var disposable: Disposable? - + override init(frame: CGRect) { super.init(frame: frame) } - + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + deinit { self.disposable?.dispose() } - + func update(component: IconComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { if self.component?.name != component.name || self.component?.fileReference?.media.fileId != component.fileReference?.media.fileId || self.component?.tintColor != component.tintColor { if let fileReference = component.fileReference { @@ -98,7 +98,7 @@ private final class IconComponent: Component { if !previousName.isEmpty { self.image = nil } - + self.disposable = (svgIconImageFile(account: component.account, fileReference: fileReference) |> runOn(Queue.concurrentDefaultQueue()) |> deliverOnMainQueue).startStrict(next: { [weak self] transform in @@ -120,15 +120,15 @@ private final class IconComponent: Component { } } self.component = component - + return availableSize } } - + public func makeView() -> View { return View(frame: CGRect()) } - + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } @@ -149,7 +149,7 @@ private final class AttachButtonComponent: CombinedComponent { let theme: PresentationTheme let action: () -> Void let longPressAction: () -> Void - + init( context: AccountContext, style: Style, @@ -196,7 +196,7 @@ private final class AttachButtonComponent: CombinedComponent { } return true } - + static var body: Body { let icon = Child(IconComponent.self) let animatedIcon = Child(AnimatedStickerComponent.self) @@ -209,10 +209,10 @@ private final class AttachButtonComponent: CombinedComponent { var imageFile: TelegramMediaFile? var animationFile: TelegramMediaFile? var botPeer: EnginePeer? - + let component = context.component let strings = component.strings - + switch component.type { case .gallery: name = strings.Attachment_Gallery @@ -271,7 +271,7 @@ private final class AttachButtonComponent: CombinedComponent { case .legacy: tintColor = component.isSelected ? component.theme.rootController.tabBar.selectedIconColor : component.theme.rootController.tabBar.iconColor } - + let iconSize = CGSize(width: 30.0, height: 30.0) var topInset: CGFloat = 4.0 + UIScreenPixel var spacing: CGFloat = 15.0 + UIScreenPixel @@ -279,7 +279,7 @@ private final class AttachButtonComponent: CombinedComponent { topInset += 5.0 spacing += UIScreenPixel } - + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - iconSize.width) / 2.0), y: topInset), size: iconSize) if let animationFile = animationFile { let icon = animatedIcon.update( @@ -302,10 +302,10 @@ private final class AttachButtonComponent: CombinedComponent { ) } else { var fileReference: FileMediaReference? - if let peer = botPeer.flatMap({ PeerReference($0._asPeer())}), let imageFile = imageFile { + if let peer = botPeer.flatMap({ PeerReference($0)}), let imageFile = imageFile { fileReference = .attachBot(peer: peer, media: imageFile) } - + let icon = icon.update( component: IconComponent( account: component.context.account, @@ -329,7 +329,7 @@ private final class AttachButtonComponent: CombinedComponent { case .legacy: titleFont = Font.regular(10.0) } - + let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( @@ -344,9 +344,9 @@ private final class AttachButtonComponent: CombinedComponent { availableSize: CGSize(width: 64.0, height: context.availableSize.height), transition: .immediate ) - + let size = CGSize(width: max(component.isFirstOrLast ? context.availableSize.width : 64.0, title.size.width + 24.0), height: context.availableSize.height) - + let button = button.update( component: Rectangle( color: .clear, @@ -361,7 +361,7 @@ private final class AttachButtonComponent: CombinedComponent { context.add(title .position(CGPoint(x: titleFrame.midX, y: titleFrame.midY)) ) - + context.add(button .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) .gesture(.tap { @@ -373,7 +373,7 @@ private final class AttachButtonComponent: CombinedComponent { } })) ) - + return size } } @@ -385,58 +385,58 @@ private final class LoadingProgressNode: ASDisplayNode { self.foregroundNode.backgroundColor = self.color } } - + private let foregroundNode: ASDisplayNode - + init(color: UIColor) { self.color = color - + self.foregroundNode = ASDisplayNode() self.foregroundNode.backgroundColor = color - + super.init() - + self.addSubnode(self.foregroundNode) } - + private var _progress: CGFloat = 0.0 func updateProgress(_ progress: CGFloat, animated: Bool = false) { if self._progress == progress && animated { return } - + var animated = animated if (progress < self._progress && animated) { animated = false } - + let size = self.bounds.size - + self._progress = progress - + let transition: ContainedViewLayoutTransition if animated && progress > 0.0 { transition = .animated(duration: 0.7, curve: .spring) } else { transition = .immediate } - + let alpaTransition: ContainedViewLayoutTransition if animated { alpaTransition = .animated(duration: 0.3, curve: .easeInOut) } else { alpaTransition = .immediate } - + transition.updateFrame(node: self.foregroundNode, frame: CGRect(x: -2.0, y: 0.0, width: (size.width + 4.0) * progress, height: size.height)) - + let alpha: CGFloat = progress < 0.001 || progress > 0.999 ? 0.0 : 1.0 alpaTransition.updateAlpha(node: self.foregroundNode, alpha: alpha) } - + override func layout() { super.layout() - + self.foregroundNode.cornerRadius = self.frame.height / 2.0 } } @@ -445,42 +445,42 @@ private final class BadgeNode: ASDisplayNode { private var fillColor: UIColor private var strokeColor: UIColor private var textColor: UIColor - + private let textNode: ImmediateTextNode private let backgroundNode: ASImageNode - + private let font: UIFont = Font.with(size: 15.0, design: .round, weight: .bold) - + var text: String = "" { didSet { self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) self.invalidateCalculatedLayout() } } - + init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { self.fillColor = fillColor self.strokeColor = strokeColor self.textColor = textColor - + self.textNode = ImmediateTextNode() self.textNode.isUserInteractionEnabled = false self.textNode.displaysAsynchronously = false - + self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.displayWithoutProcessing = true self.backgroundNode.displaysAsynchronously = false self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: nil, strokeWidth: 1.0) - + super.init() - + self.addSubnode(self.backgroundNode) self.addSubnode(self.textNode) - + self.isUserInteractionEnabled = false } - + func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) { self.fillColor = fillColor self.strokeColor = strokeColor @@ -488,7 +488,7 @@ private final class BadgeNode: ASDisplayNode { self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0) self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor) } - + func animateBump(incremented: Bool) { if incremented { let firstTransition = ContainedViewLayoutTransition.animated(duration: 0.1, curve: .easeInOut) @@ -512,20 +512,20 @@ private final class BadgeNode: ASDisplayNode { }) } } - + func animateOut() { let timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue self.backgroundNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil) self.textNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, delay: 0.0, timingFunction: timingFunction, removeOnCompletion: true, completion: nil) } - + func update(_ constrainedSize: CGSize) -> CGSize { let badgeSize = self.textNode.updateLayout(constrainedSize) let backgroundSize = CGSize(width: max(18.0, badgeSize.width + 8.0), height: 18.0) let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize) self.backgroundNode.frame = backgroundFrame self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: floorToScreenPixels((backgroundFrame.size.height - badgeSize.height) / 2.0) - UIScreenPixel), size: badgeSize) - + return backgroundSize } } @@ -534,7 +534,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { private var state: AttachmentMainButtonState private var panelStyle: AttachmentPanel.Style? private var size: CGSize? - + private let backgroundAnimationNode: ASImageNode private var iconNode: ASImageNode? private var iconLayer: InlineStickerItemLayer? @@ -542,35 +542,35 @@ private final class MainButtonNode: HighlightTrackingButtonNode { private var badgeNode: BadgeNode? private let statusNode: SemanticStatusNode private var progressNode: ASImageNode? - + private var shimmerView: ShimmerEffectForegroundView? private var borderView: UIView? private var borderMaskView: UIView? private var borderShimmerView: ShimmerEffectForegroundView? - + private var context: AccountContext? - + override init(pointerStyle: PointerStyle? = nil) { self.state = AttachmentMainButtonState.initial - + self.backgroundAnimationNode = ASImageNode() self.backgroundAnimationNode.displaysAsynchronously = false - + self.textNode = ImmediateTextNode() self.textNode.textAlignment = .center self.textNode.displaysAsynchronously = false - + self.statusNode = SemanticStatusNode(backgroundNodeColor: .clear, foregroundNodeColor: .white) - + super.init(pointerStyle: pointerStyle) - + self.isExclusiveTouch = true self.clipsToBounds = true - + self.addSubnode(self.backgroundAnimationNode) self.addSubnode(self.textNode) self.addSubnode(self.statusNode) - + self.highligthedChanged = { [weak self] highlighted in if let self, self.state.isEnabled { if highlighted { @@ -583,22 +583,22 @@ private final class MainButtonNode: HighlightTrackingButtonNode { } } } - + override func didLoad() { super.didLoad() - + if #available(iOS 13.0, *) { self.layer.cornerCurve = .continuous } } - + public func transitionToProgress() { guard self.progressNode == nil, let size = self.size else { return } - + self.isUserInteractionEnabled = false - + let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z") rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) rotationAnimation.duration = 1.0 @@ -607,88 +607,88 @@ private final class MainButtonNode: HighlightTrackingButtonNode { rotationAnimation.repeatCount = Float.infinity rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) rotationAnimation.beginTime = 1.0 - + let buttonOffset: CGFloat = 0.0 let buttonWidth = size.width - + let progressNode = ASImageNode() - + let diameter: CGFloat = size.height - 22.0 let progressFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(buttonOffset + (buttonWidth - diameter) / 2.0), y: floorToScreenPixels((size.height - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter)) progressNode.frame = progressFrame progressNode.image = generateIndefiniteActivityIndicatorImage(color: self.state.textColor, diameter: diameter, lineWidth: 3.0) - + self.addSubnode(progressNode) - + progressNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) progressNode.layer.add(rotationAnimation, forKey: "progressRotation") self.progressNode = progressNode - + self.textNode.alpha = 0.0 self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) - + self.iconLayer?.opacity = 0.0 self.iconLayer?.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) - + self.shimmerView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.borderShimmerView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } - + public func transitionFromProgress() { guard let progressNode = self.progressNode else { return } self.progressNode = nil - + progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak progressNode, weak self] _ in progressNode?.removeFromSupernode() self?.isUserInteractionEnabled = true }) - + self.textNode.alpha = 1.0 self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - + self.iconLayer?.opacity = 1.0 self.iconLayer?.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - + self.shimmerView?.layer.removeAllAnimations() self.shimmerView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.borderShimmerView?.layer.removeAllAnimations() self.borderShimmerView?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - + private func setupShimmering() { if self.state.hasShimmer { if self.shimmerView == nil { let shimmerView = ShimmerEffectForegroundView() shimmerView.isUserInteractionEnabled = false self.shimmerView = shimmerView - + shimmerView.layer.cornerRadius = 12.0 if #available(iOS 13.0, *) { shimmerView.layer.cornerCurve = .continuous } - + let borderView = UIView() borderView.isUserInteractionEnabled = false self.borderView = borderView - + let borderMaskView = UIView() borderMaskView.layer.borderWidth = 1.0 + UIScreenPixel borderMaskView.layer.borderColor = UIColor.white.cgColor borderMaskView.layer.cornerRadius = 12.0 borderView.mask = borderMaskView self.borderMaskView = borderMaskView - + let borderShimmerView = ShimmerEffectForegroundView() self.borderShimmerView = borderShimmerView borderView.addSubview(borderShimmerView) - + self.view.addSubview(shimmerView) self.view.addSubview(borderView) - + self.updateShimmerParameters() - + if let size = self.size, let context = self.context, let style = self.panelStyle { self.updateLayout(size: size, context: context, style: style, state: state, transition: .immediate) } @@ -698,19 +698,19 @@ private final class MainButtonNode: HighlightTrackingButtonNode { self.borderView?.removeFromSuperview() self.borderMaskView?.removeFromSuperview() self.borderShimmerView?.removeFromSuperview() - + self.shimmerView = nil self.borderView = nil self.borderMaskView = nil self.borderShimmerView = nil } } - + func updateShimmerParameters() { guard let shimmerView = self.shimmerView, let borderShimmerView = self.borderShimmerView else { return } - + let color = UIColor.white let alpha: CGFloat let borderAlpha: CGFloat @@ -724,14 +724,14 @@ private final class MainButtonNode: HighlightTrackingButtonNode { borderAlpha = 0.3 compositingFilter = nil } - + shimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(alpha), gradientSize: 70.0, globalTimeOffset: false, duration: 4.0, horizontal: true) borderShimmerView.update(backgroundColor: .clear, foregroundColor: color.withAlphaComponent(borderAlpha), gradientSize: 70.0, globalTimeOffset: false, duration: 4.0, horizontal: true) - + shimmerView.layer.compositingFilter = compositingFilter borderShimmerView.layer.compositingFilter = compositingFilter } - + private func setupGradientAnimations() { if let _ = self.backgroundAnimationNode.layer.animation(forKey: "movement") { } else { @@ -742,15 +742,15 @@ private final class MainButtonNode: HighlightTrackingButtonNode { newValue -= self.backgroundAnimationNode.frame.width * 0.35 } self.backgroundAnimationNode.position = CGPoint(x: newValue, y: self.backgroundAnimationNode.bounds.size.height / 2.0) - + CATransaction.begin() - + let animation = CABasicAnimation(keyPath: "position.x") animation.duration = 4.5 animation.fromValue = previousValue animation.toValue = newValue animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - + CATransaction.setCompletionBlock { [weak self] in self?.setupGradientAnimations() } @@ -759,31 +759,31 @@ private final class MainButtonNode: HighlightTrackingButtonNode { CATransaction.commit() } } - + func updateLayout(size: CGSize, context: AccountContext, style: AttachmentPanel.Style, state: AttachmentMainButtonState, animateBackground: Bool = false, transition: ContainedViewLayoutTransition) { let previousState = self.state self.context = context self.state = state self.panelStyle = style self.size = size - + switch style { case .glass: self.cornerRadius = size.height * 0.5 case .legacy: self.cornerRadius = 12.0 } - + self.isUserInteractionEnabled = state.isVisible - + self.setupShimmering() - + let colorUpdated = previousState.textColor != state.textColor if let progressNode = self.progressNode, colorUpdated { let diameter: CGFloat = size.height - 22.0 progressNode.image = generateIndefiniteActivityIndicatorImage(color: state.textColor, diameter: diameter, lineWidth: 3.0) } - + var textFrame: CGRect = .zero if let text = state.text { let font: UIFont @@ -794,10 +794,10 @@ private final class MainButtonNode: HighlightTrackingButtonNode { font = Font.semibold(17.0) } self.textNode.attributedText = NSAttributedString(string: text, font: font, textColor: state.textColor) - + let textSize = self.textNode.updateLayout(size) textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) - + switch state.background { case let .color(backgroundColor): self.backgroundAnimationNode.image = nil @@ -821,7 +821,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { locations.append(delta * CGFloat(i)) } self.backgroundAnimationNode.image = generateGradientImage(size: CGSize(width: 200.0, height: 50.0), colors: backgroundColors, locations: locations, direction: .horizontal) - + self.backgroundAnimationNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: size.width * 2.4, height: size.height)) if self.backgroundAnimationNode.layer.animation(forKey: "movement") == nil { self.backgroundAnimationNode.position = CGPoint(x: size.width * 2.4 / 2.0 - self.backgroundAnimationNode.frame.width * 0.35, y: size.height / 2.0) @@ -831,7 +831,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { self.backgroundColor = UIColor(rgb: 0x8878ff) } } - + if let badge = state.badge { let badgeNode: BadgeNode var badgeTransition = transition @@ -858,7 +858,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { self.badgeNode = nil badgeNode.removeFromSupernode() } - + if let iconName = state.iconName { let iconNode: ASImageNode if let current = self.iconNode { @@ -878,7 +878,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { self.iconNode = nil iconNode.removeFromSupernode() } - + if let iconCustomEmojiId = state.iconCustomEmojiId { let iconLayer: InlineStickerItemLayer if let current = self.iconLayer { @@ -898,7 +898,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { ) self.layer.addSublayer(iconLayer) self.iconLayer = iconLayer - + if transition.isAnimated { iconLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) iconLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -909,7 +909,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { iconLayer.frame = CGRect(origin: CGPoint(x: textFrame.minX - iconSize.width - 6.0, y: textFrame.minY + floorToScreenPixels((textFrame.height - iconSize.height) * 0.5)), size: iconSize) } else if let iconLayer = self.iconLayer { self.iconLayer = nil - + if transition.isAnimated { iconLayer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false) iconLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in @@ -919,14 +919,14 @@ private final class MainButtonNode: HighlightTrackingButtonNode { iconLayer.removeFromSuperlayer() } } - + if self.textNode.frame.width.isZero { self.textNode.frame = textFrame } else { self.textNode.bounds = CGRect(origin: .zero, size: textFrame.size) transition.updatePosition(node: self.textNode, position: textFrame.center) } - + if previousState.progress != state.progress { if state.progress == .center { self.transitionToProgress() @@ -934,7 +934,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { self.transitionFromProgress() } } - + if let shimmerView = self.shimmerView, let borderView = self.borderView, let borderMaskView = self.borderMaskView, let borderShimmerView = self.borderShimmerView { let buttonFrame = CGRect(origin: .zero, size: size) let buttonWidth = size.width @@ -943,14 +943,14 @@ private final class MainButtonNode: HighlightTrackingButtonNode { transition.updateFrame(view: borderView, frame: buttonFrame) transition.updateFrame(view: borderMaskView, frame: buttonFrame) transition.updateFrame(view: borderShimmerView, frame: buttonFrame) - + shimmerView.updateAbsoluteRect(CGRect(origin: CGPoint(x: buttonWidth * 4.0, y: 0.0), size: size), within: CGSize(width: buttonWidth * 9.0, height: buttonHeight)) borderShimmerView.updateAbsoluteRect(CGRect(origin: CGPoint(x: buttonWidth * 4.0, y: 0.0), size: size), within: CGSize(width: buttonWidth * 9.0, height: buttonHeight)) } - + let statusSize = CGSize(width: 20.0, height: 20.0) transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: size.width - statusSize.width - 15.0, y: floorToScreenPixels((size.height - statusSize.height) / 2.0)), size: statusSize)) - + self.statusNode.foregroundNodeColor = state.textColor self.statusNode.transitionToState(state.progress == .side ? .progress(value: nil, cancelEnabled: false, appearance: SemanticStatusNodeState.ProgressAppearance(inset: 0.0, lineWidth: 2.0), animateRotation: true) : .none) } @@ -961,9 +961,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog case glass case legacy } - + private let panelStyle: Style - + private weak var controller: AttachmentController? private let context: AccountContext private let isScheduledMessages: Bool @@ -973,47 +973,46 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog private var peerDisposable: Disposable? private var mediaStatusDisposable: Disposable? private var playlistPreloadDisposable: Disposable? - + private var iconDisposables: [MediaId: Disposable] = [:] private var playlistStateAndType: (SharedMediaPlaylistItem, SharedMediaPlaylistItem?, SharedMediaPlaylistItem?, MusicPlaybackSettingsOrder, MediaManagerPlayerType, Account)? private var playlistLocation: SharedMediaPlaylistLocation? private var mediaAccessoryPanel: (MediaNavigationAccessoryPanel, MediaManagerPlayerType)? private var dismissingMediaAccessoryPanel: ASDisplayNode? - + private var presentationInterfaceState: ChatPresentationInterfaceState private var interfaceInteraction: ChatPanelInterfaceInteraction? - - private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? - + private let customEmojiAvailable: Bool + private let containerNode: ASDisplayNode private var backgroundView: GlassBackgroundView? private var liquidLensView: LiquidLensView? private let backgroundNode: NavigationBackgroundNode private let scrollNode: ASScrollNode private let separatorNode: ASDisplayNode - + private let selectionNode: ASImageNode - + private var itemsContainer = UIView() private var itemViews: [AnyHashable: ComponentHostView] = [:] private var selectedItemsContainer = UIView() private var selectedItemViews: [AnyHashable: ComponentHostView] = [:] private var itemSizes: [AnyHashable: CGSize] = [:] - + private var tabSelectionRecognizer: TabSelectionRecognizer? private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable, isLifted: Bool)? private var lensIsLifted = false - + private var textInputPanelNode: AttachmentTextInputPanelNode? private var progressNode: LoadingProgressNode? private var mainButtonNode: MainButtonNode private var secondaryButtonNode: MainButtonNode - + private var loadingProgress: CGFloat? private var mainButtonState: AttachmentMainButtonState = .initial private var secondaryButtonState: AttachmentMainButtonState = .initial private var customBottomPanelBackgroundColor: UIColor? - + private var hideButtons: Bool = false private var elevateProgress: Bool = false private var buttons: [AttachmentButtonType] = [] @@ -1021,7 +1020,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog private var selectionOverrideIndex: Int? private(set) var isSelecting: Bool = false private var selectionCount: Int = 0 - + private var _isButtonVisible: Bool = false var isButtonVisible: Bool { return self.mainButtonState.isVisible || self.secondaryButtonState.isVisible @@ -1051,13 +1050,13 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog var hasMediaAccessoryPanel: Bool { return self.shouldDisplayMediaAccessoryPanel } - + private var validLayout: ContainerViewLayout? private var scrollLayout: (width: CGFloat, contentSize: CGSize)? - + var fromMenu: Bool = false var isStandalone: Bool = false - + var selectionChanged: (AttachmentButtonType) -> Bool = { _ in return false } var longPressed: (AttachmentButtonType) -> Void = { _ in } @@ -1068,43 +1067,43 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog var requestLayout: () -> Void = {} var present: (ViewController) -> Void = { _ in } var presentInGlobalOverlay: (ViewController) -> Void = { _ in } - + var getNavigationController: () -> NavigationController? = { nil } + var getCurrentSendMessageContextMediaPreview: (() -> ChatSendMessageContextScreenMediaPreview?)? - + var onMainButtonPressed: () -> Void = { } var onSecondaryButtonPressed: () -> Void = { } - - init(controller: AttachmentController, style: Style, context: AccountContext, chatLocation: ChatLocation?, isScheduledMessages: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { + + init(controller: AttachmentController, style: Style, context: AccountContext, chatLocation: ChatLocation?, isScheduledMessages: Bool, customEmojiAvailable: Bool, updatedPresentationData: (initial: PresentationData, signal: Signal)?) { self.controller = controller self.context = context self.panelStyle = style self.updatedPresentationData = updatedPresentationData self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.isScheduledMessages = isScheduledMessages - - self.makeEntityInputView = makeEntityInputView - - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, preferredGlassType: .default, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: chatLocation ?? .peer(id: context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) - + self.customEmojiAvailable = customEmojiAvailable + + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, preferredGlassType: .default, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: chatLocation ?? .peer(id: context.account.peerId), subject: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil).updatedCustomEmojiAvailable(customEmojiAvailable) + self.containerNode = ASDisplayNode() self.containerNode.clipsToBounds = false - + self.scrollNode = ASScrollNode() self.scrollNode.clipsToBounds = true - + self.selectionNode = ASImageNode() - + self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor) self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor - + self.mainButtonNode = MainButtonNode() self.secondaryButtonNode = MainButtonNode() - + super.init() - + self.addSubnode(self.containerNode) - + switch style { case .glass: self.scrollNode.cornerRadius = glassButtonSize.height * 0.5 @@ -1113,13 +1112,13 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog self.containerNode.addSubnode(self.separatorNode) self.containerNode.addSubnode(self.scrollNode) } - + self.addSubnode(self.secondaryButtonNode) self.addSubnode(self.mainButtonNode) - + self.mainButtonNode.addTarget(self, action: #selector(self.mainButtonPressed), forControlEvents: .touchUpInside) self.secondaryButtonNode.addTarget(self, action: #selector(self.secondaryButtonPressed), forControlEvents: .touchUpInside) - + self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _, _, _ in }, setupEditMessage: { _, _ in }, beginMessageSelection: { _, _ in @@ -1189,7 +1188,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog }, finishMediaRecording: { _ in }, stopMediaRecording: { }, lockMediaRecording: { - }, resumeMediaRecording: { + }, resumeMediaRecording: { }, deleteRecordedMedia: { }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in @@ -1213,9 +1212,10 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog }, toggleMessageStickerStarred: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in - }, getNavigationController: { - return nil - }, presentGlobalOverlayController: { _, _ in + }, getNavigationController: { [weak self] in + return self?.getNavigationController() + }, presentGlobalOverlayController: { [weak self] controller, _ in + self?.presentInGlobalOverlay(controller) }, navigateFeed: { }, openGrouping: { }, toggleSilentPost: { @@ -1237,7 +1237,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog inputMode = state.inputMode return state }) - + var link: String? if let text { text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in @@ -1270,7 +1270,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog strongSelf.present(controller) } }, openDateEditing: { - + }, displaySlowmodeTooltip: { _, _ in }, displaySendMessageOptions: { [weak self] node, gesture in guard let strongSelf = self, let textInputPanelNode = strongSelf.textInputPanelNode else { @@ -1280,12 +1280,12 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog guard let textInputNode = textInputPanelNode.textInputNode, let peerId = chatLocation?.peerId else { return } - + var hasEntityKeyboard = false if case .media = strongSelf.presentationInterfaceState.inputMode { hasEntityKeyboard = true } - + let effectItems: Signal<[ReactionItem]?, NoError> if strongSelf.presentationInterfaceState.chatLocation.peerId != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser { effectItems = effectMessageReactions(context: strongSelf.context) @@ -1293,7 +1293,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } else { effectItems = .single(nil) } - + let availableMessageEffects = strongSelf.context.availableMessageEffects |> take(1) let hasPremium = strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)) |> map { peer -> Bool in @@ -1302,7 +1302,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } return user.isPremium } - + let _ = (combineLatest( strongSelf.context.account.viewTracker.peerView(peerId) |> take(1), effectItems, @@ -1323,7 +1323,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog if peer.id.isTelegramNotifications { sendWhenOnlineAvailable = false } - + let mediaPreview = strongSelf.getCurrentSendMessageContextMediaPreview?() let isReady: Signal if let mediaPreview { @@ -1334,7 +1334,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } else { isReady = .single(true) } - + var captionIsAboveMedia: Signal = .single(false) var canMakePaidContent = false var currentPrice: Int64? @@ -1345,7 +1345,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog currentPrice = mediaPickerContext.price hasTimers = mediaPickerContext.hasTimers } - + let _ = (combineLatest( isReady, captionIsAboveMedia |> take(1), @@ -1355,7 +1355,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog guard let strongSelf else { return } - + let controller = makeChatSendMessageActionSheetController( initialData: initialData, context: strongSelf.context, @@ -1437,7 +1437,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog }, openSendAsPeer: { _, _ in }, presentChatRequestAdminInfo: { }, displayCopyProtectionTip: { _, _ in - }, openWebView: { _, _, _, _ in + }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in }, insertText: { _ in }, backwardsDeleteText: { @@ -1453,7 +1453,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog }, openMessagePayment: { }, openBoostToUnrestrict: { }, updateRecordingTrimRange: { _, _, _, _ in - }, dismissAllTooltips: { + }, dismissAllTooltips: { }, editTodoMessage: { _, _, _ in }, dismissUrlPreview: { }, dismissForwardMessages: { @@ -1471,7 +1471,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog }, chatController: { return nil }, statuses: nil) - + if case .glass = style { self.mediaStatusDisposable = (context.sharedContext.mediaManager.globalMediaPlayerState |> mapToSignal { playlistStateAndType -> Signal<(Account, SharedMediaPlayerItemPlaybackState, MediaManagerPlayerType)?, NoError> in @@ -1497,7 +1497,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog strongSelf.playlistStateAndType?.3 != playlistStateAndType?.1.order || strongSelf.playlistStateAndType?.4 != playlistStateAndType?.2 || strongSelf.peerMessagesPlaylistLocation != updatedPeerMessagesPlaylistLocation { - + if let playlistStateAndType { strongSelf.playlistStateAndType = ( playlistStateAndType.1.item, @@ -1512,7 +1512,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog strongSelf.playlistStateAndType = nil strongSelf.playlistLocation = nil } - + if hadMediaAccessoryPanel != strongSelf.hasMediaAccessoryPanel { strongSelf.requestLayout() } else if let layout = strongSelf.validLayout { @@ -1529,12 +1529,12 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } }) } - + self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in if let strongSelf = self { strongSelf.presentationData = presentationData - + strongSelf.backgroundNode.updateColor(color: strongSelf.customBottomPanelBackgroundColor ?? presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate) strongSelf.separatorNode.backgroundColor = presentationData.theme.rootController.tabBar.separatorColor strongSelf.selectionNode.backgroundColor = presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05) @@ -1546,15 +1546,15 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog strongSelf.dismissingMediaAccessoryPanel = nil dismissingMediaAccessoryPanel.removeFromSupernode() } - + strongSelf.updateChatPresentationInterfaceState({ $0.updatedTheme(presentationData.theme) }) - + if let layout = strongSelf.validLayout { let _ = strongSelf.update(layout: layout, buttons: strongSelf.buttons, isSelecting: strongSelf.isSelecting, selectionCount: strongSelf.selectionCount, elevateProgress: strongSelf.elevateProgress, hideButtons: strongSelf.hideButtons, transition: .immediate) } } }).strict() - + if let peerId = chatLocation?.peerId { self.peerDisposable = ((self.context.account.viewTracker.peerView(peerId) |> map { view -> StarsAmount? in @@ -1581,7 +1581,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog })) } } - + deinit { self.presentationDataDisposable?.dispose() self.peerDisposable?.dispose() @@ -1591,34 +1591,34 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog disposable.dispose() } } - + override func didLoad() { super.didLoad() if #available(iOS 13.0, *) { self.containerNode.layer.cornerCurve = .continuous } - + self.scrollNode.view.delegate = self.wrappedScrollViewDelegate self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.showsVerticalScrollIndicator = false - + self.view.accessibilityTraits = .tabBar } - + func requestLayout(transition: ContainedViewLayoutTransition) { if let layout = self.validLayout { let _ = self.update(layout: layout, buttons: self.buttons, isSelecting: self.isSelecting, selectionCount: self.selectionCount, elevateProgress: self.elevateProgress, hideButtons: self.hideButtons, transition: transition) } } - + @objc private func mainButtonPressed() { self.onMainButtonPressed() } - + @objc private func secondaryButtonPressed() { self.onSecondaryButtonPressed() } - + private func mediaPlaybackStatusSignal() -> Signal { let delayedStatus = self.context.sharedContext.mediaManager.globalMediaPlayerState |> mapToSignal { value -> Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> in @@ -1632,7 +1632,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog return .single(value) |> delay(0.1, queue: .mainQueue()) } } - + return delayedStatus |> map { state -> MediaPlayerStatus in if let stateOrLoading = state?.1, case let .state(state) = stateOrLoading { @@ -1642,7 +1642,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } } } - + private func dismissMediaAccessoryPanel(transition: ContainedViewLayoutTransition) { guard let (mediaAccessoryPanel, _) = self.mediaAccessoryPanel else { return @@ -1656,12 +1656,12 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } }) } - + private func presentAudioRateTooltip(baseRate: AudioPlaybackRate, changeType: MediaNavigationAccessoryPanel.ChangeType) { guard let controller = self.controller else { return } - + var hasTooltip = false controller.forEachController({ controller in if let controller = controller as? UndoOverlayController { @@ -1670,7 +1670,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } return true }) - + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let text: String? let rate: CGFloat? @@ -1701,12 +1701,12 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog text = nil rate = nil } - + var showTooltip = true if case .sliderChange = changeType { showTooltip = false } - + if let rate, let text, showTooltip { controller.present( UndoOverlayController( @@ -1725,7 +1725,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog ) } } - + private func makeMediaAccessoryPanel() -> MediaNavigationAccessoryPanel { let mediaAccessoryPanel = MediaNavigationAccessoryPanel(context: self.context, presentationData: self.presentationData, displayBackground: false, customTintColor: self.presentationData.theme.rootController.tabBar.textColor) mediaAccessoryPanel.getController = { [weak self] in @@ -1745,7 +1745,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } let _ = (self.context.sharedContext.accountManager.transaction { transaction -> AudioPlaybackRate in let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings)?.get(MusicPlaybackSettings.self) ?? MusicPlaybackSettings.defaultSettings - + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings, { _ in return PreferencesEntry(settings.withUpdatedVoicePlaybackRate(rate)) }) @@ -1778,13 +1778,13 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } return mediaAccessoryPanel } - + private func updateMediaAccessoryPanel(frame: CGRect?, transition: ContainedViewLayoutTransition) { guard case .glass = self.panelStyle, self.hasMediaAccessoryPanel, let frame, let (item, previousItem, nextItem, order, type, _) = self.playlistStateAndType else { self.dismissMediaAccessoryPanel(transition: transition) return } - + let mediaAccessoryPanel: MediaNavigationAccessoryPanel let isNewPanel: Bool if let (currentPanel, currentType) = self.mediaAccessoryPanel, currentType == type { @@ -1802,7 +1802,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog self.mediaAccessoryPanel = (mediaAccessoryPanel, type) isNewPanel = true } - + mediaAccessoryPanel.containerNode.headerNode.displayScrubber = item.playbackData?.type != .instantVideo mediaAccessoryPanel.containerNode.headerNode.playbackStatus = self.mediaPlaybackStatusSignal() switch order { @@ -1813,7 +1813,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog case .random: mediaAccessoryPanel.containerNode.headerNode.playbackItems = (item, nil, nil) } - + let inset: CGFloat = self.hideButtons ? 19.0 : 0.0 if isNewPanel { mediaAccessoryPanel.updateLayout(size: frame.size, leftInset: inset, rightInset: inset, isHidden: false, transition: .immediate) @@ -1822,15 +1822,15 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog transition.updateFrame(node: mediaAccessoryPanel, frame: frame) mediaAccessoryPanel.updateLayout(size: frame.size, leftInset: inset, rightInset: inset, isHidden: false, transition: transition) } - + transition.updateAlpha(node: mediaAccessoryPanel.containerNode.headerNode.separatorNode, alpha: self.hideButtons ? 0.0 : 1.0) } - + func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) { transition.updateAlpha(node: self.separatorNode, alpha: alpha) transition.updateAlpha(node: self.backgroundNode, alpha: alpha) } - + func updateCaption(_ caption: NSAttributedString) { if !caption.string.isEmpty { self.loadTextNodeIfNeeded() @@ -1841,23 +1841,23 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog private func updateChatPresentationInterfaceState(animated: Bool = true, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, f, completion: completion) } - + private func updateChatPresentationInterfaceState(update: Bool = true, transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { let presentationInterfaceState = f(self.presentationInterfaceState) - + let updateInputTextState = self.presentationInterfaceState.interfaceState.effectiveInputState != presentationInterfaceState.interfaceState.effectiveInputState - + self.presentationInterfaceState = presentationInterfaceState - + if update { if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated) - + self.textUpdated(presentationInterfaceState.interfaceState.effectiveInputState.inputText) } } } - + func updateSelectedIndex(_ index: Int) { let hadMediaAccessoryPanel = self.hasMediaAccessoryPanel self.selectedIndex = index @@ -1866,7 +1866,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog self.requestLayout(transition: .immediate) } } - + var buttonSize: CGSize { switch self.panelStyle { case .glass: @@ -1875,7 +1875,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog return legacyButtonSize } } - + private func item(at point: CGPoint) -> AnyHashable? { let contentOffset = self.scrollNode.view.contentOffset.x let point = point.offsetBy(dx: contentOffset, dy: 0.0) @@ -1896,7 +1896,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } return closestItem?.0 } - + @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { guard let liquidLensView = self.liquidLensView else { return @@ -1907,7 +1907,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog if let itemId = self.item(at: location), let itemView = self.itemViews[itemId] { let startX = itemView.frame.minX self.selectionGestureState = (startX, startX, itemId, !self.scrollNode.view.isScrollEnabled) - + self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } case .changed: @@ -1924,9 +1924,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog if !selectionGestureState.isLifted { self.lensIsLifted = true self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) - + self.liquidLensView?.clipsToBounds = false - + Queue.mainQueue().after(0.1, { self.lensIsLifted = false self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) @@ -1935,7 +1935,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog }) }) } - + self.selectionGestureState = nil if case .ended = recognizer.state { guard let index = self.buttons.firstIndex(where: { AnyHashable($0.key) == selectionGestureState.itemId }) else { @@ -1948,7 +1948,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog self.selectedIndex = index if self.buttons.count > 5, let button = self.itemViews[button.key] { let transition = ComponentTransition.spring(duration: 0.4) - + let scrollView = self.scrollNode.view let targetRect = button.frame.insetBy(dx: -35.0, dy: 0.0) @@ -1976,22 +1976,22 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog break } } - + func updateViews(transition: ComponentTransition) { guard let layout = self.validLayout else { return } - + var buttons = self.buttons if buttons.count == 1 { buttons.removeAll() } - + var width = layout.size.width if buttons.count == 3 { width = smallPanelWidth } - + var panelSideInset: CGFloat switch self.panelStyle { case .glass: @@ -1999,7 +1999,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog case .legacy: panelSideInset = 3.0 } - + var distanceBetweenNodes = floorToScreenPixels((width - panelSideInset * 2.0 - self.buttonSize.width) / CGFloat(max(1, buttons.count - 1))) if buttons.count == 3 { distanceBetweenNodes = floorToScreenPixels((width - panelSideInset * 2.0 - 32.0) / CGFloat(max(1, buttons.count - 1))) @@ -2020,7 +2020,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog if buttons.count == 3 { leftNodeOriginX = floor((layout.size.width - width) / 2.0) + 16.0 } - + if buttons.count > maxButtonsToFit && layout.size.width < layout.size.height { switch self.panelStyle { case .glass: @@ -2032,19 +2032,19 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } leftNodeOriginX = layout.safeInsets.left + buttonWidth / 2.0 } - + var validIds = Set() - + var selectionFrame = CGRect() var mostRightX = 0.0 for i in 0 ..< buttons.count { let originX = floorToScreenPixels(leftNodeOriginX + CGFloat(i) * distanceBetweenNodes - buttonWidth / 2.0) let buttonFrame = CGRect(origin: CGPoint(x: originX, y: -3.0), size: CGSize(width: buttonWidth, height: self.buttonSize.height)) mostRightX = buttonFrame.maxX - + let type = buttons[i] let _ = validIds.insert(type.key) - + var buttonTransition = transition let buttonView: ComponentHostView let selectedButtonView: ComponentHostView @@ -2056,27 +2056,27 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog buttonView = ComponentHostView() self.itemViews[type.key] = buttonView self.itemsContainer.addSubview(buttonView) - + selectedButtonView = ComponentHostView() self.selectedItemViews[type.key] = selectedButtonView self.selectedItemsContainer.addSubview(selectedButtonView) } - + if case let .app(bot) = type { for (name, file) in bot.icons { if [.default, .iOSAnimated, .iOSSettingsStatic, .placeholder].contains(name) { - if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(bot.peer._asPeer()) { + if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(bot.peer) { if case .placeholder = name { let account = self.context.account let path = account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation()) if !FileManager.default.fileExists(atPath: path) { let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let accountResource = account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedPreparedSvgRepresentation(), complete: false, fetch: true) - + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: file), reference: .media(media: .attachBot(peer: peer, media: file), resource: file.resource)) let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = accountResource.start() - + return ActionDisposable { fetchedFullSizeDisposable.dispose() fullSizeDisposable.dispose() @@ -2107,7 +2107,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog if strongSelf.selectionChanged(type) { strongSelf.selectedIndex = i strongSelf.updateViews(transition: .init(animation: .curve(duration: 0.2, curve: .spring))) - + if buttons.count > 5, let button = strongSelf.itemViews[type.key] { strongSelf.scrollNode.view.scrollRectToVisible(button.frame.insetBy(dx: -35.0, dy: 0.0), animated: true) } @@ -2124,7 +2124,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog containerSize: CGSize(width: buttonWidth, height: self.buttonSize.height) ) self.itemSizes[type.key] = actualButtonSize - + let _ = selectedButtonView.update( transition: buttonTransition, component: AnyComponent(AttachButtonComponent( @@ -2143,7 +2143,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog environment: {}, containerSize: CGSize(width: buttonWidth, height: self.buttonSize.height) ) - + if i == self.selectedIndex { selectionFrame = CGRect(origin: CGPoint(x: buttonFrame.midX - actualButtonSize.width * 0.5, y: buttonFrame.minY), size: actualButtonSize) } @@ -2192,20 +2192,20 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog for id in removeIds { self.itemViews.removeValue(forKey: id) } - + selectionFrame = selectionFrame.insetBy(dx: 1.0, dy: 4.0) self.selectionNode.cornerRadius = selectionFrame.height * 0.5 transition.setFrame(view: self.selectionNode.view, frame: selectionFrame) - + mostRightX += layout.safeInsets.right + 3.0 - + let contentSize = CGSize(width: mostRightX, height: self.buttonSize.height) if contentSize != self.scrollNode.view.contentSize && self.scrollNode.view.bounds.width > 0.0 { self.scrollNode.view.contentSize = contentSize self.scrollNode.view.isScrollEnabled = contentSize.width - self.scrollNode.view.bounds.width > 1.0 self.liquidLensView?.clipsToBounds = self.scrollNode.view.isScrollEnabled } - + if !self.hideButtons && self.itemsContainer.isHidden { Queue.mainQueue().after(0.3, { self.itemsContainer.isHidden = false @@ -2216,17 +2216,21 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog self.selectedItemsContainer.isHidden = self.hideButtons } } - + private func loadTextNodeIfNeeded() { if let _ = self.textInputPanelNode { } else { - let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, glass: self.panelStyle == .glass, isAttachment: true, isScheduledMessages: self.isScheduledMessages, presentController: { [weak self] c in + let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, glass: self.panelStyle == .glass, isAttachment: true, isScheduledMessages: self.isScheduledMessages, customEmojiAvailable: self.customEmojiAvailable, presentController: { [weak self] c in if let strongSelf = self { strongSelf.present(c) } - }, makeEntityInputView: self.makeEntityInputView) + }, presentInGlobalOverlay: { [weak self] c in + self?.presentInGlobalOverlay(c) + }, getNavigationController: { [weak self] in + return self?.getNavigationController() + }) 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 @@ -2252,16 +2256,16 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } self.addSubnode(textInputPanelNode) self.textInputPanelNode = textInputPanelNode - + textInputPanelNode.alpha = self.isSelecting ? 1.0 : 0.0 textInputPanelNode.isUserInteractionEnabled = self.isSelecting } } - + func updateLoadingProgress(_ progress: CGFloat?) { self.loadingProgress = progress } - + func updateMainButtonState(_ mainButtonState: AttachmentMainButtonState?) { var currentButtonState = self.mainButtonState if mainButtonState == nil { @@ -2269,7 +2273,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } self.mainButtonState = mainButtonState ?? currentButtonState } - + func updateSecondaryButtonState(_ secondaryButtonState: AttachmentMainButtonState?) { var currentButtonState = self.secondaryButtonState if secondaryButtonState == nil { @@ -2277,19 +2281,19 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } self.secondaryButtonState = secondaryButtonState ?? currentButtonState } - + func updateCustomBottomPanelBackgroundColor(_ color: UIColor?) { self.customBottomPanelBackgroundColor = color self.backgroundNode.updateColor(color: self.customBottomPanelBackgroundColor ?? presentationData.theme.rootController.tabBar.backgroundColor, transition: .animated(duration: 0.2, curve: .linear)) } - + let animatingTransitionPromise = ValuePromise(false) private(set) var animatingTransition = false { didSet { self.animatingTransitionPromise.set(self.animatingTransition) } } - + func animateTransitionIn(inputTransition: AttachmentController.InputPanelTransition, transition: ContainedViewLayoutTransition) { guard !self.animatingTransition, let inputNodeSnapshotView = inputTransition.inputNode.view.snapshotView(afterScreenUpdates: false) else { return @@ -2298,21 +2302,21 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog return } self.animatingTransition = true - + let targetButtonColor = self.mainButtonNode.backgroundColor self.mainButtonNode.backgroundColor = inputTransition.menuButtonBackgroundView.backgroundColor transition.updateBackgroundColor(node: self.mainButtonNode, color: targetButtonColor ?? .clear) - + transition.animateFrame(layer: self.mainButtonNode.layer, from: inputTransition.menuButtonNode.frame) transition.animatePosition(node: self.mainButtonNode.textNode, from: CGPoint(x: inputTransition.menuButtonNode.frame.width / 2.0, y: inputTransition.menuButtonNode.frame.height / 2.0)) - + let targetButtonCornerRadius = self.mainButtonNode.cornerRadius self.mainButtonNode.cornerRadius = inputTransition.menuButtonNode.cornerRadius transition.updateCornerRadius(node: self.mainButtonNode, cornerRadius: targetButtonCornerRadius) self.mainButtonNode.subnodeTransform = CATransform3DMakeScale(0.2, 0.2, 1.0) transition.updateSublayerTransformScale(node: self.mainButtonNode, scale: 1.0) self.mainButtonNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - + let menuContentDelta = (self.mainButtonNode.frame.width - inputTransition.menuButtonNode.frame.width) / 2.0 menuIconSnapshotView.frame = inputTransition.menuIconNode.frame.offsetBy(dx: inputTransition.menuButtonNode.frame.minX, dy: inputTransition.menuButtonNode.frame.minY) self.view.addSubview(menuIconSnapshotView) @@ -2320,26 +2324,26 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog menuIconSnapshotView?.removeFromSuperview() }) transition.updatePosition(layer: menuIconSnapshotView.layer, position: CGPoint(x: menuIconSnapshotView.center.x + menuContentDelta, y: self.mainButtonNode.position.y)) - + menuTextSnapshotView.frame = inputTransition.menuTextNode.frame.offsetBy(dx: inputTransition.menuButtonNode.frame.minX + 19.0, dy: inputTransition.menuButtonNode.frame.minY) self.view.addSubview(menuTextSnapshotView) menuTextSnapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak menuTextSnapshotView] _ in menuTextSnapshotView?.removeFromSuperview() }) transition.updatePosition(layer: menuTextSnapshotView.layer, position: CGPoint(x: menuTextSnapshotView.center.x + menuContentDelta, y: self.mainButtonNode.position.y)) - + inputNodeSnapshotView.clipsToBounds = true inputNodeSnapshotView.contentMode = .right inputNodeSnapshotView.frame = CGRect(x: inputTransition.menuButtonNode.frame.maxX, y: 0.0, width: inputNodeSnapshotView.frame.width - inputTransition.menuButtonNode.frame.maxX, height: inputNodeSnapshotView.frame.height) self.view.addSubview(inputNodeSnapshotView) - + let targetInputPosition = CGPoint(x: inputNodeSnapshotView.center.x + inputNodeSnapshotView.frame.width, y: self.mainButtonNode.position.y) transition.updatePosition(layer: inputNodeSnapshotView.layer, position: targetInputPosition, completion: { [weak inputNodeSnapshotView, weak self] _ in inputNodeSnapshotView?.removeFromSuperview() self?.animatingTransition = false }) } - + private var dismissed = false func animateTransitionOut(inputTransition: AttachmentController.InputPanelTransition, dismissed: Bool, transition: ContainedViewLayoutTransition) { guard !self.animatingTransition, let inputNodeSnapshotView = inputTransition.inputNode.view.snapshotView(afterScreenUpdates: false) else { @@ -2348,28 +2352,28 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog if dismissed { inputTransition.prepareForDismiss() } - + self.animatingTransition = true self.dismissed = dismissed - + let action = { guard let menuIconSnapshotView = inputTransition.menuIconNode.view.snapshotView(afterScreenUpdates: false), let menuTextSnapshotView = inputTransition.menuTextNode.view.snapshotView(afterScreenUpdates: false) else { return } - + let sourceButtonColor = self.mainButtonNode.backgroundColor transition.updateBackgroundColor(node: self.mainButtonNode, color: inputTransition.menuButtonBackgroundView.backgroundColor ?? .clear) - + let sourceButtonFrame = self.mainButtonNode.frame transition.updateFrame(node: self.mainButtonNode, frame: inputTransition.menuButtonNode.frame) let sourceButtonTextPosition = self.mainButtonNode.textNode.position transition.updatePosition(node: self.mainButtonNode.textNode, position: CGPoint(x: inputTransition.menuButtonNode.frame.width / 2.0, y: inputTransition.menuButtonNode.frame.height / 2.0)) - + let sourceButtonCornerRadius = self.mainButtonNode.cornerRadius transition.updateCornerRadius(node: self.mainButtonNode, cornerRadius: inputTransition.menuButtonNode.cornerRadius) transition.updateSublayerTransformScale(node: self.mainButtonNode, scale: 0.2) self.mainButtonNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - + let menuContentDelta = (sourceButtonFrame.width - inputTransition.menuButtonNode.frame.width) / 2.0 var menuIconSnapshotViewFrame = inputTransition.menuIconNode.frame.offsetBy(dx: inputTransition.menuButtonNode.frame.minX + menuContentDelta, dy: inputTransition.menuButtonNode.frame.minY) menuIconSnapshotViewFrame.origin.y = self.mainButtonNode.position.y - menuIconSnapshotViewFrame.height / 2.0 @@ -2377,14 +2381,14 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog self.view.addSubview(menuIconSnapshotView) menuIconSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) transition.updatePosition(layer: menuIconSnapshotView.layer, position: CGPoint(x: menuIconSnapshotView.center.x - menuContentDelta, y: inputTransition.menuButtonNode.position.y)) - + var menuTextSnapshotViewFrame = inputTransition.menuTextNode.frame.offsetBy(dx: inputTransition.menuButtonNode.frame.minX + 19.0 + menuContentDelta, dy: inputTransition.menuButtonNode.frame.minY) menuTextSnapshotViewFrame.origin.y = self.mainButtonNode.position.y - menuTextSnapshotViewFrame.height / 2.0 menuTextSnapshotView.frame = menuTextSnapshotViewFrame self.view.addSubview(menuTextSnapshotView) menuTextSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) transition.updatePosition(layer: menuTextSnapshotView.layer, position: CGPoint(x: menuTextSnapshotView.center.x - menuContentDelta, y: inputTransition.menuButtonNode.position.y)) - + inputNodeSnapshotView.clipsToBounds = true inputNodeSnapshotView.contentMode = .right let targetInputFrame = CGRect(x: inputTransition.menuButtonNode.frame.maxX, y: 0.0, width: inputNodeSnapshotView.frame.width - inputTransition.menuButtonNode.frame.maxX, height: inputNodeSnapshotView.frame.height) @@ -2393,11 +2397,11 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog transition.updateFrame(layer: inputNodeSnapshotView.layer, frame: targetInputFrame, completion: { [weak inputNodeSnapshotView, weak menuIconSnapshotView, weak menuTextSnapshotView, weak self] _ in inputNodeSnapshotView?.removeFromSuperview() self?.animatingTransition = false - + if !dismissed { menuIconSnapshotView?.removeFromSuperview() menuTextSnapshotView?.removeFromSuperview() - + self?.mainButtonNode.backgroundColor = sourceButtonColor self?.mainButtonNode.frame = sourceButtonFrame self?.mainButtonNode.textNode.position = sourceButtonTextPosition @@ -2406,20 +2410,20 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } }) } - + if dismissed { Queue.mainQueue().after(0.01, action) } else { action() } } - + func update(layout: ContainerViewLayout, buttons: [AttachmentButtonType], isSelecting: Bool, selectionCount: Int, elevateProgress: Bool, hideButtons: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = layout self.buttons = buttons self.elevateProgress = elevateProgress self.hideButtons = hideButtons - + if selectionCount != self.selectionCount { self.selectionCount = selectionCount self.updateChatPresentationInterfaceState(update: false, transition: .immediate, { state in @@ -2432,54 +2436,55 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } }) } - + let isButtonVisibleUpdated = self._isButtonVisible != self.mainButtonState.isVisible self._isButtonVisible = self.mainButtonState.isVisible - + let isSelectingUpdated = self.isSelecting != isSelecting self.isSelecting = isSelecting - + self.scrollNode.isUserInteractionEnabled = !isSelecting - + let isAnyButtonVisible = self.mainButtonState.isVisible || self.secondaryButtonState.isVisible let isNarrowButton = isAnyButtonVisible && self.mainButtonState.font == .regular - + let isTwoVerticalButtons = self.mainButtonState.isVisible && self.secondaryButtonState.isVisible && [.top, .bottom].contains(self.secondaryButtonState.position) let isTwoHorizontalButtons = self.mainButtonState.isVisible && self.secondaryButtonState.isVisible && [.left, .right].contains(self.secondaryButtonState.position) - + var insets = layout.insets(options: []) if let inputHeight = layout.inputHeight, inputHeight > 0.0 && (isSelecting || isAnyButtonVisible) { insets.bottom = inputHeight } else if layout.intrinsicInsets.bottom > 0.0 { insets.bottom = layout.intrinsicInsets.bottom } - + let topAccessoryHeight: CGFloat if self.hasMediaAccessoryPanel { topAccessoryHeight = MediaNavigationAccessoryHeaderNode.minimizedHeight } else { topAccessoryHeight = 0.0 } - + if isSelecting { self.loadTextNodeIfNeeded() } else { self.textInputPanelNode?.ensureUnfocused() } - + let textPanelSideInset: CGFloat = 16.0 let defaultPanelSideInset: CGFloat = glassPanelSideInset let panelSideInset: CGFloat = (isSelecting ? textPanelSideInset : defaultPanelSideInset) + layout.safeInsets.left var textPanelHeight: CGFloat = 0.0 + var visualTextPanelHeight: CGFloat = 0.0 var textPanelWidth: CGFloat = 0.0 if let textInputPanelNode = self.textInputPanelNode { textInputPanelNode.isUserInteractionEnabled = isSelecting - + var panelTransition = transition if textInputPanelNode.frame.width.isZero { panelTransition = .immediate } - let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false) + let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, bottomInset: layout.safeInsets.bottom, keyboardHeight: layout.inputHeight ?? 0.0, additionalSideInsets: UIEdgeInsets(), textFieldMaxHeight: layout.size.height / 2.0, availableHeight: layout.size.height, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false) let panelFrame = CGRect(x: 0.0, y: topAccessoryHeight, width: layout.size.width, height: panelHeight) if textInputPanelNode.frame.width.isZero { textInputPanelNode.frame = panelFrame @@ -2487,14 +2492,16 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog transition.updateFrame(node: textInputPanelNode, frame: panelFrame) if panelFrame.height > 0.0 { textPanelHeight = panelFrame.height + visualTextPanelHeight = max(0.0, textPanelHeight - textInputPanelNode.additionalInputHeight) } else { textPanelHeight = self.panelStyle == .glass ? 40.0 : 45.0 + visualTextPanelHeight = textPanelHeight } textPanelWidth = layout.size.width - panelSideInset * 2.0 } - + self.updateViews(transition: .immediate) - + let glassPanelHeight: CGFloat = 62.0 var bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: topAccessoryHeight + self.buttonSize.height + insets.bottom)) if (buttons.count == 1 || hideButtons) && topAccessoryHeight > 0.0 { @@ -2510,43 +2517,43 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } else { backgroundView = GlassBackgroundView() self.backgroundView = backgroundView - + liquidLensView = LiquidLensView(kind: .noContainer) liquidLensView.clipsToBounds = true liquidLensView.layer.cornerRadius = 28.0 - + self.containerNode.view.addSubview(backgroundView) self.containerNode.view.addSubview(liquidLensView) self.containerNode.view.addSubview(self.scrollNode.view) self.liquidLensView = liquidLensView - + liquidLensView.contentView.addSubview(self.itemsContainer) liquidLensView.selectedContentView.addSubview(self.selectedItemsContainer) - + self.itemsContainer.clipsToBounds = true self.itemsContainer.layer.cornerRadius = 28.0 self.selectedItemsContainer.clipsToBounds = true self.selectedItemsContainer.layer.cornerRadius = 28.0 - + let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) tabSelectionRecognizer.delegate = self.wrappedGestureRecognizerDelegate self.tabSelectionRecognizer = tabSelectionRecognizer self.scrollNode.view.addGestureRecognizer(tabSelectionRecognizer) } - + let buttonsPanelWidth: CGFloat if buttons.count == 3 { buttonsPanelWidth = smallPanelWidth } else { buttonsPanelWidth = layout.size.width - layout.safeInsets.left - layout.safeInsets.right - panelSideInset * 2.0 } - - let basePanelHeight = isSelecting ? max(0.0, textPanelHeight - 11.0) : glassPanelHeight + + let basePanelHeight = isSelecting ? max(0.0, visualTextPanelHeight - 11.0) : glassPanelHeight var panelSize = CGSize(width: isSelecting ? textPanelWidth : buttonsPanelWidth, height: basePanelHeight + topAccessoryHeight) if !isSelecting && (buttons.count == 1 || hideButtons) && topAccessoryHeight > 0.0 { panelSize.height = topAccessoryHeight } - + let cornerRadius: CGFloat if isSelecting { cornerRadius = min(20.0, basePanelHeight * 0.5) @@ -2556,35 +2563,35 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog cornerRadius = glassPanelHeight * 0.5 } let backgroundOriginX: CGFloat = isSelecting ? panelSideInset : floorToScreenPixels((layout.size.width - panelSize.width) / 2.0) - + backgroundView.update(size: panelSize, cornerRadius: cornerRadius, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: ComponentTransition(transition)) - + let lensSideInset: CGFloat = defaultPanelSideInset + layout.safeInsets.left let lensPanelSize = CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - lensSideInset * 2.0, height: glassPanelHeight) self.lensParams = (lensPanelSize, cornerRadius) self.updateLiquidLens(transition: ComponentTransition(transition)) - + transition.updatePosition(layer: liquidLensView.layer, position: CGPoint(x: backgroundOriginX + panelSize.width * 0.5, y: topAccessoryHeight + lensPanelSize.height * 0.5)) transition.updateBounds(layer: liquidLensView.layer, bounds: CGRect(origin: .zero, size: CGSize(width: lensPanelSize.width - 3.0 * 2.0, height: lensPanelSize.height - 3.0 * 2.0))) - + transition.updatePosition(layer: backgroundView.layer, position: CGPoint(x: backgroundOriginX + panelSize.width * 0.5, y: panelSize.height * 0.5)) transition.updateBounds(layer: backgroundView.layer, bounds: CGRect(origin: .zero, size: panelSize)) - + let itemsContainerFrame = CGRect(origin: .zero, size: CGSize(width: lensPanelSize.width - 3.0 * 2.0, height: lensPanelSize.height - 3.0 * 2.0)) transition.updateBounds(layer: self.itemsContainer.layer, bounds: CGRect(origin: .zero, size: itemsContainerFrame.size)) transition.updateBounds(layer: self.selectedItemsContainer.layer, bounds: CGRect(origin: .zero, size: itemsContainerFrame.size)) transition.updatePosition(layer: self.itemsContainer.layer, position: itemsContainerFrame.center) transition.updatePosition(layer: self.selectedItemsContainer.layer, position: itemsContainerFrame.center) - + if topAccessoryHeight > 0.0 { mediaAccessoryPanelFrame = CGRect(origin: CGPoint(x: backgroundOriginX, y: 0.0), size: CGSize(width: panelSize.width, height: topAccessoryHeight)) } } self.updateMediaAccessoryPanel(frame: mediaAccessoryPanelFrame, transition: transition) - + var containerTransition: ContainedViewLayoutTransition var containerFrame: CGRect - + let buttonSideInset: CGFloat let buttonSpacing: CGFloat = 16.0 let buttonHeight: CGFloat @@ -2596,11 +2603,18 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog buttonHeight = 50.0 buttonSideInset = 16.0 } - + if self.mainButtonState.hidesPanelBackground { self.backgroundView?.isHidden = true } - + + let effectiveBottomInset: CGFloat + if let textInputPanelNode = self.textInputPanelNode, textInputPanelNode.additionalInputHeight > 0.0 { + effectiveBottomInset = 0.0 + } else { + effectiveBottomInset = insets.bottom + } + if isAnyButtonVisible { var height: CGFloat if layout.intrinsicInsets.bottom > 0.0 && (layout.inputHeight ?? 0.0).isZero { @@ -2616,7 +2630,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog height = bounds.height + 8.0 } if isTwoVerticalButtons && self.secondaryButtonState.smallSpacing { - + } else if !isNarrowButton { if case .glass = self.panelStyle { } else { @@ -2628,7 +2642,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: height)) } else if isSelecting { - containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: textPanelHeight + insets.bottom + topAccessoryHeight)) + containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: textPanelHeight + effectiveBottomInset + topAccessoryHeight)) } else { containerFrame = bounds } @@ -2645,33 +2659,33 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog let alphaTransition = ContainedViewLayoutTransition.animated(duration: isSelecting ? 0.1 : 0.25, curve: .easeInOut) alphaTransition.updateAlpha(node: self.scrollNode, alpha: isSelecting || isAnyButtonVisible ? 0.0 : 1.0) containerTransition.updateTransformScale(node: self.scrollNode, scale: isSelecting || isAnyButtonVisible ? 0.85 : 1.0) - + if let liquidLensView = self.liquidLensView { alphaTransition.updateAlpha(layer: liquidLensView.layer, alpha: isSelecting || isAnyButtonVisible ? 0.0 : 1.0) containerTransition.updateTransformScale(layer: liquidLensView.layer, scale: isSelecting || isAnyButtonVisible ? 0.85 : 1.0) } - + if isSelectingUpdated { if isSelecting { self.loadTextNodeIfNeeded() if let textInputPanelNode = self.textInputPanelNode { textInputPanelNode.isUserInteractionEnabled = true if case .glass = self.panelStyle { - var textInputPanelHeight = textInputPanelNode.frame.height - if textInputPanelHeight < 1.0 { + var textInputPanelHeight = visualTextPanelHeight + if textInputPanelNode.frame.height < 1.0 { textInputPanelHeight = 51.0 } let heightDelta = glassPanelHeight - textInputPanelHeight - + let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) alphaTransition.updateAlpha(node: textInputPanelNode, alpha: 1.0) - + let componentTransition = ComponentTransition.easeInOut(duration: 0.25) if let liquidLensView = self.liquidLensView { componentTransition.animateBlur(layer: liquidLensView.layer, fromRadius: 0.0, toRadius: 10.0) transition.animatePositionAdditive(layer: liquidLensView.layer, offset: .zero, to: CGPoint(x: -27.0, y: 0.0)) } - + let blurTransition = ComponentTransition.easeInOut(duration: 0.18) transition.animateTransformScale(node: textInputPanelNode.opaqueActionButtons, from: 0.01) blurTransition.animateBlur(layer: textInputPanelNode.opaqueActionButtons.layer, fromRadius: 4.0, toRadius: 0.0) @@ -2680,10 +2694,10 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog blurTransition.animateBlur(layer: textInputPanelNode.inputModeView.layer, fromRadius: 4.0, toRadius: 0.0) componentTransition.animateAlpha(view: textInputPanelNode.inputModeView, from: 0.0, to: 1.0) - + transition.animatePositionAdditive(layer: textInputPanelNode.textPlaceholderNode.layer, offset: CGPoint(x: 6.0, y: heightDelta)) transition.animatePositionAdditive(layer: textInputPanelNode.inputModeView.layer, offset: CGPoint(x: 64.0, y: heightDelta)) - + textInputPanelNode.animateIn(transition: transition) } else { textInputPanelNode.alpha = 1.0 @@ -2695,21 +2709,21 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog if let textInputPanelNode = self.textInputPanelNode { textInputPanelNode.isUserInteractionEnabled = false if case .glass = self.panelStyle { - var textInputPanelHeight = textInputPanelNode.frame.height - if textInputPanelHeight < 1.0 { + var textInputPanelHeight = visualTextPanelHeight + if textInputPanelNode.frame.height < 1.0 { textInputPanelHeight = 51.0 } let heightDelta = glassPanelHeight - textInputPanelHeight - + let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) alphaTransition.updateAlpha(node: textInputPanelNode, alpha: 0.0) - + let componentTransition = ComponentTransition.easeInOut(duration: 0.25) if let liquidLensView = self.liquidLensView { componentTransition.animateBlur(layer: liquidLensView.layer, fromRadius: 10.0, toRadius: 0.0) transition.animatePositionAdditive(layer: liquidLensView.layer, offset: CGPoint(x: -27.0, y: 0.0)) } - + let blurTransition = ComponentTransition.easeInOut(duration: 0.18) transition.animateTransformScale(layer: textInputPanelNode.opaqueActionButtons.layer, from: CGPoint(x: 1.0, y: 1.0), to: CGPoint(x: 0.01, y: 0.01)) blurTransition.animateBlur(layer: textInputPanelNode.opaqueActionButtons.layer, fromRadius: 0.0, toRadius: 4.0) @@ -2718,7 +2732,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog blurTransition.animateBlur(layer: textInputPanelNode.inputModeView.layer, fromRadius: 0.0, toRadius: 4.0) componentTransition.animateAlpha(view: textInputPanelNode.inputModeView, from: 1.0, to: 0.0) - + transition.animatePositionAdditive(layer: textInputPanelNode.textPlaceholderNode.layer, offset: .zero, to: CGPoint(x: 6.0, y: heightDelta)) transition.animatePositionAdditive(layer: textInputPanelNode.inputModeView.layer, offset: .zero, to: CGPoint(x: 64.0, y: heightDelta)) } else { @@ -2729,16 +2743,16 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog } } } - + if self.containerNode.frame.size.width.isZero { containerTransition = .immediate } - + containerTransition.updateFrame(node: self.containerNode, frame: containerFrame) containerTransition.updateFrame(node: self.backgroundNode, frame: containerBounds) self.backgroundNode.update(size: containerBounds.size, transition: transition) containerTransition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: UIScreenPixel))) - + if case .glass = self.panelStyle { let scrollFrame = CGRect(origin: CGPoint(x: self.isSelecting ? panelSideInset - defaultPanelSideInset : panelSideInset, y: topAccessoryHeight + (self.isSelecting ? -11.0 : 0.0)), size: CGSize(width: layout.size.width - panelSideInset * 2.0, height: self.buttonSize.height)) transition.updatePosition(node: self.scrollNode, position: scrollFrame.center) @@ -2757,7 +2771,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog let loadingProgressHeight: CGFloat = 2.0 let loadingProgressY: CGFloat = elevateProgress ? -loadingProgressHeight : -loadingProgressHeight / 2.0 transition.updateFrame(node: loadingProgressNode, frame: CGRect(origin: CGPoint(x: 0.0, y: loadingProgressY), size: CGSize(width: layout.size.width, height: loadingProgressHeight))) - + loadingProgressNode.updateProgress(progress, animated: true) } else if let progressNode = self.progressNode { self.progressNode = nil @@ -2765,7 +2779,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog progressNode?.removeFromSupernode() }) } - + var buttonSize = CGSize(width: layout.size.width - (buttonSideInset + layout.safeInsets.left) * 2.0, height: buttonHeight) if isTwoHorizontalButtons { buttonSize = CGSize(width: (buttonSize.width - buttonSideInset) / 2.0, height: buttonSize.height) @@ -2777,7 +2791,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog case .legacy: buttonTopInset = isNarrowButton ? 2.0 : 8.0 } - + if !self.animatingTransition { let buttonOriginX = layout.safeInsets.left + buttonSideInset let buttonOriginY = isAnyButtonVisible || self.fromMenu ? topAccessoryHeight + buttonTopInset : containerFrame.height @@ -2807,7 +2821,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY), size: buttonSize) } } - + if let mainButtonFrame { if !self.dismissed { self.mainButtonNode.updateLayout(size: buttonSize, context: self.context, style: self.panelStyle, state: self.mainButtonState, animateBackground: self.mainButtonState.background.colorValue == self.backgroundNode.color && transition.isAnimated, transition: transition) @@ -2835,73 +2849,73 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog transition.updateAlpha(node: self.secondaryButtonNode, alpha: 0.0) } } - + return containerFrame.height } - + func updateItemContainers(contentOffset: CGFloat, transition: ComponentTransition) { let transform = CATransform3DMakeTranslation(-contentOffset, 0.0, 0.0) transition.setSublayerTransform(view: self.itemsContainer, transform: transform) transition.setSublayerTransform(view: self.selectedItemsContainer, transform: transform) } - + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { if self.selectionGestureState != nil { self.selectionGestureState = nil self.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) } } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { self.updateItemContainers(contentOffset: scrollView.contentOffset.x, transition: .immediate) self.updateViews(transition: .immediate) self.updateLiquidLens(transition: .immediate) } - + private var skipLensUpdate = false private var lensParams: (panelSize: CGSize, cornerRadius: CGFloat)? func updateLiquidLens(transition: ComponentTransition) { guard !self.skipLensUpdate, let liquidLensView = self.liquidLensView, let (panelSize, _) = self.lensParams else { return } - + var selectionFrame = CGRect() if self.selectedIndex >= 0 && self.selectedIndex < self.buttons.count, let itemView = self.itemViews[self.buttons[self.selectedIndex].key], let itemSize = self.itemSizes[self.buttons[self.selectedIndex].key] { let contentOffset = self.scrollNode.view.contentOffset.x selectionFrame = CGRect(origin: CGPoint(x: itemView.center.x - itemSize.width / 2.0 - contentOffset, y: itemView.frame.minY), size: itemSize) } - + var lensSelection: (x: CGFloat, width: CGFloat) if let selectionGestureState = self.selectionGestureState, selectionGestureState.isLifted { lensSelection = (selectionGestureState.currentX, selectionFrame.width) } else { lensSelection = (selectionFrame.minX, selectionFrame.width) } - + if !self.scrollNode.view.isScrollEnabled { lensSelection.x = max(0.0, min(lensSelection.x, panelSize.width - lensSelection.width)) } - + var isLifted = self.selectionGestureState?.isLifted == true || self.lensIsLifted if let widthClass = self.validLayout?.metrics.widthClass, case .regular = widthClass { isLifted = false } - + let inset: CGFloat = 3.0 liquidLensView.update(size: CGSize(width: panelSize.width - inset * 2.0, height: panelSize.height - inset * 2.0), cornerRadius: 28.0, selectionOrigin: CGPoint(x: lensSelection.x, y: 0.0), selectionSize: CGSize(width: lensSelection.width, height: panelSize.height - inset * 2.0), inset: 0.0, isDark: self.presentationData.theme.overallDarkAppearance, isLifted: isLifted, isCollapsed: self.isSelecting || self.buttons.count < 2 || self.hideButtons, transition: transition) } - + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer == self.tabSelectionRecognizer { return true } return super.gestureRecognizerShouldBegin(gestureRecognizer) } - + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { return true } - + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index bc640f69bf..1b4da5efdc 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, accentDisabledButtonColor: theme.chat.inputPanel.panelControlDisabledColor, accentForegroundColor: theme.list.itemCheckColors.foregroundColor, style: .glass) } private let sharedContext: SharedAccountContext @@ -810,8 +810,8 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth return controller } - private func paymentController(number: String, phoneCodeHash: String, storeProduct: String, supportEmailAddress: String, supportEmailSubject: String) -> AuthorizationSequencePaymentScreen { - let controller = AuthorizationSequencePaymentScreen(sharedContext: self.sharedContext, engine: self.engine, presentationData: self.presentationData, inAppPurchaseManager: self.inAppPurchaseManager, phoneNumber: number, phoneCodeHash: phoneCodeHash, storeProduct: storeProduct, supportEmailAddress: supportEmailAddress, supportEmailSubject: supportEmailSubject, back: { [weak self] in + private func paymentController(number: String, phoneCodeHash: String, storeProduct: String, premiumDays: Int32, supportEmailAddress: String, supportEmailSubject: String) -> AuthorizationSequencePaymentScreen { + let controller = AuthorizationSequencePaymentScreen(sharedContext: self.sharedContext, engine: self.engine, presentationData: self.presentationData, inAppPurchaseManager: self.inAppPurchaseManager, phoneNumber: number, phoneCodeHash: phoneCodeHash, storeProduct: storeProduct, premiumDays: premiumDays, supportEmailAddress: supportEmailAddress, supportEmailSubject: supportEmailSubject, back: { [weak self] in guard let self else { return } @@ -997,7 +997,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: suggestReset ? strongSelf.presentationData.strings.TwoStepAuth_RecoveryFailed : strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [ - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: { if let strongSelf = self, let strongController = controller { strongController.inProgress = true @@ -1084,7 +1084,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_ResetAccountConfirmation, actions: [ - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: { if let strongSelf = self, let strongController = controller { strongController.inProgress = true @@ -1159,7 +1159,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth let avatarVideo: Signal? if let avatarAsset = avatarAsset as? AVAsset { let engine = strongSelf.engine - avatarVideo = Signal { subscriber in + avatarVideo = Signal { subscriber in let entityRenderer: LegacyPaintEntityRenderer? = avatarAdjustments.flatMap { adjustments in if let paintingData = adjustments.paintingData, paintingData.hasAnimation { return LegacyPaintEntityRenderer(postbox: nil, adjustments: adjustments) @@ -1178,7 +1178,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth if let data = try? Data(contentsOf: result.fileURL) { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) engine.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - subscriber.putNext(resource) + subscriber.putNext(EngineMediaResource(resource)) EngineTempBox.shared.dispose(tempFile) } @@ -1348,12 +1348,12 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth } controllers.append(self.signUpController(firstName: firstName, lastName: lastName, termsOfService: termsOfService, displayCancel: displayCancel)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) - case let .payment(number, codeHash, storeProduct, supportEmailAddress, supportEmailSubject, _): + case let .payment(number, codeHash, storeProduct, premiumDays, supportEmailAddress, supportEmailSubject, _): var controllers: [ViewController] = [] if !self.otherAccountPhoneNumbers.1.isEmpty { controllers.append(self.splashController()) } - controllers.append(self.paymentController(number: number, phoneCodeHash: codeHash, storeProduct: storeProduct, supportEmailAddress: supportEmailAddress, supportEmailSubject: supportEmailSubject)) + controllers.append(self.paymentController(number: number, phoneCodeHash: codeHash, storeProduct: storeProduct, premiumDays: premiumDays, supportEmailAddress: supportEmailAddress, supportEmailSubject: supportEmailSubject)) self.setViewControllers(controllers, animated: !self.viewControllers.isEmpty) } } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift index 61715e95a0..8658893b75 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences @@ -40,6 +39,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { let phoneNumber: String let phoneCodeHash: String let storeProduct: String + let premiumDays: Int32 let supportEmailAddress: String let supportEmailSubject: String @@ -51,6 +51,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { phoneNumber: String, phoneCodeHash: String, storeProduct: String, + premiumDays: Int32, supportEmailAddress: String, supportEmailSubject: String ) { @@ -61,6 +62,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { self.phoneNumber = phoneNumber self.phoneCodeHash = phoneCodeHash self.storeProduct = storeProduct + self.premiumDays = premiumDays self.supportEmailAddress = supportEmailAddress self.supportEmailSubject = supportEmailSubject } @@ -116,7 +118,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { self.state?.updated() let (currency, amount) = storeProduct.priceCurrencyAndAmount - let purpose: AppStoreTransactionPurpose = .authCode(restore: false, phoneNumber: component.phoneNumber, phoneCodeHash: component.phoneCodeHash, currency: currency, amount: amount) + let purpose: AppStoreTransactionPurpose = .authCode(restore: false, phoneNumber: component.phoneNumber, phoneCodeHash: component.phoneCodeHash, premiumDays: component.premiumDays, currency: currency, amount: amount) let _ = (component.engine.payments.canPurchasePremium(purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] available in guard let self else { @@ -169,7 +171,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { title: nil, text: errorText, actions: [ - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in guard let self else { return @@ -334,13 +336,24 @@ final class AuthorizationSequencePaymentScreenComponent: Component { )) ) ) + + let supportText: String + if component.premiumDays == 7 { + supportText = environment.strings.Login_Fee_Support_Text + } else if component.premiumDays > 0 { + let daysString = environment.strings.Login_Fee_Support_NewText_Days(component.premiumDays) + supportText = environment.strings.Login_Fee_Support_NewText(daysString).string + } else { + supportText = environment.strings.Login_Fee_Support_NewTextNone + } + items.append( AnyComponentWithIdentity( id: "support", component: AnyComponent(ParagraphComponent( title: environment.strings.Login_Fee_Support_Title, titleColor: textColor, - text: environment.strings.Login_Fee_Support_Text, + text: supportText, textColor: secondaryTextColor, iconName: "Premium/Authorization/Support", iconColor: linkColor, @@ -352,7 +365,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { sharedContext: component.sharedContext, engine: component.engine, inAppPurchaseManager: component.inAppPurchaseManager, - source: .auth(product.price), + source: .auth(product.price, component.premiumDays), proceed: { [weak self] in self?.proceed() } @@ -411,6 +424,16 @@ final class AuthorizationSequencePaymentScreenComponent: Component { } let buttonString = environment.strings.Login_Fee_SignUp(priceString).string + let buttonSubtitle: String + if component.premiumDays == 7 { + buttonSubtitle = environment.strings.Login_Fee_GetPremiumForAWeek + } else if component.premiumDays > 0 { + let daysString = environment.strings.Login_Fee_GetPremiumForDays_Days(component.premiumDays) + buttonSubtitle = environment.strings.Login_Fee_GetPremiumForDays(daysString).string + } else { + buttonSubtitle = environment.strings.Login_Fee_GetPremiumNone + } + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) let buttonSize = self.button.update( transition: transition, @@ -426,7 +449,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { component: AnyComponent( VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))), - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Login_Fee_GetPremiumForAWeek, font: Font.medium(11.0), textColor: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center))))) + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: buttonSubtitle, font: Font.medium(11.0), textColor: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center))))) ], spacing: 1.0) ) ), @@ -468,6 +491,7 @@ public final class AuthorizationSequencePaymentScreen: ViewControllerComponentCo phoneNumber: String, phoneCodeHash: String, storeProduct: String, + premiumDays: Int32, supportEmailAddress: String, supportEmailSubject: String, back: @escaping () -> Void @@ -480,6 +504,7 @@ public final class AuthorizationSequencePaymentScreen: ViewControllerComponentCo phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, storeProduct: storeProduct, + premiumDays: premiumDays, supportEmailAddress: supportEmailAddress, supportEmailSubject: supportEmailSubject ), navigationBarAppearance: .transparent, theme: .default, updatedPresentationData: (initial: presentationData, signal: .single(presentationData))) diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift index c28585b3de..9f9d886649 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift @@ -4,7 +4,6 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore -import Postbox import TelegramPresentationData import PresentationDataUtils import ProgressNavigationButtonNode diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift index 69a9fcc455..72bffa0e99 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift @@ -8,7 +8,6 @@ import PhoneInputNode import CountrySelectionUI import QrCode import SwiftSignalKit -import Postbox import AccountContext import AnimatedStickerNode import TelegramAnimatedStickerNode diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSplashController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSplashController.swift index c422320ddf..9056e2daab 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceSplashController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceSplashController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SSignalKit import SwiftSignalKit diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 115c8c0149..bbecec9430 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -643,7 +643,7 @@ public final class AvatarNode: ASDisplayNode { let parameters: AvatarNodeParameters - if let peer = peer, let signal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer._asPeer()), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded, cutoutRect: cutoutRect) { + if let peer = peer, let signal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded, cutoutRect: cutoutRect) { self.contents = nil self.displaySuspended = true self.imageReady.set(self.imageNode.contentReady) @@ -763,7 +763,7 @@ public final class AvatarNode: ASDisplayNode { self.imageNode.view.mask = nil } - if let imageCache = genericContext.imageCache as? DirectMediaImageCache, let peer, let smallProfileImage = peer.smallProfileImage, let peerReference = PeerReference(peer._asPeer()) { + if let imageCache = genericContext.imageCache as? DirectMediaImageCache, let peer, let smallProfileImage = peer.smallProfileImage, let peerReference = PeerReference(peer) { if let result = imageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: peer.profileImageRepresentations.first?.immediateThumbnailData, size: Int(displayDimensions.width * UIScreenScale), synchronous: synchronousLoad) { if let image = result.image { self.imageNode.contents = image.cgImage @@ -852,7 +852,7 @@ public final class AvatarNode: ASDisplayNode { let account = account ?? genericContext.account - if let peer = peer, let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded, cutoutRect: cutoutRect) { + if let peer = peer, let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded, cutoutRect: cutoutRect) { self.contents = nil self.displaySuspended = true self.imageReady.set(self.imageNode.contentReady) diff --git a/submodules/AvatarNode/Sources/PeerAvatar.swift b/submodules/AvatarNode/Sources/PeerAvatar.swift index 17d3374616..61b7742175 100644 --- a/submodules/AvatarNode/Sources/PeerAvatar.swift +++ b/submodules/AvatarNode/Sources/PeerAvatar.swift @@ -126,8 +126,8 @@ public func peerAvatarCompleteImage(postbox: Postbox, network: Network, peer: En thumbnailRepresentation = peer.profileImageRepresentations.first } - if let signal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: thumbnailRepresentation, displayDimensions: size, clipStyle: clipStyle, blurred: blurred, inset: 0.0, emptyColor: nil, synchronousLoad: fullSize) { - if fullSize, let fullSizeSignal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: peer.profileImageRepresentations.last, displayDimensions: size, emptyColor: nil, synchronousLoad: true) { + if let signal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer), authorOfMessage: nil, representation: thumbnailRepresentation, displayDimensions: size, clipStyle: clipStyle, blurred: blurred, inset: 0.0, emptyColor: nil, synchronousLoad: fullSize) { + if fullSize, let fullSizeSignal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.profileImageRepresentations.last, displayDimensions: size, emptyColor: nil, synchronousLoad: true) { iconSignal = combineLatest(.single(nil) |> then(signal), .single(nil) |> then(fullSizeSignal)) |> mapToSignal { thumbnailImage, fullSizeImage -> Signal in if let fullSizeImage = fullSizeImage { diff --git a/submodules/AvatarVideoNode/BUILD b/submodules/AvatarVideoNode/BUILD index dd69602adc..1cd082613a 100644 --- a/submodules/AvatarVideoNode/BUILD +++ b/submodules/AvatarVideoNode/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AnimationUI:AnimationUI", diff --git a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift index bb787e1836..c541eda360 100644 --- a/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift +++ b/submodules/AvatarVideoNode/Sources/AvatarVideoNode.swift @@ -206,7 +206,7 @@ public final class AvatarVideoNode: ASDisplayNode { self.internalSize = size if let markup = photo.emojiMarkup { self.update(markup: markup, size: size, useAnimationNode: false) - } else if let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) { + } else if let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer) { self.backgroundNode.image = nil let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value() @@ -217,7 +217,7 @@ public final class AvatarVideoNode: ASDisplayNode { self.videoContent = videoContent self.videoFileDisposable?.dispose() - self.videoFileDisposable = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .avatar, reference: videoFileReference.resourceReference(videoFileReference.media.resource)).startStrict() + self.videoFileDisposable = self.context.engine.resources.fetch(reference: videoFileReference.resourceReference(videoFileReference.media.resource), userLocation: .peer(peer.id), userContentType: .avatar).startStrict() } } } @@ -229,7 +229,7 @@ public final class AvatarVideoNode: ASDisplayNode { if isVisible, let animationNode = self.animationNode, let file = self.animationFile { if !self.didSetupAnimation { self.didSetupAnimation = true - let pathPrefix = self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let pathPrefix = self.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.resource.id)) let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm") diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index 74cd9f60b2..8b9315a112 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/TelegramUI/Components/MinimizedContainer", "//submodules/Pasteboard", "//submodules/SaveToCameraRoll", + "//submodules/TextFormat:TextFormat", "//submodules/TelegramUI/Components/NavigationStackComponent", "//submodules/LocationUI", "//submodules/OpenInExternalAppUI", diff --git a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift index d72f35d457..bc84de9157 100644 --- a/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserBookmarksScreen.swift @@ -176,7 +176,7 @@ public final class BrowserBookmarksScreen: ViewController { }, sendGift: { _ in }, openUniqueGift: { _ in }, openMessageFeeException: { - }, requestMessageUpdate: { _, _ in + }, requestMessageUpdate: { _, _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { }, scrollToMessageId: { _ in @@ -189,7 +189,10 @@ public final class BrowserBookmarksScreen: ViewController { }, requestToggleTodoMessageItem: { _, _, _ in }, displayTodoToggleUnavailable: { _ in }, openStarsPurchase: { _ in - }, openRankInfo: { _, _, _ in }, openSetPeerAvatar: {}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) + }, openRankInfo: { _, _, _ in + }, openSetPeerAvatar: { + }, displayPollRestrictedToast: { _ in + }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) let tagMask: MessageTags = .webPage diff --git a/submodules/BrowserUI/Sources/BrowserDocumentContent.swift b/submodules/BrowserUI/Sources/BrowserDocumentContent.swift index fedb0f2c10..841300360d 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 @@ -63,7 +62,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate var title: String = "file" var url = "" - if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.media.resource) { + if let path = self.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.media.resource.id)) { var updatedPath = path if let fileName = file.media.fileName { let tempFile = TempBox.shared.file(path: path, fileName: fileName) @@ -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/BrowserExceptionDomainAlertContentNode.swift b/submodules/BrowserUI/Sources/BrowserExceptionDomainAlertContentNode.swift index b3f347b7a4..40fd616490 100644 --- a/submodules/BrowserUI/Sources/BrowserExceptionDomainAlertContentNode.swift +++ b/submodules/BrowserUI/Sources/BrowserExceptionDomainAlertContentNode.swift @@ -3,7 +3,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 38beedd814..933b8af958 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -16,11 +16,11 @@ import TranslateUI import ContextUI import Pasteboard import SaveToCameraRoll -import ShareController import SafariServices import LocationUI import OpenInExternalAppUI import GalleryUI +import TextFormat final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDelegate { private let context: AccountContext @@ -66,6 +66,10 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg var currentWebEmbedHeights: [Int : CGFloat] = [:] var currentExpandedDetails: [Int : Bool]? var currentDetailsItems: [InstantPageDetailsItem] = [] + private var resolvedExternalMediaDimensions: [MediaId: PixelDimensions] = [:] + private var pendingResolvedExternalMediaDimensions = Set() + private var codeHighlight: CachedMessageSyntaxHighlight? + private var codeHighlightState: (specs: [CachedMessageSyntaxHighlight.Spec], disposable: Disposable)? var currentAccessibilityAreas: [AccessibilityAreaNode] = [] @@ -88,6 +92,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg private let loadWebpageDisposable = MetaDisposable() private let resolveUrlDisposable = MetaDisposable() private let updateLayoutDisposable = MetaDisposable() + private let updateExternalMediaDimensionsDisposable = MetaDisposable() + private let updateCodeHighlightDisposable = MetaDisposable() private let loadProgress = ValuePromise(1.0, ignoreRepeated: true) private let readingProgress = ValuePromise(0.0, ignoreRepeated: true) @@ -180,12 +186,16 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } self.scrollNode.view.addGestureRecognizer(recognizer) - self.webpageDisposable = (actualizedWebpage(account: context.account, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in - guard let self else { - return - } - self.updateWebPage(result, anchor: self.initialAnchor) - }) + if case let .Loaded(content) = webPage.content, let scheme = URL(string: content.url)?.scheme?.lowercased(), scheme == "http" || scheme == "https" { + self.webpageDisposable = (actualizedWebpage(account: context.account, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + self.updateWebPage(result, anchor: self.initialAnchor) + }) + } + + self.updateCodeHighlight() } deinit { @@ -194,6 +204,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg self.loadWebpageDisposable.dispose() self.resolveUrlDisposable.dispose() self.updateLayoutDisposable.dispose() + self.updateExternalMediaDimensionsDisposable.dispose() + self.updateCodeHighlightDisposable.dispose() } required init?(coder: NSCoder) { @@ -305,6 +317,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } else { self.webPage = nil } + self.resolvedExternalMediaDimensions.removeAll() + self.pendingResolvedExternalMediaDimensions.removeAll() if let anchor = anchor { self.initialAnchor = anchor.removingPercentEncoding } else if let state = state { @@ -318,6 +332,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } } self.currentLayout = nil + self.updateCodeHighlight() self.updatePageLayout() self.scrollNode.frame = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0) @@ -479,16 +494,11 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } private func updatePageLayout() { - guard let (size, insets, _) = self.containerLayout, let (webPage, instantPage) = self.webPage else { + guard let (size, insets, _) = self.containerLayout, let (webPage, instantPage) = self.resolvedWebPage() else { return } - let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights) - - for (_, tileNode) in self.visibleTiles { - tileNode.removeFromSupernode() - } - self.visibleTiles.removeAll() + let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights, cachedMessageSyntaxHighlight: self.codeHighlight) let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: size.width) @@ -549,6 +559,96 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg self.scrollNode.view.contentSize = currentLayout.contentSize self.scrollNodeFooter.frame = CGRect(origin: CGPoint(x: 0.0, y: currentLayout.contentSize.height), size: CGSize(width: size.width, height: 2000.0)) } + + private func updateCodeHighlight() { + guard let instantPage = self.webPage?.instantPage else { + self.codeHighlight = nil + self.codeHighlightState = nil + self.updateCodeHighlightDisposable.set(nil) + return + } + + let specs = syntaxHighlightSpecs(for: instantPage.blocks) + if let currentState = self.codeHighlightState, currentState.specs == specs { + return + } + + if specs.isEmpty { + let hadHighlight = self.codeHighlight != nil + self.codeHighlight = nil + self.codeHighlightState = nil + self.updateCodeHighlightDisposable.set(nil) + if hadHighlight { + self.updatePageLayout() + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) + } + return + } + + let disposable = MetaDisposable() + self.codeHighlightState = (specs, disposable) + self.updateCodeHighlightDisposable.set(disposable) + disposable.set((asyncStanaloneSyntaxHighlight(current: self.codeHighlight, specs: specs) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + if self.codeHighlight != result { + self.codeHighlight = result + self.updatePageLayout() + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) + } + })) + } + + private func syntaxHighlightSpecs(for blocks: [InstantPageBlock]) -> [CachedMessageSyntaxHighlight.Spec] { + var specs: [CachedMessageSyntaxHighlight.Spec] = [] + var seen = Set() + + func collect(blocks: [InstantPageBlock]) { + for block in blocks { + switch block { + case let .preformatted(text, language): + guard let language = normalizedCodeBlockLanguage(language), !text.plainText.isEmpty else { + continue + } + let spec = CachedMessageSyntaxHighlight.Spec(language: language, text: text.plainText) + if seen.insert(spec).inserted { + specs.append(spec) + } + case let .cover(block): + collect(blocks: [block]) + case let .postEmbed(_, _, _, _, _, blocks, _): + collect(blocks: blocks) + case let .collage(items, _): + collect(blocks: items) + case let .slideshow(items, _): + collect(blocks: items) + case let .details(_, blocks, _): + collect(blocks: blocks) + case let .list(items, _): + for item in items { + if case let .blocks(blocks, _) = item { + collect(blocks: blocks) + } + } + default: + break + } + } + } + + collect(blocks: blocks) + return specs + } + + private func normalizedCodeBlockLanguage(_ language: String?) -> String? { + guard let language else { + return nil + } + let normalized = language.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + return normalized.isEmpty ? nil : normalized + } func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) { var visibleTileIndices = Set() @@ -656,6 +756,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg topNode = newNode self.visibleItemsWithNodes[itemIndex] = newNode itemNode = newNode + self.configureExternalMediaDimensionsUpdates(for: newNode) if let itemNode = itemNode as? InstantPageDetailsNode { itemNode.requestLayoutUpdate = { [weak self] animated in @@ -679,6 +780,10 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } } + if let itemNode = itemNode { + self.configureExternalMediaDimensionsUpdates(for: itemNode) + } + if let itemNode = itemNode as? InstantPageDetailsNode { itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated) } @@ -709,8 +814,11 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg topNode = tileNode self.visibleTiles[tileIndex] = tileNode } else { - if visibleTiles[tileIndex]!.frame != tileFrame { - transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame) + if let tileNode = self.visibleTiles[tileIndex] { + tileNode.update(tile: tile, backgroundColor: theme.pageBackgroundColor) + if tileNode.frame != tileFrame { + transition.updateFrame(node: tileNode, frame: tileFrame) + } } } } @@ -930,6 +1038,385 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg return nil } + private func configureExternalMediaDimensionsUpdates(for itemNode: InstantPageNode) { + let update: (MediaId, PixelDimensions) -> Void = { [weak self] mediaId, dimensions in + self?.updateExternalMediaDimensions(mediaId, dimensions) + } + if let itemNode = itemNode as? InstantPageExternalMediaDimensionsNode { + itemNode.updateExternalMediaDimensions = update + } + if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.contentNode.updateExternalMediaDimensions = update + } + } + + private func updateExternalMediaDimensions(_ mediaId: MediaId, _ dimensions: PixelDimensions) { + if self.resolvedExternalMediaDimensions[mediaId] == dimensions { + return + } + self.resolvedExternalMediaDimensions[mediaId] = dimensions + self.pendingResolvedExternalMediaDimensions.insert(mediaId) + + let signal: Signal = (.complete() |> delay(0.08, queue: Queue.mainQueue())) + self.updateExternalMediaDimensionsDisposable.set(signal.start(completed: { [weak self] in + self?.relayoutForResolvedExternalMediaDimensions() + })) + } + + private func relayoutForResolvedExternalMediaDimensions() { + guard !self.pendingResolvedExternalMediaDimensions.isEmpty else { + return + } + + let mediaIds = Array(self.pendingResolvedExternalMediaDimensions) + self.pendingResolvedExternalMediaDimensions.removeAll() + + let detailsStateMaps = self.captureExpandedDetailsStateMaps() + let viewportTop = self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentInset.top + var oldFrames: [MediaId: CGRect] = [:] + for mediaId in mediaIds { + if let frame = self.effectiveFrameForMedia(mediaId, detailsStateMaps: detailsStateMaps) { + oldFrames[mediaId] = frame + } + } + + self.updatePageLayout() + + var newFrames: [MediaId: CGRect] = [:] + for mediaId in mediaIds { + if let frame = self.effectiveFrameForMedia(mediaId, detailsStateMaps: detailsStateMaps) { + newFrames[mediaId] = frame + } + } + + if let compensatedViewportTop = self.compensatedViewportTop(oldFrames: oldFrames, newFrames: newFrames, viewportTop: viewportTop) { + self.setViewportTop(compensatedViewportTop) + } + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) + } + + private func setViewportTop(_ viewportTop: CGFloat) { + let scrollView = self.scrollNode.view + let minOffsetY = -scrollView.contentInset.top + let maxOffsetY = max(minOffsetY, scrollView.contentSize.height - scrollView.bounds.height + scrollView.contentInset.bottom) + let contentOffsetY = min(max(viewportTop - scrollView.contentInset.top, minOffsetY), maxOffsetY) + if contentOffsetY.isFinite { + scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: contentOffsetY) + } + } + + private func compensatedViewportTop(oldFrames: [MediaId: CGRect], newFrames: [MediaId: CGRect], viewportTop: CGFloat) -> CGFloat? { + var pairedFrames: [(old: CGRect, new: CGRect)] = [] + for (mediaId, oldFrame) in oldFrames { + if let newFrame = newFrames[mediaId] { + pairedFrames.append((oldFrame, newFrame)) + } + } + if pairedFrames.isEmpty { + return nil + } + + if let intersecting = pairedFrames + .filter({ $0.old.height > 0.0 && $0.new.height > 0.0 && viewportTop > $0.old.minY && viewportTop < $0.old.maxY }) + .max(by: { $0.old.minY < $1.old.minY }) { + let ratio = min(max((viewportTop - intersecting.old.minY) / intersecting.old.height, 0.0), 1.0) + return intersecting.new.minY + ratio * intersecting.new.height + } + + if let above = pairedFrames + .filter({ viewportTop >= $0.old.maxY }) + .max(by: { $0.old.maxY < $1.old.maxY }) { + return viewportTop + (above.new.maxY - above.old.maxY) + } + + return nil + } + + private func captureExpandedDetailsStateMaps() -> [String: [Int: Bool]] { + guard let currentLayout = self.currentLayout else { + return [:] + } + var result: [String: [Int: Bool]] = [:] + self.captureExpandedDetailsStateMaps(items: currentLayout.items, visibleItemsWithNodes: self.visibleItemsWithNodes, path: [], result: &result) + return result + } + + private func captureExpandedDetailsStateMaps(items: [InstantPageItem], visibleItemsWithNodes: [Int: InstantPageNode], path: [Int], result: inout [String: [Int: Bool]]) { + let detailsNodes = visibleItemsWithNodes.compactMap { $0.value as? InstantPageDetailsNode } + + var detailsIndex = -1 + for item in items { + guard let detailsItem = item as? InstantPageDetailsItem else { + continue + } + detailsIndex += 1 + + guard let detailsNode = detailsNodes.first(where: { $0.item === detailsItem }) else { + continue + } + let nextPath = path + [detailsIndex] + result[self.detailsStateKey(nextPath)] = detailsNode.contentNode.currentExpandedDetails ?? [:] + self.captureExpandedDetailsStateMaps(items: detailsItem.items, visibleItemsWithNodes: detailsNode.contentNode.visibleItemsWithNodes, path: nextPath, result: &result) + } + } + + private func detailsStateKey(_ path: [Int]) -> String { + if path.isEmpty { + return "" + } + return path.map(String.init).joined(separator: ".") + } + + private func effectiveFrameForMedia(_ mediaId: MediaId, detailsStateMaps: [String: [Int: Bool]]) -> CGRect? { + guard let currentLayout = self.currentLayout else { + return nil + } + return self.effectiveFrameForMedia(mediaId, items: currentLayout.items, origin: .zero, expandedDetails: self.currentExpandedDetails, path: [], detailsStateMaps: detailsStateMaps) + } + + private func effectiveFrameForMedia(_ mediaId: MediaId, items: [InstantPageItem], origin: CGPoint, expandedDetails: [Int: Bool]?, path: [Int], detailsStateMaps: [String: [Int: Bool]]) -> CGRect? { + var collapseOffset: CGFloat = 0.0 + var detailsIndex = -1 + + for item in items { + if item is InstantPageDetailsItem { + detailsIndex += 1 + } + + var itemFrame = item.frame.offsetBy(dx: origin.x, dy: origin.y - collapseOffset) + if let detailsItem = item as? InstantPageDetailsItem { + let nextPath = path + [detailsIndex] + let nestedExpandedDetails = detailsStateMaps[self.detailsStateKey(nextPath)] + let expanded = expandedDetails?[detailsIndex] ?? detailsItem.initiallyExpanded + let height = expanded ? detailsItem.titleHeight + self.effectiveContentHeight(items: detailsItem.items, baseHeight: detailsItem.frame.height - detailsItem.titleHeight, expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) : detailsItem.titleHeight + collapseOffset += item.frame.height - height + itemFrame.size.height = height + + if expanded, let nestedFrame = self.effectiveFrameForMedia(mediaId, items: detailsItem.items, origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + detailsItem.titleHeight), expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) { + return nestedFrame + } + continue + } + + if self.itemContainsMedia(item, mediaId: mediaId) { + return itemFrame + } + } + + return nil + } + + private func effectiveContentHeight(items: [InstantPageItem], baseHeight: CGFloat, expandedDetails: [Int: Bool]?, path: [Int], detailsStateMaps: [String: [Int: Bool]]) -> CGFloat { + var contentHeight = baseHeight + var detailsIndex = -1 + + for item in items { + guard let detailsItem = item as? InstantPageDetailsItem else { + continue + } + detailsIndex += 1 + + let nextPath = path + [detailsIndex] + let nestedExpandedDetails = detailsStateMaps[self.detailsStateKey(nextPath)] + let expanded = expandedDetails?[detailsIndex] ?? detailsItem.initiallyExpanded + let height = expanded ? detailsItem.titleHeight + self.effectiveContentHeight(items: detailsItem.items, baseHeight: detailsItem.frame.height - detailsItem.titleHeight, expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) : detailsItem.titleHeight + contentHeight += -detailsItem.frame.height + height + } + + return contentHeight + } + + private func itemContainsMedia(_ item: InstantPageItem, mediaId: MediaId) -> Bool { + for media in item.medias { + if media.media.id == mediaId { + return true + } + } + return false + } + + private func resolvedWebPage() -> (webPage: TelegramMediaWebpage, instantPage: InstantPage?)? { + guard let (webPage, instantPage) = self.webPage else { + return nil + } + guard !self.resolvedExternalMediaDimensions.isEmpty, case let .Loaded(content) = webPage.content else { + return (webPage, instantPage) + } + + var instantPageUpdated = false + var effectiveInstantPage = instantPage + if let instantPage { + var media = instantPage.media + for (mediaId, currentMedia) in instantPage.media { + if let updatedMedia = self.updatedMediaIfNeeded(currentMedia) { + media[mediaId] = updatedMedia + instantPageUpdated = true + } + } + if instantPageUpdated { + effectiveInstantPage = InstantPage(blocks: instantPage.blocks, media: media, isComplete: instantPage.isComplete, rtl: instantPage.rtl, url: instantPage.url, views: instantPage.views) + } + } + + var imageUpdated = false + let effectiveImage = content.image.map { image -> TelegramMediaImage in + if let updated = self.updatedImageIfNeeded(image) { + imageUpdated = true + return updated + } else { + return image + } + } + + var fileUpdated = false + let effectiveFile = content.file.map { file -> TelegramMediaFile in + if let updated = self.updatedFileIfNeeded(file) { + fileUpdated = true + return updated + } else { + return file + } + } + + if !instantPageUpdated && !imageUpdated && !fileUpdated { + return (webPage, instantPage) + } + + let effectiveContent = TelegramMediaWebpageLoadedContent( + url: content.url, + displayUrl: content.displayUrl, + hash: content.hash, + type: content.type, + websiteName: content.websiteName, + title: content.title, + text: content.text, + embedUrl: content.embedUrl, + embedType: content.embedType, + embedSize: content.embedSize, + duration: content.duration, + author: content.author, + isMediaLargeByDefault: content.isMediaLargeByDefault, + imageIsVideoCover: content.imageIsVideoCover, + image: effectiveImage, + file: effectiveFile, + story: content.story, + attributes: content.attributes, + instantPage: effectiveInstantPage + ) + return (TelegramMediaWebpage(webpageId: webPage.webpageId, content: .Loaded(effectiveContent)), effectiveInstantPage) + } + + private func updatedMediaIfNeeded(_ media: Media) -> Media? { + if let image = media as? TelegramMediaImage { + return self.updatedImageIfNeeded(image) + } else if let file = media as? TelegramMediaFile { + return self.updatedFileIfNeeded(file) + } else { + return nil + } + } + + private func updatedImageIfNeeded(_ image: TelegramMediaImage) -> TelegramMediaImage? { + guard let dimensions = self.resolvedExternalMediaDimensions[image.imageId] else { + return nil + } + + var updatedRepresentations = image.representations + var didUpdate = false + for i in 0 ..< updatedRepresentations.count { + let representation = updatedRepresentations[i] + guard representation.resource is InstantPageExternalMediaResource, representation.dimensions != dimensions else { + continue + } + updatedRepresentations[i] = TelegramMediaImageRepresentation( + dimensions: dimensions, + resource: representation.resource, + progressiveSizes: representation.progressiveSizes, + immediateThumbnailData: representation.immediateThumbnailData, + hasVideo: representation.hasVideo, + isPersonal: representation.isPersonal, + typeHint: representation.typeHint + ) + didUpdate = true + } + + guard didUpdate else { + return nil + } + return TelegramMediaImage( + imageId: image.imageId, + representations: updatedRepresentations, + videoRepresentations: image.videoRepresentations, + immediateThumbnailData: image.immediateThumbnailData, + emojiMarkup: image.emojiMarkup, + reference: image.reference, + partialReference: image.partialReference, + flags: image.flags, + video: image.video + ) + } + + private func updatedFileIfNeeded(_ file: TelegramMediaFile) -> TelegramMediaFile? { + guard let dimensions = self.resolvedExternalMediaDimensions[file.fileId], file.resource is InstantPageExternalMediaResource else { + return nil + } + + let (attributes, didUpdate) = self.fileAttributesWithResolvedDimensions(file.attributes, dimensions: dimensions) + guard didUpdate else { + return nil + } + + return TelegramMediaFile( + fileId: file.fileId, + partialReference: file.partialReference, + resource: file.resource, + previewRepresentations: file.previewRepresentations, + videoThumbnails: file.videoThumbnails, + videoCover: file.videoCover, + immediateThumbnailData: file.immediateThumbnailData, + mimeType: file.mimeType, + size: file.size, + attributes: attributes, + alternativeRepresentations: file.alternativeRepresentations + ) + } + + private func fileAttributesWithResolvedDimensions(_ attributes: [TelegramMediaFileAttribute], dimensions: PixelDimensions) -> ([TelegramMediaFileAttribute], Bool) { + var updatedAttributes: [TelegramMediaFileAttribute] = [] + var didUpdate = false + var hasSizeAttribute = false + + for attribute in attributes { + switch attribute { + case let .ImageSize(size): + hasSizeAttribute = true + if size != dimensions { + updatedAttributes.append(.ImageSize(size: dimensions)) + didUpdate = true + } else { + updatedAttributes.append(attribute) + } + case let .Video(duration, size, flags, preloadSize, coverTime, videoCodec): + hasSizeAttribute = true + if size != dimensions { + updatedAttributes.append(.Video(duration: duration, size: dimensions, flags: flags, preloadSize: preloadSize, coverTime: coverTime, videoCodec: videoCodec)) + didUpdate = true + } else { + updatedAttributes.append(attribute) + } + default: + updatedAttributes.append(attribute) + } + } + + if !hasSizeAttribute { + updatedAttributes.append(.ImageSize(size: dimensions)) + didUpdate = true + } + + return (updatedAttributes, didUpdate) + } + private func openUrl(_ url: InstantPageUrlItem) { var baseUrl = url.url var anchor: String? @@ -1011,7 +1498,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)) |> deliverOnMainQueue).start(next: { peer in if let strongSelf = self, let peer = peer { - if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { strongSelf.getNavigationController()?.pushViewController(controller) } } @@ -1173,16 +1660,16 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in if let self, let image = media.media._asMedia() as? TelegramMediaImage { let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - let _ = copyToPasteboard(context: self.context, postbox: self.context.account.postbox, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() + let _ = copyToPasteboard(context: self.context, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() } }), ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_LinkDialogSave, accessibilityLabel: self.presentationData.strings.Conversation_LinkDialogSave), action: { [weak self] in if let self, let image = media.media._asMedia() as? TelegramMediaImage { let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - let _ = saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() + let _ = saveToCameraRoll(context: self.context, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() } }), 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 +1812,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/BrowserMarkdown.swift b/submodules/BrowserUI/Sources/BrowserMarkdown.swift new file mode 100644 index 0000000000..c3723b4075 --- /dev/null +++ b/submodules/BrowserUI/Sources/BrowserMarkdown.swift @@ -0,0 +1,2280 @@ +import Foundation +import UIKit +import Postbox +import TelegramCore +import AccountContext +import InstantPageUI + +private let markdownPresentationIntentAttribute = NSAttributedString.Key("NSPresentationIntent") +private let markdownInlinePresentationIntentAttribute = NSAttributedString.Key("NSInlinePresentationIntent") +private let markdownLinkAttribute = NSAttributedString.Key("NSLink") +private let markdownImageURLAttribute = NSAttributedString.Key("NSImageURL") +private let markdownAlternateDescriptionAttribute = NSAttributedString.Key("NSAlternateDescription") + +@available(iOS 15.0, *) +private let markdownSoftBreakInlineIntent = InlinePresentationIntent(rawValue: 1 << 6) +@available(iOS 15.0, *) +private let markdownHardBreakInlineIntent = InlinePresentationIntent(rawValue: 1 << 7) +@available(iOS 15.0, *) +private let markdownInlineHTMLInlineIntent = InlinePresentationIntent(rawValue: 1 << 8) + +private let markdownDefaultBlockImageDimensions = PixelDimensions(width: 1200, height: 900) +private let markdownDefaultInlineImageDimensions = PixelDimensions(width: 18, height: 18) +private let markdownImageParsingEnabled = false +private let markdownTaskListUncheckedNumber = "\u{001f}tg-md-task:unchecked" +private let markdownTaskListCheckedNumber = "\u{001f}tg-md-task:checked" +private let markdownRawHTMLTagRegex = try! NSRegularExpression(pattern: #"]*?>"#) +private let markdownFormulaPlaceholderRegex = try! NSRegularExpression(pattern: #"TGMDMATH\d+TGMD"#) +private let markdownVoidHTMLTags: Set = [ + "area", + "base", + "br", + "col", + "embed", + "hr", + "img", + "input", + "link", + "meta", + "param", + "source", + "track", + "wbr" +] + +private struct MarkdownSafetyLimits { + let maxFileSize = 524_288 + let maxLineLength = 32_768 + let maxBlockquoteDepth = 64 + let maxListIndent = 96 + let maxRawHTMLTagCount = 8_000 + let maxRawHTMLNestingDepth = 64 + let maxAttributedStringLength = 400_000 + let maxAttributeRuns = 20_000 + let maxPresentationIntentDepth = 128 + let maxIntentNodes = 10_000 + let maxEmittedBlocks = 5_000 + let maxTableColumns = 32 + let maxTableCells = 2_000 + let maxMediaItems = 100 + let maxInlineHTMLStyleDepth = 32 + let maxDataImageBytes = 2_097_152 + let maxDataImagePixelCount = 12_000_000 + let maxFormulas = 200 + let maxFormulaSourceCharacters = 20_000 + let maxInlineFormulaLength = 256 + let maxBlockFormulaLength = 4_096 +} + +private let markdownSafetyLimits = MarkdownSafetyLimits() + +private final class MarkdownConversionBudget { + let limits: MarkdownSafetyLimits + + private(set) var isExceeded = false + + private var attributeRunCount = 0 + private var intentNodeCount = 0 + private var tableCellCount = 0 + private var mediaItemCount = 0 + + init(limits: MarkdownSafetyLimits) { + self.limits = limits + } + + @discardableResult + func fail() -> Bool { + self.isExceeded = true + return false + } + + @discardableResult + func registerAttributedStringLength(_ length: Int) -> Bool { + guard length <= self.limits.maxAttributedStringLength else { + return self.fail() + } + return true + } + + @discardableResult + func registerAttributeRun() -> Bool { + self.attributeRunCount += 1 + guard self.attributeRunCount <= self.limits.maxAttributeRuns else { + return self.fail() + } + return true + } + + @discardableResult + func registerPresentationIntentDepth(_ depth: Int) -> Bool { + guard depth <= self.limits.maxPresentationIntentDepth else { + return self.fail() + } + return true + } + + @discardableResult + func registerIntentNode() -> Bool { + self.intentNodeCount += 1 + guard self.intentNodeCount <= self.limits.maxIntentNodes else { + return self.fail() + } + return true + } + + @discardableResult + func registerBlockDepth(_ depth: Int) -> Bool { + guard depth <= self.limits.maxPresentationIntentDepth else { + return self.fail() + } + return true + } + + @discardableResult + func registerTableColumns(_ count: Int) -> Bool { + guard count <= self.limits.maxTableColumns else { + return self.fail() + } + return true + } + + @discardableResult + func registerTableCells(_ count: Int) -> Bool { + self.tableCellCount += count + guard self.tableCellCount <= self.limits.maxTableCells else { + return self.fail() + } + return true + } + + @discardableResult + func registerMediaItem() -> Bool { + self.mediaItemCount += 1 + guard self.mediaItemCount <= self.limits.maxMediaItems else { + return self.fail() + } + return true + } + + @discardableResult + func registerInlineHTMLStyleDepth(_ depth: Int) -> Bool { + guard depth <= self.limits.maxInlineHTMLStyleDepth else { + return self.fail() + } + return true + } + + @discardableResult + func validateFinalBlocks(_ blocks: [InstantPageBlock]) -> Bool { + var count = 0 + return self.validate(blocks: blocks, depth: 0, count: &count) + } + + private func validate(blocks: [InstantPageBlock], depth: Int, count: inout Int) -> Bool { + guard self.registerBlockDepth(depth) else { + return false + } + for block in blocks { + count += 1 + guard count <= self.limits.maxEmittedBlocks else { + return self.fail() + } + switch block { + case let .list(items, _): + for item in items { + if !self.validate(listItem: item, depth: depth + 1, count: &count) { + return false + } + } + case let .details(_, nestedBlocks, _): + if !self.validate(blocks: nestedBlocks, depth: depth + 1, count: &count) { + return false + } + default: + break + } + } + return true + } + + private func validate(listItem: InstantPageListItem, depth: Int, count: inout Int) -> Bool { + guard self.registerBlockDepth(depth) else { + return false + } + if case let .blocks(blocks, _) = listItem { + return self.validate(blocks: blocks, depth: depth + 1, count: &count) + } else { + return true + } + } +} + +private struct MarkdownPageResult { + let blocks: [InstantPageBlock] + let media: [MediaId: Media] +} + +private enum MarkdownFormulaMode { + case inline + case block +} + +private struct MarkdownFormulaDescriptor { + let placeholder: String + let latex: String + let mode: MarkdownFormulaMode +} + +private struct MarkdownPreparedSource { + let text: String + let formulasByPlaceholder: [String: MarkdownFormulaDescriptor] +} + +private enum MarkdownInlineTextSegment { + case plain(String) + case formula(MarkdownFormulaDescriptor) +} + +private enum MarkdownInlineFragment { + case richText(RichText) + case formula(MarkdownFormulaDescriptor, RichText) + case image(MarkdownResolvedImage) +} + +private struct MarkdownInlineContent { + let fragments: [MarkdownInlineFragment] + + var richText: RichText { + var result: [RichText] = [] + result.reserveCapacity(self.fragments.count) + + for fragment in self.fragments { + switch fragment { + case let .richText(text): + result.append(text) + case let .formula(_, text): + result.append(text) + case let .image(image): + var text: RichText = .image(id: image.mediaId, dimensions: image.inlineDimensions) + if let linkUrl = image.linkUrl { + text = .url(text: text, url: linkUrl, webpageId: nil) + } + result.append(text) + } + } + + return markdownCompact(result) + } + + var standaloneBlockFormula: MarkdownFormulaDescriptor? { + var result: MarkdownFormulaDescriptor? + + for fragment in self.fragments { + switch fragment { + case let .richText(text): + if !markdownIsWhitespaceOnly(text) { + return nil + } + case let .formula(descriptor, _): + guard descriptor.mode == .block else { + return nil + } + if result != nil { + return nil + } + result = descriptor + case .image: + return nil + } + } + + return result + } + + var standaloneImage: MarkdownResolvedImage? { + var result: MarkdownResolvedImage? + + for fragment in self.fragments { + switch fragment { + case let .richText(text): + if !markdownIsWhitespaceOnly(text) { + return nil + } + case .formula: + return nil + case let .image(image): + if result != nil { + return nil + } + result = image + } + } + + return result + } +} + +private struct MarkdownResolvedImage { + let mediaId: MediaId + let inlineDimensions: PixelDimensions + let caption: InstantPageCaption + let linkUrl: String? +} + +private enum MarkdownResolvedImageSource { + case remote(String) + case data(Data, PixelDimensions) + case unsupported +} + +private enum MarkdownTaskListState { + case unchecked + case checked +} + +private final class MarkdownConversionContext { + private let context: AccountContext + fileprivate let documentURL: URL + fileprivate let formulasByPlaceholder: [String: MarkdownFormulaDescriptor] + fileprivate let budget: MarkdownConversionBudget + private var nextRemoteMediaId: Int64 = 0 + private var nextLocalMediaId: Int64 = 0 + + private(set) var media: [MediaId: Media] = [:] + + init(context: AccountContext, documentURL: URL, formulasByPlaceholder: [String: MarkdownFormulaDescriptor], budget: MarkdownConversionBudget) { + self.context = context + self.documentURL = documentURL + self.formulasByPlaceholder = formulasByPlaceholder + self.budget = budget + } + + func makePageResult(blocks: [InstantPageBlock]) -> MarkdownPageResult { + return MarkdownPageResult(blocks: blocks, media: self.media) + } + + func resolveImage(attributes: [NSAttributedString.Key: Any]) -> MarkdownResolvedImage? { + guard markdownImageParsingEnabled else { + return nil + } + guard let imageUrl = markdownImageURL(attributes: attributes) else { + return nil + } + + let inlineDimensions = markdownInlineImageDimensions(attributes: attributes) + let caption = markdownImageCaption(markdownAlternateDescription(attributes: attributes)) + let linkUrl = markdownLink(attributes: attributes, documentURL: self.documentURL) + + switch markdownResolveImageSource(imageUrl, limits: self.budget.limits) { + case let .remote(url): + guard self.budget.registerMediaItem() else { + return nil + } + let mediaId = self.nextMediaId(namespace: Namespaces.Media.CloudImage) + self.media[mediaId] = TelegramMediaImage( + imageId: mediaId, + representations: [ + TelegramMediaImageRepresentation( + dimensions: markdownDefaultBlockImageDimensions, + resource: InstantPageExternalMediaResource(url: url), + progressiveSizes: [], + immediateThumbnailData: nil + ) + ], + immediateThumbnailData: nil, + reference: nil, + partialReference: nil, + flags: [] + ) + return MarkdownResolvedImage( + mediaId: mediaId, + inlineDimensions: inlineDimensions, + caption: caption, + linkUrl: linkUrl + ) + case let .data(data, dimensions): + guard self.budget.registerMediaItem() else { + return nil + } + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: Int64(data.count), isSecretRelated: false) + self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) + + let mediaId = self.nextMediaId(namespace: Namespaces.Media.LocalImage) + self.media[mediaId] = TelegramMediaImage( + imageId: mediaId, + representations: [ + TelegramMediaImageRepresentation( + dimensions: dimensions, + resource: resource, + progressiveSizes: [], + immediateThumbnailData: nil + ) + ], + immediateThumbnailData: nil, + reference: nil, + partialReference: nil, + flags: [] + ) + return MarkdownResolvedImage( + mediaId: mediaId, + inlineDimensions: inlineDimensions, + caption: caption, + linkUrl: linkUrl + ) + case .unsupported: + return nil + } + } + + private func nextMediaId(namespace: Int32) -> MediaId { + switch namespace { + case Namespaces.Media.LocalImage: + self.nextLocalMediaId += 1 + return MediaId(namespace: namespace, id: self.nextLocalMediaId) + default: + self.nextRemoteMediaId += 1 + return MediaId(namespace: namespace, id: self.nextRemoteMediaId) + } + } +} + +private func markdownPassesPreflight(data: Data, limits: MarkdownSafetyLimits) -> Bool { + guard data.count <= limits.maxFileSize else { + return false + } + guard let text = markdownDecodedSourceText(data) else { + return false + } + return markdownPassesPreflight(text: text, limits: limits) +} + +private func markdownDecodedSourceText(_ data: Data) -> String? { + if let text = String(data: data, encoding: .utf8) { + return text + } + if data.starts(with: [0xff, 0xfe]) { + return String(data: data, encoding: .utf16LittleEndian) + } + if data.starts(with: [0xfe, 0xff]) { + return String(data: data, encoding: .utf16BigEndian) + } + return nil +} + +private func markdownPassesPreflight(text: String, limits: MarkdownSafetyLimits) -> Bool { + var activeFence: Character? + var htmlTagCount = 0 + var htmlDepth = 0 + + for rawLine in text.split(separator: "\n", omittingEmptySubsequences: false) { + var line = String(rawLine) + if line.hasSuffix("\r") { + line.removeLast() + } + + if (line as NSString).length > limits.maxLineLength { + return false + } + + if let fenceMarker = markdownFenceMarker(in: line) { + if activeFence == fenceMarker { + activeFence = nil + } else { + activeFence = fenceMarker + } + continue + } + + if activeFence != nil { + continue + } + + if markdownBlockquoteDepth(in: line) > limits.maxBlockquoteDepth { + return false + } + if markdownIndentWidth(in: line) > limits.maxListIndent { + return false + } + if !markdownScanRawHTMLTags(in: line, limits: limits, tagCount: &htmlTagCount, depth: &htmlDepth) { + return false + } + } + + return true +} + +private func markdownFenceMarker(in line: String) -> Character? { + let trimmed = line.drop(while: { $0 == " " || $0 == "\t" }) + guard let marker = trimmed.first, marker == "`" || marker == "~" else { + return nil + } + var count = 0 + var index = trimmed.startIndex + while index < trimmed.endIndex, trimmed[index] == marker { + count += 1 + index = trimmed.index(after: index) + } + return count >= 3 ? marker : nil +} + +private func markdownBlockquoteDepth(in line: String) -> Int { + var depth = 0 + var index = line.startIndex + + while index < line.endIndex { + switch line[index] { + case " ", "\t": + index = line.index(after: index) + case ">": + depth += 1 + index = line.index(after: index) + if index < line.endIndex, (line[index] == " " || line[index] == "\t") { + index = line.index(after: index) + } + default: + return depth + } + } + + return depth +} + +private func markdownIndentWidth(in line: String) -> Int { + var width = 0 + for character in line { + switch character { + case " ": + width += 1 + case "\t": + width += 4 + default: + return width + } + } + return width +} + +private func markdownScanRawHTMLTags(in line: String, limits: MarkdownSafetyLimits, tagCount: inout Int, depth: inout Int) -> Bool { + let nsLine = line as NSString + let matches = markdownRawHTMLTagRegex.matches(in: line, range: NSRange(location: 0, length: nsLine.length)) + + for match in matches { + tagCount += 1 + guard tagCount <= limits.maxRawHTMLTagCount else { + return false + } + + let tagText = nsLine.substring(with: match.range) + let tagName = nsLine.substring(with: match.range(at: 1)).lowercased() + let lowercasedTag = tagText.lowercased() + + if lowercasedTag.hasPrefix("") && !markdownVoidHTMLTags.contains(tagName) { + depth += 1 + guard depth <= limits.maxRawHTMLNestingDepth else { + return false + } + } + } + + return true +} + +private func markdownPreparedSource(text: String, limits: MarkdownSafetyLimits) -> MarkdownPreparedSource { + let lines = markdownLinesPreservingEndings(text) + + var activeFence: Character? + var formulasByPlaceholder: [String: MarkdownFormulaDescriptor] = [:] + var acceptedFormulaCount = 0 + var totalFormulaCharacters = 0 + var nextPlaceholderIndex = 0 + var result = "" + + var lineIndex = 0 + while lineIndex < lines.count { + let line = lines[lineIndex] + let (content, _) = markdownLineContentAndEnding(line) + + if let fenceMarker = markdownFenceMarker(in: content) { + if activeFence == fenceMarker { + activeFence = nil + } else { + activeFence = fenceMarker + } + result.append(contentsOf: line) + lineIndex += 1 + continue + } + + if activeFence != nil { + result.append(contentsOf: line) + lineIndex += 1 + continue + } + + if let replacement = markdownBlockFormulaReplacement( + in: lines, + startLineIndex: lineIndex, + limits: limits, + acceptedFormulaCount: &acceptedFormulaCount, + totalFormulaCharacters: &totalFormulaCharacters, + nextPlaceholderIndex: &nextPlaceholderIndex + ) { + result.append(contentsOf: replacement.replacement) + formulasByPlaceholder[replacement.descriptor.placeholder] = replacement.descriptor + lineIndex = replacement.nextLineIndex + continue + } + + let processedLine = markdownReplacingInlineFormulas( + in: line, + limits: limits, + acceptedFormulaCount: &acceptedFormulaCount, + totalFormulaCharacters: &totalFormulaCharacters, + nextPlaceholderIndex: &nextPlaceholderIndex, + formulasByPlaceholder: &formulasByPlaceholder + ) + result.append(contentsOf: processedLine) + lineIndex += 1 + } + + return MarkdownPreparedSource(text: result, formulasByPlaceholder: formulasByPlaceholder) +} + +private func markdownLinesPreservingEndings(_ text: String) -> [String] { + guard !text.isEmpty else { + return [] + } + + var result: [String] = [] + var lineStart = text.startIndex + var index = text.startIndex + + while index < text.endIndex { + if text[index] == "\n" { + let nextIndex = text.index(after: index) + result.append(String(text[lineStart ..< nextIndex])) + lineStart = nextIndex + } + index = text.index(after: index) + } + + if lineStart < text.endIndex { + result.append(String(text[lineStart...])) + } + + return result +} + +private func markdownLineContentAndEnding(_ line: String) -> (content: String, ending: String) { + if line.hasSuffix("\r\n") { + return (String(line.dropLast(2)), "\r\n") + } else if line.hasSuffix("\n") { + return (String(line.dropLast()), "\n") + } else { + return (line, "") + } +} + +private func markdownFormulaPlaceholder(_ index: Int) -> String { + return "TGMDMATH\(index)TGMD" +} + +private func markdownAcceptedFormulaDescriptor( + latex: String, + mode: MarkdownFormulaMode, + limits: MarkdownSafetyLimits, + acceptedFormulaCount: inout Int, + totalFormulaCharacters: inout Int, + nextPlaceholderIndex: inout Int +) -> MarkdownFormulaDescriptor? { + let normalizedLatex: String + switch mode { + case .inline: + normalizedLatex = latex + case .block: + normalizedLatex = latex.trimmingCharacters(in: .whitespacesAndNewlines) + } + + guard !normalizedLatex.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + return nil + } + + let latexLength = (normalizedLatex as NSString).length + let maxLength = mode == .inline ? limits.maxInlineFormulaLength : limits.maxBlockFormulaLength + guard latexLength <= maxLength else { + return nil + } + guard acceptedFormulaCount < limits.maxFormulas else { + return nil + } + guard totalFormulaCharacters + latexLength <= limits.maxFormulaSourceCharacters else { + return nil + } + + let placeholder = markdownFormulaPlaceholder(nextPlaceholderIndex) + nextPlaceholderIndex += 1 + acceptedFormulaCount += 1 + totalFormulaCharacters += latexLength + return MarkdownFormulaDescriptor(placeholder: placeholder, latex: normalizedLatex, mode: mode) +} + +private func markdownBlockFormulaReplacement( + in lines: [String], + startLineIndex: Int, + limits: MarkdownSafetyLimits, + acceptedFormulaCount: inout Int, + totalFormulaCharacters: inout Int, + nextPlaceholderIndex: inout Int +) -> (replacement: String, nextLineIndex: Int, descriptor: MarkdownFormulaDescriptor)? { + guard startLineIndex >= 0, startLineIndex < lines.count else { + return nil + } + + let (content, _) = markdownLineContentAndEnding(lines[startLineIndex]) + let indentation = String(content.prefix { $0 == " " || $0 == "\t" }) + let trimmedStart = content.drop(while: { $0 == " " || $0 == "\t" }) + + let opener: String + let closer: String + if trimmedStart.hasPrefix("$$") { + opener = "$$" + closer = "$$" + } else if trimmedStart.hasPrefix("\\[") { + opener = "\\[" + closer = "\\]" + } else { + return nil + } + + let openerContent = String(trimmedStart.dropFirst(opener.count)) + var latex = "" + + if let closeRange = markdownFirstUnescapedRange(of: closer, in: openerContent, from: openerContent.startIndex) { + let trailing = String(openerContent[closeRange.upperBound...]) + guard trailing.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + return nil + } + latex = String(openerContent[.. String { + let (content, ending) = markdownLineContentAndEnding(line) + guard !content.isEmpty else { + return line + } + + var result = "" + var index = content.startIndex + var activeCodeDelimiterLength: Int? + + while index < content.endIndex { + if content[index] == "`" { + let delimiterEnd = markdownIndex(afterRepeating: "`", in: content, from: index) + let delimiterLength = content.distance(from: index, to: delimiterEnd) + result.append(contentsOf: content[index ..< delimiterEnd]) + if activeCodeDelimiterLength == delimiterLength { + activeCodeDelimiterLength = nil + } else { + activeCodeDelimiterLength = delimiterLength + } + index = delimiterEnd + continue + } + + if activeCodeDelimiterLength != nil { + result.append(content[index]) + index = content.index(after: index) + continue + } + + if content[index] == "\\", !markdownIsEscaped(content, at: index), let nextIndex = markdownIndex(content, offsetBy: 1, from: index), nextIndex < content.endIndex, content[nextIndex] == "(" { + let bodyStart = content.index(after: nextIndex) + if let closeRange = markdownFirstUnescapedRange(of: "\\)", in: content, from: bodyStart) { + let latex = String(content[bodyStart ..< closeRange.lowerBound]) + if let descriptor = markdownAcceptedFormulaDescriptor( + latex: latex, + mode: .inline, + limits: limits, + acceptedFormulaCount: &acceptedFormulaCount, + totalFormulaCharacters: &totalFormulaCharacters, + nextPlaceholderIndex: &nextPlaceholderIndex + ) { + result.append(contentsOf: descriptor.placeholder) + formulasByPlaceholder[descriptor.placeholder] = descriptor + index = closeRange.upperBound + continue + } + } + } + + if content[index] == "$", !markdownIsEscaped(content, at: index) { + guard let bodyStart = markdownIndex(content, offsetBy: 1, from: index), bodyStart < content.endIndex else { + result.append(content[index]) + index = content.index(after: index) + continue + } + if content[bodyStart] == "$" || content[bodyStart].isWhitespace { + result.append(content[index]) + index = content.index(after: index) + continue + } + + var searchIndex = bodyStart + var matchedRange: Range? + while let closeRange = markdownFirstUnescapedRange(of: "$", in: content, from: searchIndex) { + let closerIndex = closeRange.lowerBound + let previousIndex = content.index(before: closerIndex) + if !content[previousIndex].isWhitespace { + matchedRange = closeRange + break + } + searchIndex = closeRange.upperBound + } + + if let matchedRange { + let latex = String(content[bodyStart ..< matchedRange.lowerBound]) + if let descriptor = markdownAcceptedFormulaDescriptor( + latex: latex, + mode: .inline, + limits: limits, + acceptedFormulaCount: &acceptedFormulaCount, + totalFormulaCharacters: &totalFormulaCharacters, + nextPlaceholderIndex: &nextPlaceholderIndex + ) { + result.append(contentsOf: descriptor.placeholder) + formulasByPlaceholder[descriptor.placeholder] = descriptor + index = matchedRange.upperBound + continue + } + } + } + + result.append(content[index]) + index = content.index(after: index) + } + + result.append(contentsOf: ending) + return result +} + +private func markdownFirstUnescapedRange(of pattern: String, in text: String, from startIndex: String.Index) -> Range? { + guard !pattern.isEmpty, startIndex <= text.endIndex else { + return nil + } + + var searchIndex = startIndex + while searchIndex < text.endIndex { + guard let range = text.range(of: pattern, range: searchIndex ..< text.endIndex) else { + return nil + } + if !markdownIsEscaped(text, at: range.lowerBound) { + return range + } + searchIndex = range.upperBound + } + return nil +} + +private func markdownIsEscaped(_ text: String, at index: String.Index) -> Bool { + guard index > text.startIndex else { + return false + } + + var slashCount = 0 + var currentIndex = text.index(before: index) + while true { + if text[currentIndex] == "\\" { + slashCount += 1 + } else { + break + } + guard currentIndex > text.startIndex else { + break + } + currentIndex = text.index(before: currentIndex) + } + + return slashCount % 2 == 1 +} + +private func markdownIndex(_ text: String, offsetBy distance: Int, from index: String.Index) -> String.Index? { + guard distance >= 0 else { + return nil + } + return text.index(index, offsetBy: distance, limitedBy: text.endIndex) +} + +private func markdownIndex(afterRepeating character: Character, in text: String, from startIndex: String.Index) -> String.Index { + var index = startIndex + while index < text.endIndex, text[index] == character { + index = text.index(after: index) + } + return index +} + +func markdownWebpage(context: AccountContext, file: FileMediaReference) -> (webPage: TelegramMediaWebpage, fileURL: URL)? { + guard #available(iOS 15.0, *) else { + return nil + } + guard let path = context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.media.resource.id)) else { + return nil + } + let fileURL = URL(fileURLWithPath: path) + guard let data = try? Data(contentsOf: fileURL) else { + return nil + } + guard let webPage = markdownWebpage(context: context, file: file, fileURL: fileURL, data: data) else { + return nil + } + return (webPage, fileURL) +} + +@available(iOS 15.0, *) +private func markdownWebpage(context: AccountContext, file: FileMediaReference, fileURL: URL, data: Data) -> TelegramMediaWebpage? { + let limits = markdownSafetyLimits + guard markdownPassesPreflight(data: data, limits: limits) else { + return nil + } + guard let sourceText = markdownDecodedSourceText(data) else { + return nil + } + let preparedSource = markdownPreparedSource(text: sourceText, limits: limits) + + let attributedString: NSAttributedString + do { + attributedString = try NSAttributedString( + markdown: Data(preparedSource.text.utf8), + options: .init(), + baseURL: fileURL.deletingLastPathComponent() + ) + } catch { + return nil + } + + let budget = MarkdownConversionBudget(limits: limits) + let conversionContext = MarkdownConversionContext(context: context, documentURL: fileURL, formulasByPlaceholder: preparedSource.formulasByPlaceholder, budget: budget) + guard let pageResult = markdownPageResult(from: attributedString, context: conversionContext) else { + return nil + } + let blocks = markdownBlocksWithGeneratedAnchors(pageResult.blocks) + guard !blocks.isEmpty, budget.validateFinalBlocks(blocks) else { + return nil + } + + let title = markdownTitle(from: blocks, file: file, fileURL: fileURL) + let text = markdownFirstParagraphText(from: blocks) + let instantPage = InstantPage( + blocks: blocks, + media: pageResult.media, + isComplete: true, + rtl: false, + url: fileURL.absoluteString, + views: nil + ) + + return TelegramMediaWebpage( + webpageId: MediaId(namespace: 0, id: 0), + content: .Loaded( + TelegramMediaWebpageLoadedContent( + url: fileURL.absoluteString, + displayUrl: fileURL.absoluteString, + hash: 0, + type: "article", + websiteName: nil, + title: title, + text: text, + embedUrl: nil, + embedType: nil, + embedSize: nil, + duration: nil, + author: nil, + isMediaLargeByDefault: nil, + imageIsVideoCover: false, + image: nil, + file: nil, + story: nil, + attributes: [], + instantPage: instantPage + ) + ) + ) +} + +@available(iOS 15.0, *) +private func markdownPageResult(from attributedString: NSAttributedString, context: MarkdownConversionContext) -> MarkdownPageResult? { + guard context.budget.registerAttributedStringLength(attributedString.length) else { + return nil + } + + var nodesByIdentity: [Int: MarkdownIntentNode] = [:] + var rootNodes: [MarkdownIntentNode] = [] + var rootIdentities: Set = [] + var didAbort = false + + attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: []) { attributes, range, stop in + guard range.length > 0 else { + return + } + guard context.budget.registerAttributeRun() else { + didAbort = true + stop.pointee = true + return + } + guard let presentationIntent = attributes[markdownPresentationIntentAttribute] as? PresentationIntent else { + return + } + let components = presentationIntent.components + guard !components.isEmpty else { + return + } + guard context.budget.registerPresentationIntentDepth(components.count) else { + didAbort = true + stop.pointee = true + return + } + + var orderedNodes: [MarkdownIntentNode] = [] + for component in components.reversed() { + let node: MarkdownIntentNode + if let current = nodesByIdentity[component.identity] { + node = current + } else { + guard context.budget.registerIntentNode() else { + didAbort = true + stop.pointee = true + return + } + let created = MarkdownIntentNode(component: component) + nodesByIdentity[component.identity] = created + node = created + } + orderedNodes.append(node) + } + + if let rootNode = orderedNodes.first, rootIdentities.insert(rootNode.identity).inserted { + rootNodes.append(rootNode) + } + if orderedNodes.count >= 2 { + for index in 0 ..< (orderedNodes.count - 1) { + orderedNodes[index].append(child: orderedNodes[index + 1]) + } + } + if let leafNode = orderedNodes.last { + leafNode.append(text: attributedString.attributedSubstring(from: range)) + } + } + + guard !didAbort, !context.budget.isExceeded else { + return nil + } + guard let blocks = markdownBlocks(from: rootNodes, context: context, depth: 0), !context.budget.isExceeded else { + return nil + } + return context.makePageResult(blocks: blocks) +} + +private func markdownBlocks(from nodes: [MarkdownIntentNode], context: MarkdownConversionContext, depth: Int) -> [InstantPageBlock]? { + guard context.budget.registerBlockDepth(depth), !context.budget.isExceeded else { + return nil + } + + var result: [InstantPageBlock] = [] + for node in nodes { + guard let blocks = markdownBlocks(from: node, context: context, depth: depth + 1) else { + return nil + } + result.append(contentsOf: blocks) + if context.budget.isExceeded { + return nil + } + } + return result +} + +private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConversionContext, depth: Int) -> [InstantPageBlock]? { + guard context.budget.registerBlockDepth(depth), !context.budget.isExceeded else { + return nil + } + + switch node.kind { + case let .table(alignments): + guard let rows = markdownTableRows(from: node.children, alignments: alignments, context: context, depth: depth + 1) else { + return nil + } + guard !rows.isEmpty else { + return [] + } + return [.table(title: .empty, rows: rows, bordered: true, striped: false)] + case let .header(level): + guard let text = markdownRichText(from: node.attributedText, context: context) else { + return nil + } + guard markdownHasDisplayableContent(text) else { + return [] + } + switch level { + case Int.min ... 1: + return [.title(text)] + default: + return [.heading(text: text, level: Int32(max(2, min(level, 6))))] + } + case .paragraph: + guard let inlineContent = markdownInlineContent(from: node.attributedText, context: context) else { + return nil + } + if let formula = inlineContent.standaloneBlockFormula { + return [.formula(latex: formula.latex)] + } + if let image = inlineContent.standaloneImage { + return [ + .image( + id: image.mediaId, + caption: image.caption, + url: image.linkUrl, + webpageId: nil + ) + ] + } + let text = inlineContent.richText + guard markdownHasDisplayableContent(text) else { + return [] + } + return [.paragraph(text)] + case let .codeBlock(languageHint): + guard let text = markdownRichText(from: markdownTrimTrailingCodeBlockNewline(node.attributedText), context: context) else { + return nil + } + guard markdownHasDisplayableContent(text) else { + return [] + } + return [.preformatted(text: text, language: markdownNormalizedCodeBlockLanguage(languageHint))] + case .thematicBreak: + return [.divider] + case .blockQuote: + var result: [InstantPageBlock] = [] + for child in node.children { + guard let childBlocks = markdownBlocks(from: child, context: context, depth: depth + 1) else { + return nil + } + for childBlock in childBlocks { + switch childBlock { + case let .paragraph(text): + result.append(.blockQuote(text: text, caption: .empty)) + default: + let plainText = markdownPlainText(from: childBlock, depth: depth + 1) + if !plainText.isEmpty { + result.append(.blockQuote(text: .plain(plainText), caption: .empty)) + } + } + } + } + return result + case .orderedList: + guard let items = markdownListItems(from: node.children, ordered: true, context: context, depth: depth + 1) else { + return nil + } + guard !items.isEmpty else { + return [] + } + return [.list(items: items, ordered: true)] + case .unorderedList: + guard let items = markdownListItems(from: node.children, ordered: false, context: context, depth: depth + 1) else { + return nil + } + guard !items.isEmpty else { + return [] + } + return [.list(items: items, ordered: false)] + case .listItem(_), .tableHeaderRow, .tableRow, .tableCell(_), .unknown: + return markdownBlocks(from: node.children, context: context, depth: depth + 1) + } +} + +private func markdownListItems(from nodes: [MarkdownIntentNode], ordered: Bool, context: MarkdownConversionContext, depth: Int) -> [InstantPageListItem]? { + guard context.budget.registerBlockDepth(depth), !context.budget.isExceeded else { + return nil + } + + var result: [InstantPageListItem] = [] + for node in nodes { + guard case let .listItem(ordinal) = node.kind else { + continue + } + guard var blocks = markdownBlocks(from: node.children, context: context, depth: depth + 1) else { + return nil + } + let taskListState = markdownApplyTaskListMarker(to: &blocks) + let number: String? + if let taskListState { + number = markdownTaskListNumber(for: taskListState) + } else if ordered { + number = "\(ordinal)" + } else { + number = nil + } + if blocks.isEmpty { + if let number { + result.append(.text(.plain(" "), number)) + } + continue + } + if blocks.count == 1, case let .paragraph(text) = blocks[0] { + if number != nil && markdownIsWhitespaceOnly(text) { + result.append(.text(.plain(" "), number)) + } else { + result.append(.text(text, number)) + } + } else { + result.append(.blocks(blocks, number)) + } + } + return result +} + +private func markdownTaskListNumber(for state: MarkdownTaskListState) -> String { + switch state { + case .unchecked: + return markdownTaskListUncheckedNumber + case .checked: + return markdownTaskListCheckedNumber + } +} + +private func markdownApplyTaskListMarker(to blocks: inout [InstantPageBlock]) -> MarkdownTaskListState? { + guard !blocks.isEmpty, case let .paragraph(text) = blocks[0] else { + return nil + } + guard let (state, strippedText) = markdownStrippingTaskListMarker(from: text) else { + return nil + } + if blocks.count > 1 && markdownIsWhitespaceOnly(strippedText) { + blocks.removeFirst() + } else { + blocks[0] = .paragraph(strippedText) + } + return state +} + +private func markdownStrippingTaskListMarker(from text: RichText) -> (MarkdownTaskListState, RichText)? { + guard let (state, markerLength) = markdownTaskListMarker(in: text.plainText) else { + return nil + } + return (state, markdownDroppingPrefixLength(markerLength, from: text)) +} + +private func markdownTaskListMarker(in plainText: String) -> (MarkdownTaskListState, Int)? { + switch plainText { + case _ where plainText.hasPrefix("[ ] "): + return (.unchecked, 4) + case "[ ]": + return (.unchecked, 3) + case _ where plainText.hasPrefix("[x] "), _ where plainText.hasPrefix("[X] "): + return (.checked, 4) + case "[x]", "[X]": + return (.checked, 3) + default: + return nil + } +} + +private func markdownTableRows(from nodes: [MarkdownIntentNode], alignments: [TableHorizontalAlignment], context: MarkdownConversionContext, depth: Int) -> [InstantPageTableRow]? { + guard context.budget.registerBlockDepth(depth), !context.budget.isExceeded else { + return nil + } + + var result: [InstantPageTableRow] = [] + for node in nodes { + switch node.kind { + case .tableHeaderRow: + guard let cells = markdownTableCells(from: node.children, alignments: alignments, header: true, context: context, depth: depth + 1) else { + return nil + } + if !cells.isEmpty { + result.append(InstantPageTableRow(cells: cells)) + } + case .tableRow: + guard let cells = markdownTableCells(from: node.children, alignments: alignments, header: false, context: context, depth: depth + 1) else { + return nil + } + if !cells.isEmpty { + result.append(InstantPageTableRow(cells: cells)) + } + default: + continue + } + } + return context.budget.isExceeded ? nil : result +} + +private func markdownTableCells(from nodes: [MarkdownIntentNode], alignments: [TableHorizontalAlignment], header: Bool, context: MarkdownConversionContext, depth: Int) -> [InstantPageTableCell]? { + guard context.budget.registerBlockDepth(depth), !context.budget.isExceeded else { + return nil + } + + let maxColumnIndex = nodes.reduce(-1) { partialResult, node in + if case let .tableCell(column) = node.kind { + return max(partialResult, column) + } else { + return partialResult + } + } + let columnCount = max(alignments.count, maxColumnIndex + 1) + guard columnCount > 0 else { + return [] + } + guard context.budget.registerTableColumns(columnCount) else { + return nil + } + + var result: [InstantPageTableCell] = [] + var nextColumn = 0 + + for node in nodes { + guard case let .tableCell(column) = node.kind else { + continue + } + + while nextColumn < column { + result.append(markdownEmptyTableCell(header: header, alignment: markdownTableAlignment(at: nextColumn, from: alignments))) + nextColumn += 1 + } + + guard let text = markdownRichText(from: node.attributedText, context: context) else { + return nil + } + result.append( + InstantPageTableCell( + text: text, + header: header, + alignment: markdownTableAlignment(at: column, from: alignments), + verticalAlignment: .top, + colspan: 1, + rowspan: 1 + ) + ) + nextColumn = max(nextColumn, column + 1) + } + + while nextColumn < columnCount { + result.append(markdownEmptyTableCell(header: header, alignment: markdownTableAlignment(at: nextColumn, from: alignments))) + nextColumn += 1 + } + + guard context.budget.registerTableCells(result.count) else { + return nil + } + return result +} + +private func markdownEmptyTableCell(header: Bool, alignment: TableHorizontalAlignment) -> InstantPageTableCell { + return InstantPageTableCell( + text: .empty, + header: header, + alignment: alignment, + verticalAlignment: .top, + colspan: 1, + rowspan: 1 + ) +} + +private func markdownTableAlignment(at index: Int, from alignments: [TableHorizontalAlignment]) -> TableHorizontalAlignment { + guard index >= 0, index < alignments.count else { + return .left + } + return alignments[index] +} + +private func markdownRichText(from attributedString: NSAttributedString, context: MarkdownConversionContext) -> RichText? { + return markdownInlineContent(from: attributedString, context: context)?.richText +} + +private func markdownInlineContent(from attributedString: NSAttributedString, context: MarkdownConversionContext) -> MarkdownInlineContent? { + guard attributedString.length > 0, #available(iOS 15.0, *) else { + return MarkdownInlineContent(fragments: []) + } + + var fragments: [MarkdownInlineFragment] = [] + var htmlStyles: [MarkdownHTMLInlineStyle] = [] + var consumeNextSoftBreak = false + var didAbort = false + + attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: []) { attributes, range, stop in + guard range.length > 0 else { + return + } + if context.budget.isExceeded { + didAbort = true + stop.pointee = true + return + } + + let text = attributedString.attributedSubstring(from: range).string + guard !text.isEmpty else { + return + } + + let inlineIntent: InlinePresentationIntent? + if let inlineIntentValue = attributes[markdownInlinePresentationIntentAttribute] as? InlinePresentationIntent { + inlineIntent = inlineIntentValue + } else if let inlineIntentValue = attributes[markdownInlinePresentationIntentAttribute] as? NSNumber { + inlineIntent = InlinePresentationIntent(rawValue: inlineIntentValue.uintValue) + } else { + inlineIntent = nil + } + + if let inlineIntent, inlineIntent.contains(markdownInlineHTMLInlineIntent), let directive = markdownHTMLDirective(for: text) { + switch directive { + case let .open(style): + htmlStyles.append(style) + guard context.budget.registerInlineHTMLStyleDepth(htmlStyles.count) else { + didAbort = true + stop.pointee = true + return + } + case let .close(style): + if let index = htmlStyles.lastIndex(of: style) { + htmlStyles.remove(at: index) + } + case .lineBreak: + fragments.append(.richText(markdownApplyHTMLStyles(htmlStyles, to: .plain("\n")))) + consumeNextSoftBreak = true + } + return + } + + if consumeNextSoftBreak { + if let inlineIntent, inlineIntent.contains(markdownSoftBreakInlineIntent), text == " " { + consumeNextSoftBreak = false + return + } + consumeNextSoftBreak = false + } + + if let image = context.resolveImage(attributes: attributes) { + fragments.append(.image(image)) + return + } + + let segments = markdownInlineTextSegments(from: text, formulasByPlaceholder: context.formulasByPlaceholder) + for segment in segments { + let baseText: RichText + let descriptor: MarkdownFormulaDescriptor? + switch segment { + case let .plain(plainText): + baseText = .plain(plainText) + descriptor = nil + case let .formula(formulaDescriptor): + baseText = .formula(latex: formulaDescriptor.latex) + descriptor = formulaDescriptor + } + + var fragment = markdownApplyingInlineIntent(inlineIntent, to: baseText) + if let url = markdownLink(attributes: attributes, documentURL: context.documentURL) { + fragment = .url(text: fragment, url: url, webpageId: nil) + } + fragment = markdownApplyHTMLStyles(htmlStyles, to: fragment) + + if let descriptor { + fragments.append(.formula(descriptor, fragment)) + } else { + fragments.append(.richText(fragment)) + } + } + } + + guard !didAbort, !context.budget.isExceeded else { + return nil + } + return MarkdownInlineContent(fragments: fragments) +} + +private func markdownInlineTextSegments(from text: String, formulasByPlaceholder: [String: MarkdownFormulaDescriptor]) -> [MarkdownInlineTextSegment] { + guard !text.isEmpty else { + return [] + } + + let nsText = text as NSString + let matches = markdownFormulaPlaceholderRegex.matches(in: text, range: NSRange(location: 0, length: nsText.length)) + guard !matches.isEmpty else { + return [.plain(text)] + } + + var result: [MarkdownInlineTextSegment] = [] + var currentLocation = 0 + + for match in matches { + if match.range.location > currentLocation { + result.append(.plain(nsText.substring(with: NSRange(location: currentLocation, length: match.range.location - currentLocation)))) + } + + let placeholder = nsText.substring(with: match.range) + if let descriptor = formulasByPlaceholder[placeholder] { + result.append(.formula(descriptor)) + } else { + result.append(.plain(placeholder)) + } + currentLocation = match.range.location + match.range.length + } + + if currentLocation < nsText.length { + result.append(.plain(nsText.substring(from: currentLocation))) + } + + return result +} + +@available(iOS 15.0, *) +private func markdownApplyingInlineIntent(_ inlineIntent: InlinePresentationIntent?, to text: RichText) -> RichText { + guard let inlineIntent else { + return text + } + + var result = text + if inlineIntent.contains(.stronglyEmphasized) { + result = .bold(result) + } + if inlineIntent.contains(.emphasized) { + result = .italic(result) + } + if inlineIntent.contains(.strikethrough) { + result = .strikethrough(result) + } + if inlineIntent.contains(.code) { + result = .fixed(result) + } + if inlineIntent.contains(markdownHardBreakInlineIntent) { + result = .plain("\n") + } + return result +} + +private enum MarkdownHTMLInlineStyle: Equatable { + case underline + case `subscript` + case superscript + case marked +} + +private enum MarkdownHTMLDirective { + case open(MarkdownHTMLInlineStyle) + case close(MarkdownHTMLInlineStyle) + case lineBreak +} + +private func markdownHTMLDirective(for text: String) -> MarkdownHTMLDirective? { + switch text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { + case "": + return .open(.underline) + case "": + return .close(.underline) + case "": + return .open(.subscript) + case "": + return .close(.subscript) + case "": + return .open(.superscript) + case "": + return .close(.superscript) + case "": + return .open(.marked) + case "": + return .close(.marked) + case "
", "
", "
": + return .lineBreak + default: + return nil + } +} + +private func markdownApplyHTMLStyles(_ styles: [MarkdownHTMLInlineStyle], to text: RichText) -> RichText { + var result = text + for style in styles { + switch style { + case .underline: + result = .underline(result) + case .subscript: + result = .subscript(result) + case .superscript: + result = .superscript(result) + case .marked: + result = .marked(result) + } + } + return result +} + +private func markdownAlternateDescription(attributes: [NSAttributedString.Key: Any]) -> String? { + if let value = attributes[markdownAlternateDescriptionAttribute] as? String, !value.isEmpty { + return value + } + return nil +} + +private func markdownImageURL(attributes: [NSAttributedString.Key: Any]) -> String? { + if let value = attributes[markdownImageURLAttribute] as? URL { + return value.absoluteString + } + if let value = attributes[markdownImageURLAttribute] as? NSURL { + return (value as URL).absoluteString + } + if let value = attributes[markdownImageURLAttribute] as? String, !value.isEmpty { + return value + } + return nil +} + +private func markdownResolveImageSource(_ value: String, limits: MarkdownSafetyLimits) -> MarkdownResolvedImageSource { + if value.hasPrefix("//") { + return .remote("https:\(value)") + } + + if value.lowercased().hasPrefix("data:") { + return markdownResolveDataImageSource(value, limits: limits) + } + + guard let url = URL(string: value), let scheme = url.scheme?.lowercased() else { + return .unsupported + } + + switch scheme { + case "http", "https": + return .remote(url.absoluteString) + case "data": + return markdownResolveDataImageSource(url.absoluteString, limits: limits) + default: + return .unsupported + } +} + +private func markdownResolveDataImageSource(_ value: String, limits: MarkdownSafetyLimits) -> MarkdownResolvedImageSource { + guard value.lowercased().hasPrefix("data:"), + let commaIndex = value.firstIndex(of: ",") else { + return .unsupported + } + + let header = String(value[value.index(value.startIndex, offsetBy: 5) ..< commaIndex]) + let payloadStart = value.index(after: commaIndex) + let payload = String(value[payloadStart...]) + let isBase64 = header.lowercased().contains(";base64") + + let data: Data? + if isBase64 { + data = Data(base64Encoded: payload, options: [.ignoreUnknownCharacters]) + } else if let decodedPayload = payload.removingPercentEncoding { + data = decodedPayload.data(using: .utf8) + } else { + data = nil + } + + guard let data = data, data.count <= limits.maxDataImageBytes else { + return .unsupported + } + + guard let image = UIImage(data: data), + let dimensions = markdownImagePixelDimensions(image) else { + return .unsupported + } + + let pixelCount = Int64(dimensions.width) * Int64(dimensions.height) + guard pixelCount <= Int64(limits.maxDataImagePixelCount) else { + return .unsupported + } + + return .data(data, dimensions) +} + +private func markdownImagePixelDimensions(_ image: UIImage) -> PixelDimensions? { + if let cgImage = image.cgImage { + return PixelDimensions(width: Int32(cgImage.width), height: Int32(cgImage.height)) + } + + let width = max(1, Int32(ceil(image.size.width * image.scale))) + let height = max(1, Int32(ceil(image.size.height * image.scale))) + return PixelDimensions(width: width, height: height) +} + +private func markdownImageCaption(_ title: String?) -> InstantPageCaption { + if let title, !title.isEmpty { + return InstantPageCaption(text: .plain(title), credit: .empty) + } else { + return InstantPageCaption(text: .empty, credit: .empty) + } +} + +private func markdownInlineImageDimensions(attributes: [NSAttributedString.Key: Any]) -> PixelDimensions { + guard let font = attributes[.font] as? UIFont else { + return markdownDefaultInlineImageDimensions + } + + let side = max(markdownDefaultInlineImageDimensions.width, Int32(ceil(font.lineHeight))) + return PixelDimensions(width: side, height: side) +} + +private func markdownLink(attributes: [NSAttributedString.Key: Any], documentURL: URL) -> String? { + if let value = attributes[markdownLinkAttribute] as? URL { + return markdownNormalizedLink(value, documentURL: documentURL) + } + if let value = attributes[markdownLinkAttribute] as? NSURL { + return markdownNormalizedLink(value as URL, documentURL: documentURL) + } + if let value = attributes[markdownLinkAttribute] as? String, !value.isEmpty { + if value.hasPrefix("#") { + return value + } + if let url = URL(string: value) { + return markdownNormalizedLink(url, documentURL: documentURL) + } + return value + } + return nil +} + +private func markdownNormalizedLink(_ url: URL, documentURL: URL) -> String { + if url.baseURL != nil { + let relative = url.relativeString + if relative.hasPrefix("#") { + return relative + } + } + if let fragment = url.fragment, markdownMatchesDocument(url, documentURL: documentURL) { + return "#\(fragment)" + } + return url.absoluteString +} + +private func markdownMatchesDocument(_ url: URL, documentURL: URL) -> Bool { + let normalizedUrl = markdownURLWithoutFragment(url) + let normalizedDocumentURL = markdownURLWithoutFragment(documentURL) + + if normalizedUrl.isFileURL && normalizedDocumentURL.isFileURL { + return normalizedUrl.standardizedFileURL == normalizedDocumentURL.standardizedFileURL + } else { + return normalizedUrl == normalizedDocumentURL + } +} + +private func markdownURLWithoutFragment(_ url: URL) -> URL { + guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { + return url + } + components.fragment = nil + return components.url ?? url +} + +private func markdownCompact(_ fragments: [RichText]) -> RichText { + var compacted: [RichText] = [] + for fragment in fragments { + switch fragment { + case .empty: + continue + case let .plain(text): + guard !text.isEmpty else { + continue + } + if let last = compacted.last, case let .plain(lastText) = last { + compacted[compacted.count - 1] = .plain(lastText + text) + } else { + compacted.append(fragment) + } + case let .concat(items): + let nested = markdownCompact(items) + switch nested { + case .empty: + continue + case let .plain(text): + if let last = compacted.last, case let .plain(lastText) = last { + compacted[compacted.count - 1] = .plain(lastText + text) + } else { + compacted.append(.plain(text)) + } + default: + compacted.append(nested) + } + default: + compacted.append(fragment) + } + } + if compacted.isEmpty { + return .empty + } else if compacted.count == 1 { + return compacted[0] + } else { + return .concat(compacted) + } +} + +private func markdownDroppingPrefixLength(_ length: Int, from text: RichText) -> RichText { + guard length > 0 else { + return text + } + switch text { + case .empty: + return .empty + case let .plain(string): + let nsString = string as NSString + if nsString.length <= length { + return .empty + } else { + return .plain(nsString.substring(from: length)) + } + case let .bold(inner): + let dropped = markdownDroppingPrefixLength(length, from: inner) + return dropped == .empty ? .empty : .bold(dropped) + case let .italic(inner): + let dropped = markdownDroppingPrefixLength(length, from: inner) + return dropped == .empty ? .empty : .italic(dropped) + case let .underline(inner): + let dropped = markdownDroppingPrefixLength(length, from: inner) + return dropped == .empty ? .empty : .underline(dropped) + case let .strikethrough(inner): + let dropped = markdownDroppingPrefixLength(length, from: inner) + return dropped == .empty ? .empty : .strikethrough(dropped) + case let .fixed(inner): + let dropped = markdownDroppingPrefixLength(length, from: inner) + return dropped == .empty ? .empty : .fixed(dropped) + case let .url(inner, url, webpageId): + let dropped = markdownDroppingPrefixLength(length, from: inner) + return dropped == .empty ? .empty : .url(text: dropped, url: url, webpageId: webpageId) + case let .email(inner, email): + let dropped = markdownDroppingPrefixLength(length, from: inner) + return dropped == .empty ? .empty : .email(text: dropped, email: email) + case let .concat(items): + var remainingLength = length + var result: [RichText] = [] + result.reserveCapacity(items.count) + for item in items { + if remainingLength > 0 { + let itemLength = (item.plainText as NSString).length + if itemLength <= remainingLength { + remainingLength -= itemLength + continue + } + result.append(markdownDroppingPrefixLength(remainingLength, from: item)) + remainingLength = 0 + } else { + result.append(item) + } + } + return markdownCompact(result) + case let .subscript(inner): + let dropped = markdownDroppingPrefixLength(length, from: inner) + return dropped == .empty ? .empty : .subscript(dropped) + case let .superscript(inner): + let dropped = markdownDroppingPrefixLength(length, from: inner) + return dropped == .empty ? .empty : .superscript(dropped) + case let .marked(inner): + let dropped = markdownDroppingPrefixLength(length, from: inner) + return dropped == .empty ? .empty : .marked(dropped) + case let .phone(inner, phone): + let dropped = markdownDroppingPrefixLength(length, from: inner) + return dropped == .empty ? .empty : .phone(text: dropped, phone: phone) + case .image: + return text + case let .formula(latex): + let nsLatex = latex as NSString + if nsLatex.length <= length { + return .empty + } else { + return .plain(nsLatex.substring(from: length)) + } + case let .anchor(inner, name): + let dropped = markdownDroppingPrefixLength(length, from: inner) + return dropped == .empty ? .empty : .anchor(text: dropped, name: name) + } +} + +private func markdownHasDisplayableContent(_ richText: RichText) -> Bool { + switch richText { + case .empty: + return false + case let .plain(text): + return !text.isEmpty + case let .bold(text), + let .italic(text), + let .underline(text), + let .strikethrough(text), + let .fixed(text), + let .subscript(text), + let .superscript(text), + let .marked(text), + let .anchor(text, _): + return markdownHasDisplayableContent(text) + case let .url(text, _, _), + let .email(text, _), + let .phone(text, _): + return markdownHasDisplayableContent(text) + case let .concat(items): + return items.contains(where: markdownHasDisplayableContent) + case .image: + return true + case let .formula(latex): + return !latex.isEmpty + } +} + +private func markdownIsWhitespaceOnly(_ richText: RichText) -> Bool { + switch richText { + case .empty: + return true + case let .plain(text): + return text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + case let .bold(text), + let .italic(text), + let .underline(text), + let .strikethrough(text), + let .fixed(text), + let .subscript(text), + let .superscript(text), + let .marked(text), + let .anchor(text, _): + return markdownIsWhitespaceOnly(text) + case let .url(text, _, _), + let .email(text, _), + let .phone(text, _): + return markdownIsWhitespaceOnly(text) + case let .concat(items): + return items.allSatisfy(markdownIsWhitespaceOnly) + case .image: + return false + case let .formula(latex): + return latex.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } +} + +private func markdownPlainText(from block: InstantPageBlock, depth: Int = 0) -> String { + guard depth <= markdownSafetyLimits.maxPresentationIntentDepth else { + return "" + } + + switch block { + case let .title(text): + return text.plainText + case let .subtitle(text): + return text.plainText + case let .authorDate(author, _): + return author.plainText + case let .header(text): + return text.plainText + case let .subheader(text): + return text.plainText + case let .heading(text, _): + return text.plainText + case let .formula(latex): + return latex + case let .paragraph(text): + return text.plainText + case let .preformatted(text, _): + return text.plainText + case let .footer(text): + return text.plainText + case let .blockQuote(text, caption): + return text.plainText.isEmpty ? caption.plainText : text.plainText + case let .pullQuote(text, caption): + return text.plainText.isEmpty ? caption.plainText : text.plainText + case let .kicker(text): + return text.plainText + case let .table(title, _, _, _): + return title.plainText + case let .details(title, _, _): + return title.plainText + case let .relatedArticles(title, _): + return title.plainText + default: + return "" + } +} + +private func markdownTitle(from blocks: [InstantPageBlock], file: FileMediaReference, fileURL: URL) -> String { + for block in blocks { + if case let .title(text) = block, !text.plainText.isEmpty { + return text.plainText + } + } + if let fileName = file.media.fileName, !fileName.isEmpty { + let baseName = URL(fileURLWithPath: fileName).deletingPathExtension().lastPathComponent + if !baseName.isEmpty { + return baseName + } + return fileName + } + let baseName = fileURL.deletingPathExtension().lastPathComponent + if !baseName.isEmpty { + return baseName + } + return fileURL.lastPathComponent +} + +private func markdownNormalizedCodeBlockLanguage(_ language: String?) -> String? { + guard let language else { + return nil + } + let normalized = language.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + return normalized.isEmpty ? nil : normalized +} + +private func markdownFirstParagraphText(from blocks: [InstantPageBlock], depth: Int = 0) -> String? { + guard depth <= markdownSafetyLimits.maxPresentationIntentDepth else { + return nil + } + + for block in blocks { + switch block { + case let .formula(latex): + if !latex.isEmpty { + return latex + } + case let .paragraph(text): + if !text.plainText.isEmpty { + return text.plainText + } + case let .list(items, _): + for item in items { + switch item { + case let .text(text, _): + if !text.plainText.isEmpty { + return text.plainText + } + case let .blocks(blocks, _): + if let text = markdownFirstParagraphText(from: blocks, depth: depth + 1) { + return text + } + default: + break + } + } + case let .details(_, blocks, _): + if let text = markdownFirstParagraphText(from: blocks, depth: depth + 1) { + return text + } + default: + break + } + } + return nil +} + +private func markdownBlocksWithGeneratedAnchors(_ blocks: [InstantPageBlock]) -> [InstantPageBlock] { + var result: [InstantPageBlock] = [] + var slugCounts: [String: Int] = [:] + + for block in blocks { + if let headingText = markdownHeadingText(from: block), !headingText.isEmpty { + let baseSlug = markdownAnchorSlug(from: headingText) + if !baseSlug.isEmpty { + let count = slugCounts[baseSlug] ?? 0 + slugCounts[baseSlug] = count + 1 + + let slug: String + if count == 0 { + slug = baseSlug + } else { + slug = "\(baseSlug)-\(count)" + } + result.append(.anchor(slug)) + } + } + result.append(block) + } + + return result +} + +private func markdownHeadingText(from block: InstantPageBlock) -> String? { + switch block { + case let .title(text): + return text.plainText + case let .header(text): + return text.plainText + case let .subheader(text): + return text.plainText + case let .heading(text, _): + return text.plainText + default: + return nil + } +} + +private func markdownAnchorSlug(from text: String) -> String { + let normalized = text + .folding(options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive], locale: nil) + .lowercased() + + let dashScalar = "-".unicodeScalars.first! + let separatorSet = CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: "-_")) + var scalars: [UnicodeScalar] = [] + var previousWasDash = false + + for scalar in normalized.unicodeScalars { + if CharacterSet.alphanumerics.contains(scalar) { + scalars.append(scalar) + previousWasDash = false + } else if separatorSet.contains(scalar) { + if !scalars.isEmpty && !previousWasDash { + scalars.append(dashScalar) + previousWasDash = true + } + } + } + + if scalars.last == dashScalar { + scalars.removeLast() + } + + return String(String.UnicodeScalarView(scalars)) +} + +private func markdownTrimTrailingCodeBlockNewline(_ attributedString: NSAttributedString) -> NSAttributedString { + guard attributedString.length > 0 else { + return attributedString + } + let mutable = NSMutableAttributedString(attributedString: attributedString) + let string = mutable.string + if string.hasSuffix("\r\n"), mutable.length >= 2 { + mutable.deleteCharacters(in: NSRange(location: mutable.length - 2, length: 2)) + } else if string.hasSuffix("\n") { + mutable.deleteCharacters(in: NSRange(location: mutable.length - 1, length: 1)) + } + return mutable +} + +private enum MarkdownIntentKind { + case table([TableHorizontalAlignment]) + case tableHeaderRow + case tableRow + case tableCell(Int) + case paragraph + case header(Int) + case codeBlock(String?) + case thematicBreak + case blockQuote + case unorderedList + case orderedList + case listItem(Int) + case unknown + + @available(iOS 15.0, *) + init(component: PresentationIntent.IntentType) { + switch component.kind { + case let .table(columns): + self = .table(columns.map(markdownTableColumnAlignment)) + case .tableHeaderRow: + self = .tableHeaderRow + case .tableRow(_): + self = .tableRow + case let .tableCell(column): + self = .tableCell(column) + case .paragraph: + self = .paragraph + case let .header(level): + self = .header(level) + case let .codeBlock(languageHint): + self = .codeBlock(languageHint) + case .thematicBreak: + self = .thematicBreak + case .blockQuote: + self = .blockQuote + case .unorderedList: + self = .unorderedList + case .orderedList: + self = .orderedList + case let .listItem(ordinal): + self = .listItem(ordinal) + default: + self = .unknown + } + } +} + +@available(iOS 15.0, *) +private func markdownTableColumnAlignment(_ column: PresentationIntent.TableColumn) -> TableHorizontalAlignment { + switch column.alignment { + case .left: + return .left + case .center: + return .center + case .right: + return .right + @unknown default: + return .left + } +} + +private final class MarkdownIntentNode { + let identity: Int + let kind: MarkdownIntentKind + + private(set) var children: [MarkdownIntentNode] = [] + private var childIdentities: Set = [] + private(set) var attributedText = NSMutableAttributedString(string: "") + + @available(iOS 15.0, *) + init(component: PresentationIntent.IntentType) { + self.identity = component.identity + self.kind = MarkdownIntentKind(component: component) + } + + func append(child: MarkdownIntentNode) { + if self.childIdentities.insert(child.identity).inserted { + self.children.append(child) + } + } + + func append(text: NSAttributedString) { + self.attributedText.append(text) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserPdfContent.swift b/submodules/BrowserUI/Sources/BrowserPdfContent.swift index 4a77353538..0f723e51ef 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 @@ -83,7 +82,7 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF var title = "file" var url = "" - if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.media.resource) { + if let path = self.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.media.resource.id)) { var updatedPath = path if let fileName = file.media.fileName { let tempFile = TempBox.shared.file(path: path, fileName: fileName) @@ -365,7 +364,7 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF let pageIndicatorSize = self.pageIndicator.update( transition: .immediate, component: AnyComponent( - Text(text: "\(self.pageNumber?.0 ?? 1) of \(self.pageNumber?.1 ?? 1)", font: Font.with(size: 15.0, weight: .regular, traits: .monospacedNumbers), color: self.presentationData.theme.list.itemPrimaryTextColor) + Text(text: self.presentationData.strings.Items_NOfM("\(self.pageNumber?.0 ?? 1)", "\(self.pageNumber?.1 ?? 1)").string, font: Font.with(size: 15.0, weight: .regular, traits: .monospacedNumbers), color: self.presentationData.theme.list.itemPrimaryTextColor) ), environment: {}, containerSize: size @@ -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/BrowserReadability.swift b/submodules/BrowserUI/Sources/BrowserReadability.swift index 4f734d98e4..e021ad2d01 100644 --- a/submodules/BrowserUI/Sources/BrowserReadability.swift +++ b/submodules/BrowserUI/Sources/BrowserReadability.swift @@ -343,6 +343,8 @@ private func trimStart(_ input: RichText) -> RichText { } case .image: break + case .formula: + break } return text } @@ -385,6 +387,8 @@ private func trimEnd(_ input: RichText) -> RichText { } case .image: break + case .formula: + break } return text } @@ -428,6 +432,8 @@ private func trim(_ input: RichText) -> RichText { } case .image: break + case .formula: + break } return text } @@ -470,6 +476,8 @@ private func addNewLine(_ input: RichText) -> RichText { } case .image: break + case let .formula(latex): + text = .concat([.formula(latex: latex), .plain("\n")]) } return text } @@ -747,10 +755,16 @@ private func parsePageBlocks(_ input: [Any], _ url: String, _ media: inout [Medi result.append(.paragraph(trim(parseRichText(item, &media)))) case "h1", "h2": result.append(.header(trim(parseRichText(item, &media)))) - case "h3", "h4", "h5", "h6": - result.append(.subheader(trim(parseRichText(item, &media)))) + case "h3": + result.append(.heading(text: trim(parseRichText(item, &media)), level: 3)) + case "h4": + result.append(.heading(text: trim(parseRichText(item, &media)), level: 4)) + case "h5": + result.append(.heading(text: trim(parseRichText(item, &media)), level: 5)) + case "h6": + result.append(.heading(text: trim(parseRichText(item, &media)), level: 6)) case "pre": - result.append(.preformatted(.fixed(trim(parseRichText(item, &media))))) + result.append(.preformatted(text: .fixed(trim(parseRichText(item, &media))), language: nil)) case "blockquote": result.append(.blockQuote(text: .italic(trim(parseRichText(item, &media))), caption: .empty)) case "img": diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index 022acc615f..c93cb4d9f7 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 @@ -30,6 +29,7 @@ private final class BrowserScreenComponent: CombinedComponent { let context: AccountContext let contentState: BrowserContentState? let presentationState: BrowserPresentationState + let toolbarMode: BrowserToolbarMode let canShare: Bool let performAction: ActionSlot let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void @@ -39,6 +39,7 @@ private final class BrowserScreenComponent: CombinedComponent { context: AccountContext, contentState: BrowserContentState?, presentationState: BrowserPresentationState, + toolbarMode: BrowserToolbarMode, canShare: Bool, performAction: ActionSlot, performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void, @@ -47,6 +48,7 @@ private final class BrowserScreenComponent: CombinedComponent { self.context = context self.contentState = contentState self.presentationState = presentationState + self.toolbarMode = toolbarMode self.canShare = canShare self.performAction = performAction self.performHoldAction = performHoldAction @@ -63,6 +65,9 @@ private final class BrowserScreenComponent: CombinedComponent { if lhs.presentationState != rhs.presentationState { return false } + if lhs.toolbarMode != rhs.toolbarMode { + return false + } if lhs.canShare != rhs.canShare { return false } @@ -113,8 +118,7 @@ private final class BrowserScreenComponent: CombinedComponent { navigationLeftItems = [] navigationRightItems = [] } else { - let contentType = context.component.contentState?.contentType ?? .instantPage - switch contentType { + switch context.component.toolbarMode { case .webPage: navigationContent = AnyComponentWithIdentity( id: "addressBar", @@ -132,7 +136,7 @@ private final class BrowserScreenComponent: CombinedComponent { ) ) ) - case .instantPage, .document: + case .instantPage, .document, .markdown: let title = context.component.contentState?.title ?? "" navigationContent = AnyComponentWithIdentity( id: "titleBar_\(title)", @@ -191,47 +195,46 @@ private final class BrowserScreenComponent: CombinedComponent { // ) // ) // #endif - - let canGoBack = context.component.contentState?.canGoBack ?? false - let canGoForward = context.component.contentState?.canGoForward ?? false - - navigationLeftItems.append( - AnyComponentWithIdentity( - id: "back", - component: AnyComponent( - Button( - content: AnyComponent( - BundleIconComponent( - name: "Instant View/Back", - tintColor: environment.theme.chat.inputPanel.panelControlColor.withAlphaComponent(canGoBack ? 1.0 : 0.4) - ) - ), - action: { - performAction.invoke(.navigateBack) - } - ).minSize(CGSize(width: 44.0, height: 44.0)) + if context.component.toolbarMode != .markdown { + let canGoBack = context.component.contentState?.canGoBack ?? false + let canGoForward = context.component.contentState?.canGoForward ?? false + navigationLeftItems.append( + AnyComponentWithIdentity( + id: "back", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Back", + tintColor: environment.theme.chat.inputPanel.panelControlColor.withAlphaComponent(canGoBack ? 1.0 : 0.4) + ) + ), + action: { + performAction.invoke(.navigateBack) + } + ).minSize(CGSize(width: 44.0, height: 44.0)) + ) ) ) - ) - - navigationLeftItems.append( - AnyComponentWithIdentity( - id: "forward", - component: AnyComponent( - Button( - content: AnyComponent( - BundleIconComponent( - name: "Instant View/Forward", - tintColor: environment.theme.chat.inputPanel.panelControlColor.withAlphaComponent(canGoForward ? 1.0 : 0.4) - ) - ), - action: { - performAction.invoke(.navigateForward) - } - ).minSize(CGSize(width: 44.0, height: 44.0)) + navigationLeftItems.append( + AnyComponentWithIdentity( + id: "forward", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Forward", + tintColor: environment.theme.chat.inputPanel.panelControlColor.withAlphaComponent(canGoForward ? 1.0 : 0.4) + ) + ), + action: { + performAction.invoke(.navigateForward) + } + ).minSize(CGSize(width: 44.0, height: 44.0)) + ) ) ) - ) + } } navigationRightItems = [ @@ -259,25 +262,27 @@ private final class BrowserScreenComponent: CombinedComponent { ] if isTablet { - navigationRightItems.insert( - AnyComponentWithIdentity( - id: "bookmarks", - component: AnyComponent( - Button( - content: AnyComponent( - BundleIconComponent( - name: "Instant View/Bookmark", - tintColor: environment.theme.chat.inputPanel.panelControlColor - ) - ), - action: { - performAction.invoke(.openBookmarks) - } - ).minSize(CGSize(width: 44.0, height: 44.0)) - ) - ), - at: 0 - ) + if context.component.toolbarMode != .markdown { + navigationRightItems.insert( + AnyComponentWithIdentity( + id: "bookmarks", + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/Bookmark", + tintColor: environment.theme.chat.inputPanel.panelControlColor + ) + ), + action: { + performAction.invoke(.openBookmarks) + } + ).minSize(CGSize(width: 44.0, height: 44.0)) + ) + ), + at: 0 + ) + } if context.component.canShare { navigationRightItems.insert( AnyComponentWithIdentity( @@ -370,7 +375,7 @@ private final class BrowserScreenComponent: CombinedComponent { canGoForward: context.component.contentState?.canGoForward ?? false, canOpenIn: canOpenIn, canShare: context.component.canShare, - isDocument: context.component.contentState?.contentType == .document, + mode: context.component.toolbarMode, performAction: performAction, performHoldAction: performHoldAction ) @@ -518,7 +523,45 @@ public class BrowserScreen: ViewController, MinimizableController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? private var validLayout: (ContainerViewLayout, CGFloat)? - + + private var isMarkdownDocument: Bool { + guard let controller = self.controller else { + return false + } + if case .markdownDocument = controller.subject { + return true + } else { + return false + } + } + private var isMarkdownTopLevelContent: Bool { + return self.isMarkdownDocument && self.content.count <= 1 + } + + private var isMarkdownInstantPageContent: Bool { + return self.isMarkdownTopLevelContent && self.content.last is BrowserInstantPageContent + } + + private var toolbarMode: BrowserToolbarMode { + if self.isMarkdownInstantPageContent { + return .markdown + } + switch self.contentState?.contentType { + case .document: + return .document + case .webPage: + return .webPage + case .instantPage: + return .instantPage + case .none: + if self.content.last is BrowserDocumentContent || self.content.last is BrowserPdfContent { + return .document + } else { + return .instantPage + } + } + } + init(controller: BrowserScreen) { self.context = controller.context self.controller = controller @@ -536,7 +579,7 @@ public class BrowserScreen: ViewController, MinimizableController { super.init() self.pushContent(controller.subject, transition: .immediate) - if let content = self.content.last { + if let content = self.content.last, !self.isMarkdownDocument { content.addToRecentlyVisited() } @@ -563,7 +606,44 @@ public class BrowserScreen: ViewController, MinimizableController { let presentationData = self.presentationData let subject: ShareControllerSubject var isDocument = false - if let content = self.content.last { + if let controller = self.controller { + switch controller.subject { + case let .document(file, _), let .pdfDocument(file, _): + subject = .media(file.abstract, nil) + isDocument = true + case let .markdownDocument(file, _): + if self.isMarkdownTopLevelContent { + subject = .media(file.abstract, nil) + isDocument = true + } else if let content = self.content.last { + if let documentContent = content as? BrowserDocumentContent { + subject = .media(documentContent.file.abstract, nil) + isDocument = true + } else if let documentContent = content as? BrowserPdfContent { + subject = .media(documentContent.file.abstract, nil) + isDocument = true + } else { + subject = .url(url) + } + } else { + subject = .url(url) + } + default: + if let content = self.content.last { + if let documentContent = content as? BrowserDocumentContent { + subject = .media(documentContent.file.abstract, nil) + isDocument = true + } else if let documentContent = content as? BrowserPdfContent { + subject = .media(documentContent.file.abstract, nil) + isDocument = true + } else { + subject = .url(url) + } + } else { + subject = .url(url) + } + } + } else if let content = self.content.last { if let documentContent = content as? BrowserDocumentContent { subject = .media(documentContent.file.abstract, nil) isDocument = true @@ -576,8 +656,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 +671,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 +696,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 +714,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() @@ -644,7 +722,10 @@ public class BrowserScreen: ViewController, MinimizableController { var processed = false if let controller = self.controller { switch controller.subject { - case let .document(file, canShare), let .pdfDocument(file, canShare): + case let .document(file, canShare), let .pdfDocument(file, canShare), let .markdownDocument(file, canShare): + if case .markdownDocument = controller.subject, !self.isMarkdownTopLevelContent { + break + } processed = true controller.openDocument(file.media, canShare) default: @@ -858,6 +939,20 @@ public class BrowserScreen: ViewController, MinimizableController { browserContent = BrowserDocumentContent(context: self.context, presentationData: self.presentationData, file: file) case let .pdfDocument(file, _): browserContent = BrowserPdfContent(context: self.context, presentationData: self.presentationData, file: file) + case let .markdownDocument(file, _): + if let (webPage, fileURL) = markdownWebpage(context: self.context, file: file) { + browserContent = BrowserInstantPageContent( + context: self.context, + presentationData: self.presentationData, + webPage: webPage, + anchor: nil, + url: fileURL.absoluteString, + sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate), + preloadedResouces: nil + ) + } else { + browserContent = BrowserDocumentContent(context: self.context, presentationData: self.presentationData, file: file) + } } browserContent.pushContent = { [weak self] content, additionalContent in guard let self else { @@ -928,6 +1023,10 @@ public class BrowserScreen: ViewController, MinimizableController { } func popContent(transition: ComponentTransition) { + guard self.content.count > 1 else { + return + } + self.content.removeLast() self.requestLayout(transition: transition) @@ -1151,10 +1250,11 @@ public class BrowserScreen: ViewController, MinimizableController { } let canOpenIn = !(self.contentState?.url.hasPrefix("tonsite") ?? false) + let toolbarMode = self.toolbarMode var canShare = true if let controller = self.controller { switch controller.subject { - case let .document(_, canShareValue), let .pdfDocument(_, canShareValue): + case let .document(_, canShareValue), let .pdfDocument(_, canShareValue), let .markdownDocument(_, canShareValue): canShare = canShareValue default: break @@ -1205,7 +1305,7 @@ public class BrowserScreen: ViewController, MinimizableController { }))) } - if [.webPage, .instantPage].contains(contentState.contentType) { + if toolbarMode != .markdown && [.webPage, .instantPage].contains(contentState.contentType) { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.WebBrowser_AddBookmark, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in performAction.invoke(.addBookmark) action(.default) @@ -1387,7 +1487,7 @@ public class BrowserScreen: ViewController, MinimizableController { var canShare = true if let controller = self.controller { switch controller.subject { - case let .document(_, canShareValue), let .pdfDocument(_, canShareValue): + case let .document(_, canShareValue), let .pdfDocument(_, canShareValue), let .markdownDocument(_, canShareValue): canShare = canShareValue default: break @@ -1401,6 +1501,7 @@ public class BrowserScreen: ViewController, MinimizableController { context: self.context, contentState: self.contentState, presentationState: self.presentationState, + toolbarMode: self.toolbarMode, canShare: canShare, performAction: self.performAction, performHoldAction: { [weak self] view, gesture, action in @@ -1480,10 +1581,11 @@ public class BrowserScreen: ViewController, MinimizableController { case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation, preloadedResources: [Any]?) case document(file: FileMediaReference, canShare: Bool) case pdfDocument(file: FileMediaReference, canShare: Bool) + case markdownDocument(file: FileMediaReference, canShare: Bool) public var fileId: MediaId? { switch self { - case let .document(file, _), let .pdfDocument(file, _): + case let .document(file, _), let .pdfDocument(file, _), let .markdownDocument(file, _): return file.media.fileId default: return nil @@ -1509,7 +1611,10 @@ public class BrowserScreen: ViewController, MinimizableController { "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.spreadsheetml.template", - "application/vnd.openxmlformats-officedocument.presentationml.presentation" + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "text/markdown", + "text/x-markdown", + "text/x-web-markdown" ] public static let supportedDocumentExtensions: [String] = [ @@ -1520,7 +1625,8 @@ public class BrowserScreen: ViewController, MinimizableController { "docx", "xls", "xlsx", - "pptx" + "pptx", + "md" ] public init(context: AccountContext, subject: Subject, preferredConfiguration: WKWebViewConfiguration? = nil, openPreviousOnClose: Bool = false) { diff --git a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift index 5406d1d459..01546e4cd9 100644 --- a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift @@ -9,6 +9,13 @@ import ContextReferenceButtonComponent import GlassBackgroundComponent import EdgeEffect +enum BrowserToolbarMode: Equatable { + case webPage + case instantPage + case document + case markdown +} + final class BrowserToolbarComponent: CombinedComponent { let theme: PresentationTheme let bottomInset: CGFloat @@ -131,7 +138,7 @@ final class NavigationToolbarContentComponent: CombinedComponent { let canGoForward: Bool let canOpenIn: Bool let canShare: Bool - let isDocument: Bool + let mode: BrowserToolbarMode let performAction: ActionSlot let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void @@ -141,7 +148,7 @@ final class NavigationToolbarContentComponent: CombinedComponent { canGoForward: Bool, canOpenIn: Bool, canShare: Bool, - isDocument: Bool, + mode: BrowserToolbarMode, performAction: ActionSlot, performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void ) { @@ -150,7 +157,7 @@ final class NavigationToolbarContentComponent: CombinedComponent { self.canGoForward = canGoForward self.canOpenIn = canOpenIn self.canShare = canShare - self.isDocument = isDocument + self.mode = mode self.performAction = performAction self.performHoldAction = performHoldAction } @@ -171,7 +178,7 @@ final class NavigationToolbarContentComponent: CombinedComponent { if lhs.canShare != rhs.canShare { return false } - if lhs.isDocument != rhs.isDocument { + if lhs.mode != rhs.mode { return false } return true @@ -217,7 +224,8 @@ final class NavigationToolbarContentComponent: CombinedComponent { transition: .easeInOut(duration: 0.2) ) - if context.component.isDocument { + switch context.component.mode { + case .document: var originX: CGFloat = sideInset let search = search.update( @@ -270,7 +278,40 @@ final class NavigationToolbarContentComponent: CombinedComponent { .position(CGPoint(x: originX, y: availableSize.height / 2.0)) ) size.width = originX + sideInset - } else { + case .markdown: + var originX: CGFloat = sideInset + + if !context.component.canShare { + context.add(share + .position(CGPoint(x: availableSize.width / 2.0, y: 10000.0)) + ) + } else { + context.add(share + .position(CGPoint(x: originX, y: availableSize.height / 2.0)) + ) + originX += spacing + } + + let quickLook = quickLook.update( + component: Button( + content: AnyComponent( + BundleIconComponent( + name: "Instant View/OpenDocument", + tintColor: textColor + ) + ), + action: { + performAction.invoke(.openIn) + } + ).minSize(buttonSize), + availableSize: buttonSize, + transition: .easeInOut(duration: 0.2) + ) + context.add(quickLook + .position(CGPoint(x: originX, y: availableSize.height / 2.0)) + ) + size.width = originX + sideInset + case .webPage, .instantPage: var originX: CGFloat = sideInset let canGoBack = context.component.canGoBack diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index baf4a556a0..835a5a9144 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 @@ -423,16 +423,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU case "oauth_request": let url = json?["url"] as? String if let url { - let securityOrigin = message.frameInfo.securityOrigin - var origin = "" - origin.append(securityOrigin.protocol) - origin.append("://") - origin.append(securityOrigin.host) - if securityOrigin.port != 0 { - origin.append(":") - origin.append("\(securityOrigin.port)") - } - + let origin = message.frameInfo.securityOriginString + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let subject: MessageActionUrlSubject = .url(url: url, inAppOrigin: origin) let _ = (self.context.engine.messages.requestMessageActionUrlAuth(subject: subject) @@ -1580,10 +1572,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) } @@ -1682,7 +1673,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU if let favicon, let imageData = favicon.pngData() { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: imageData) + self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: imageData) image = TelegramMediaImage( imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: [ @@ -2036,3 +2027,18 @@ private func findScrollView(view: UIView?) -> UIScrollView? { return nil } } + +private extension WKFrameInfo { + var securityOriginString: String { + let securityOrigin = self.securityOrigin + var origin = "" + origin.append(securityOrigin.protocol) + origin.append("://") + origin.append(securityOrigin.host) + if securityOrigin.port != 0 { + origin.append(":") + origin.append("\(securityOrigin.port)") + } + return origin + } +} diff --git a/submodules/BrowserUI/Sources/Utils.swift b/submodules/BrowserUI/Sources/Utils.swift index 71489f05a3..fe17f25240 100644 --- a/submodules/BrowserUI/Sources/Utils.swift +++ b/submodules/BrowserUI/Sources/Utils.swift @@ -23,8 +23,6 @@ func fetchFavicon(context: AccountContext, url: String, size: CGSize) -> Signal< if let data { if let image = UIImage(data: data) { return image - } else if url.lowercased().contains(".svg"), let preparedData = prepareSvgImage(data, false), let image = renderPreparedImage(preparedData, size, .clear, UIScreenScale, false) { - return image } return nil } else { diff --git a/submodules/CalendarMessageScreen/BUILD b/submodules/CalendarMessageScreen/BUILD index db198e2a0a..f91ea4c217 100644 --- a/submodules/CalendarMessageScreen/BUILD +++ b/submodules/CalendarMessageScreen/BUILD @@ -14,7 +14,6 @@ swift_library( "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/ComponentFlow:ComponentFlow", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 194c3c97da..991c81f67a 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -372,7 +372,7 @@ public final class CallListController: TelegramBaseController { TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) |> deliverOnMainQueue).startStandalone(next: { peer in - if let strongSelf = self, let peer = peer, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .calls(messages: messages.map({ $0._asMessage() })), avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let strongSelf = self, let peer = peer, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .calls(messages: messages.map({ $0._asMessage() })), avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { (strongSelf.navigationController as? NavigationController)?.pushViewController(controller) } }) diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 6790ba59ec..e2353ce62b 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -146,7 +146,7 @@ private final class CameraContext { transform = CGAffineTransformTranslate(transform, 0.0, -size.height) ciImage = ciImage.transformed(by: transform) } - ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: Camera.isDualCameraSupported(forRoundVideo: true) ? 100.0 : 40.0).cropped(to: CGRect(origin: .zero, size: size)) + ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: Camera.isDualCameraSupported(forRoundVideo: true) ? 60.0 : 40.0).cropped(to: CGRect(origin: .zero, size: size)) if let cgImage = self.ciContext.createCGImage(ciImage, from: ciImage.extent) { let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) if front { @@ -189,6 +189,7 @@ private final class CameraContext { deinit { Logger.shared.log("CameraContext", "deinit") + NotificationCenter.default.removeObserver(self) } private var isSessionRunning = false @@ -202,7 +203,7 @@ private final class CameraContext { } func stopCapture(invalidate: Bool = false) { - Logger.shared.log("CameraContext", "startCapture(invalidate: \(invalidate))") + Logger.shared.log("CameraContext", "stopCapture(invalidate: \(invalidate))") if invalidate { self.mainDeviceContext?.device.resetZoom() @@ -212,6 +213,7 @@ private final class CameraContext { } self.session.session.stopRunning() + self.isSessionRunning = false } func focus(at point: CGPoint, autoFocus: Bool) { @@ -228,7 +230,7 @@ private final class CameraContext { } func setFps(_ fps: Float64) { - self.mainDeviceContext?.device.fps = fps + self.mainDeviceContext?.device.setFps(fps) } private var modeChange: Camera.ModeChange = .none { @@ -276,7 +278,6 @@ private final class CameraContext { self._positionPromise.set(targetPosition) self.modeChange = .position - let preferWide = self.initialConfiguration.preferWide || isRoundVideo let preferLowerFramerate = self.initialConfiguration.preferLowerFramerate || isRoundVideo @@ -565,6 +566,11 @@ private final class CameraContext { return .finished(mainImage, additionalImage, CACurrentMediaTime()) } } else { + if case .failed = main { + return .failed + } else if case .failed = additional { + return .failed + } return .began } } |> distinctUntilChanged @@ -583,6 +589,10 @@ private final class CameraContext { mainDeviceContext.device.setTorchMode(self._flashMode) } + let timestamp = CACurrentMediaTime() + 2.0 + self.lastSnapshotTimestamp = timestamp + self.lastAdditionalSnapshotTimestamp = timestamp + let orientation = self.simplePreviewView?.videoPreviewLayer.connection?.videoOrientation ?? .portrait if self.initialConfiguration.isRoundVideo { return mainDeviceContext.output.startRecording(mode: .roundVideo, orientation: DeviceModel.current.isIpad ? orientation : .portrait, additionalOutput: self.additionalDeviceContext?.output) @@ -788,6 +798,11 @@ public final class Camera { secondaryPreviewView.setSession(session.session, autoConnect: false) } + if #available(iOS 14.5, *), configuration.isRoundVideo { + AVCaptureDevice.centerStageControlMode = .app + AVCaptureDevice.isCenterStageEnabled = false + } + self.queue.async { let context = CameraContext(queue: self.queue, session: session, configuration: configuration, metrics: self.metrics, previewView: previewView, secondaryPreviewView: secondaryPreviewView) self.contextRef = Unmanaged.passRetained(context) @@ -801,6 +816,10 @@ public final class Camera { self.queue.async { contextRef?.release() } + + if #available(iOS 14.5, *) { + AVCaptureDevice.centerStageControlMode = .user + } } public func startCapture() { @@ -1170,6 +1189,7 @@ public struct CameraRecordingData { } public enum CameraRecordingError { + case videoRecorderInitializationError case audioInitializationError } diff --git a/submodules/Camera/Sources/CameraDevice.swift b/submodules/Camera/Sources/CameraDevice.swift index 6778c2dff5..c1d3eefbc6 100644 --- a/submodules/Camera/Sources/CameraDevice.swift +++ b/submodules/Camera/Sources/CameraDevice.swift @@ -141,10 +141,12 @@ final class CameraDevice { Logger.shared.log("Camera", "No format selected") } + #if DEBUG Logger.shared.log("Camera", "Available formats:") for format in device.formats { Logger.shared.log("Camera", format.description) } + #endif if let targetFPS = device.actualFPS(maxFramerate) { device.activeVideoMinFrameDuration = targetFPS.duration @@ -154,7 +156,7 @@ final class CameraDevice { if device.isLowLightBoostSupported { device.automaticallyEnablesLowLightBoostWhenAvailable = true } - + if device.isExposureModeSupported(.continuousAutoExposure) { device.exposureMode = .continuousAutoExposure } @@ -180,18 +182,16 @@ final class CameraDevice { self.setFocusPoint(CGPoint(x: 0.5, y: 0.5), focusMode: .continuousAutoFocus, exposureMode: .continuousAutoExposure, monitorSubjectAreaChange: false) } - var fps: Double = defaultFPS { - didSet { - guard let device = self.videoDevice, let targetFPS = device.actualFPS(Double(self.fps)) else { - return - } - - self.fps = targetFPS.fps - - self.transaction(device) { device in - device.activeVideoMinFrameDuration = targetFPS.duration - device.activeVideoMaxFrameDuration = targetFPS.duration - } + private(set) var fps: Double = defaultFPS + + func setFps(_ fps: Double) { + guard let device = self.videoDevice, let targetFPS = device.actualFPS(Double(fps)) else { + return + } + self.fps = targetFPS.fps + self.transaction(device) { device in + device.activeVideoMinFrameDuration = targetFPS.duration + device.activeVideoMaxFrameDuration = targetFPS.duration } } @@ -305,7 +305,8 @@ final class CameraDevice { return } self.transaction(device) { device in - device.videoZoomFactor = max(device.neutralZoomFactor, min(10.0, device.neutralZoomFactor + zoomLevel)) + let target = device.neutralZoomFactor + zoomLevel + device.videoZoomFactor = self.clampedZoomFactor(target, for: device) } } @@ -314,7 +315,8 @@ final class CameraDevice { return } self.transaction(device) { device in - device.videoZoomFactor = max(1.0, min(10.0, device.videoZoomFactor * zoomDelta)) + let target = device.videoZoomFactor * zoomDelta + device.videoZoomFactor = self.clampedZoomFactor(target, for: device) } } @@ -323,7 +325,8 @@ final class CameraDevice { return } self.transaction(device) { device in - device.ramp(toVideoZoomFactor: zoomLevel, withRate: Float(rate)) + let target = self.clampedZoomFactor(zoomLevel, for: device) + device.ramp(toVideoZoomFactor: target, withRate: Float(rate)) } } @@ -332,7 +335,14 @@ final class CameraDevice { return } self.transaction(device) { device in - device.videoZoomFactor = neutral ? device.neutralZoomFactor : device.minAvailableVideoZoomFactor + let target = neutral ? device.neutralZoomFactor : device.minAvailableVideoZoomFactor + device.videoZoomFactor = self.clampedZoomFactor(target, for: device) } } + + private func clampedZoomFactor(_ value: CGFloat, for device: AVCaptureDevice) -> CGFloat { + let minimum = max(1.0, device.minAvailableVideoZoomFactor) + let maximum = max(minimum, device.maxAvailableVideoZoomFactor) + return min(maximum, max(minimum, value)) + } } diff --git a/submodules/Camera/Sources/CameraInput.swift b/submodules/Camera/Sources/CameraInput.swift index 29adeca209..f9114e2db0 100644 --- a/submodules/Camera/Sources/CameraInput.swift +++ b/submodules/Camera/Sources/CameraInput.swift @@ -15,11 +15,14 @@ class CameraInput { } func invalidate(for session: CameraSession, switchAudio: Bool = true) { - for input in session.session.inputs { - if !switchAudio && input === self.audioInput { - continue - } - session.session.removeInput(input) + if let videoInput = self.videoInput, session.session.inputs.contains(where: { $0 === videoInput }) { + session.session.removeInput(videoInput) + } + self.videoInput = nil + + if switchAudio, let audioInput = self.audioInput, session.session.inputs.contains(where: { $0 === audioInput }) { + session.session.removeInput(audioInput) + self.audioInput = nil } } diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index a1f0a3fb1c..75dce41a14 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -93,6 +93,11 @@ public struct CameraCode: Equatable { } final class CameraOutput: NSObject { + private struct RoundVideoFormatDescriptionCacheEntry { + let sourceFormatDescription: CMFormatDescription + let outputFormatDescription: CMFormatDescription + } + let exclusive: Bool let ciContext: CIContext let colorSpace: CGColorSpace @@ -111,13 +116,14 @@ final class CameraOutput: NSObject { private var roundVideoFilter: CameraRoundLegacyVideoFilter? private let semaphore = DispatchSemaphore(value: 1) + private var roundVideoFormatDescriptionCache: [RoundVideoFormatDescriptionCacheEntry] = [] private let videoQueue = DispatchQueue(label: "", qos: .userInitiated) private let audioQueue = DispatchQueue(label: "") private let metadataQueue = DispatchQueue(label: "") - private var photoCaptureRequests: [Int64: PhotoCaptureContext] = [:] + private var photoCaptureRequests = Atomic<[Int64: PhotoCaptureContext]>(value: [:]) private var videoRecorder: VideoRecorder? private var captureOrientation: AVCaptureVideoOrientation = .portrait @@ -186,9 +192,9 @@ final class CameraOutput: NSObject { } if #available(iOS 13.0, *), session.hasMultiCam { - if let device = device.videoDevice, let ports = input.videoInput?.ports(for: AVMediaType.video, sourceDeviceType: device.deviceType, sourceDevicePosition: device.position) { + if let device = device.videoDevice, let ports = input.videoInput?.ports(for: AVMediaType.video, sourceDeviceType: device.deviceType, sourceDevicePosition: device.position), let firstPort = ports.first { if let previewView { - let previewConnection = AVCaptureConnection(inputPort: ports.first!, videoPreviewLayer: previewView.videoPreviewLayer) + let previewConnection = AVCaptureConnection(inputPort: firstPort, videoPreviewLayer: previewView.videoPreviewLayer) if session.session.canAddConnection(previewConnection) { session.session.addConnection(previewConnection) self.previewConnection = previewConnection @@ -268,8 +274,8 @@ final class CameraOutput: NSObject { return EmptyDisposable } subscriber.putNext(self.photoOutput.isFlashScene) - let observer = self.photoOutput.observe(\.isFlashScene, options: [.new], changeHandler: { device, _ in - subscriber.putNext(self.photoOutput.isFlashScene) + let observer = self.photoOutput.observe(\.isFlashScene, options: [.new], changeHandler: { output, _ in + subscriber.putNext(output.isFlashScene) }) return ActionDisposable { observer.invalidate() @@ -316,12 +322,20 @@ final class CameraOutput: NSObject { #else let uniqueId = settings.uniqueID let photoCapture = PhotoCaptureContext(ciContext: self.ciContext, settings: settings, orientation: orientation, mirror: mirror) - self.photoCaptureRequests[uniqueId] = photoCapture + let _ = self.photoCaptureRequests.modify { dict in + var dict = dict + dict[uniqueId] = photoCapture + return dict + } self.photoOutput.capturePhoto(with: settings, delegate: photoCapture) return photoCapture.signal |> afterDisposed { [weak self] in - self?.photoCaptureRequests.removeValue(forKey: uniqueId) + let _ = self?.photoCaptureRequests.modify { dict in + var dict = dict + dict.removeValue(forKey: uniqueId) + return dict + } } #endif } @@ -419,18 +433,21 @@ final class CameraOutput: NSObject { } } ) + guard let videoRecorder else { + return .fail(.videoRecorderInitializationError) + } - videoRecorder?.start() + videoRecorder.start() self.videoRecorder = videoRecorder if case .dualCamera = mode, let position { - videoRecorder?.markPositionChange(position: position, time: .zero) + videoRecorder.markPositionChange(position: position, time: .zero) } else if case .roundVideo = mode { additionalOutput?.masterOutput = self } return Signal { subscriber in - let timer = SwiftSignalKit.Timer(timeout: 0.033, repeat: true, completion: { [weak videoRecorder] in + let timer = SwiftSignalKit.Timer(timeout: 0.09, repeat: true, completion: { [weak videoRecorder] in let recordingData = CameraRecordingData(duration: videoRecorder?.duration ?? 0.0, filePath: outputFilePath) subscriber.putNext(recordingData) }, queue: Queue.mainQueue()) @@ -463,6 +480,43 @@ final class CameraOutput: NSObject { private var lastSampleTimestamp: CMTime? + private func roundVideoFormatDescription(for sourceFormatDescription: CMFormatDescription) -> CMFormatDescription? { + if let entry = self.roundVideoFormatDescriptionCache.first(where: { CFEqual($0.sourceFormatDescription, sourceFormatDescription) }) { + return entry.outputFormatDescription + } + + guard let extensions = CMFormatDescriptionGetExtensions(sourceFormatDescription) as? [String: Any] else { + return nil + } + + let mediaSubType = CMFormatDescriptionGetMediaSubType(sourceFormatDescription) + var updatedExtensions = extensions + updatedExtensions["CVBytesPerRow"] = videoMessageDimensions.width * 4 + + var outputFormatDescription: CMFormatDescription? + let status = CMVideoFormatDescriptionCreate( + allocator: nil, + codecType: mediaSubType, + width: videoMessageDimensions.width, + height: videoMessageDimensions.height, + extensions: updatedExtensions as CFDictionary, + formatDescriptionOut: &outputFormatDescription + ) + guard status == noErr, let outputFormatDescription else { + return nil + } + + self.roundVideoFormatDescriptionCache.append(RoundVideoFormatDescriptionCacheEntry( + sourceFormatDescription: sourceFormatDescription, + outputFormatDescription: outputFormatDescription + )) + if self.roundVideoFormatDescriptionCache.count > 4 { + self.roundVideoFormatDescriptionCache.removeFirst(self.roundVideoFormatDescriptionCache.count - 4) + } + + return outputFormatDescription + } + private var needsCrossfadeTransition = false private var crossfadeTransitionStart: Double = 0.0 @@ -564,17 +618,11 @@ final class CameraOutput: NSObject { return nil } self.semaphore.wait() - - let mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription) - let extensions = CMFormatDescriptionGetExtensions(formatDescription) as! [String: Any] - - var updatedExtensions = extensions - updatedExtensions["CVBytesPerRow"] = videoMessageDimensions.width * 4 - - var newFormatDescription: CMFormatDescription? - var status = CMVideoFormatDescriptionCreate(allocator: nil, codecType: mediaSubType, width: videoMessageDimensions.width, height: videoMessageDimensions.height, extensions: updatedExtensions as CFDictionary, formatDescriptionOut: &newFormatDescription) - guard status == noErr, let newFormatDescription else { + defer { self.semaphore.signal() + } + + guard let newFormatDescription = self.roundVideoFormatDescription(for: formatDescription) else { return nil } @@ -585,12 +633,11 @@ final class CameraOutput: NSObject { filter = CameraRoundLegacyVideoFilter(ciContext: self.ciContext, colorSpace: self.colorSpace, simple: self.exclusive) self.roundVideoFilter = filter } - if !filter.isPrepared { + if !filter.isPrepared || filter.inputFormatDescription.map({ !CFEqual($0, newFormatDescription) }) ?? true { filter.prepare(with: newFormatDescription, outputRetainedBufferCountHint: 4) } guard let newPixelBuffer = filter.render(pixelBuffer: videoPixelBuffer, additional: additional, captureOrientation: self.captureOrientation, transitionFactor: transitionFactor) else { - self.semaphore.signal() return nil } @@ -603,7 +650,7 @@ final class CameraOutput: NSObject { } var newSampleBuffer: CMSampleBuffer? - status = CMSampleBufferCreateForImageBuffer( + let status = CMSampleBufferCreateForImageBuffer( allocator: kCFAllocatorDefault, imageBuffer: newPixelBuffer, dataReady: true, @@ -615,10 +662,8 @@ final class CameraOutput: NSObject { ) if status == noErr, let newSampleBuffer { - self.semaphore.signal() return newSampleBuffer } - self.semaphore.signal() return nil } @@ -640,23 +685,23 @@ extension CameraOutput: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureA guard CMSampleBufferDataIsReady(sampleBuffer) else { return } - - if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { - self.processSampleBuffer?(sampleBuffer, videoPixelBuffer, connection) - } else if sampleBuffer.type == kCMMediaType_Audio { - self.processAudioBuffer?(sampleBuffer) - } - + if let masterOutput = self.masterOutput { masterOutput.processVideoRecording(sampleBuffer, fromAdditionalOutput: true) } else { self.processVideoRecording(sampleBuffer, fromAdditionalOutput: false) } + + if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { + self.processSampleBuffer?(sampleBuffer, videoPixelBuffer, connection) + } else if sampleBuffer.type == kCMMediaType_Audio { + self.processAudioBuffer?(sampleBuffer) + } } func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { if #available(iOS 13.0, *) { - Logger.shared.log("VideoRecorder", "Dropped sample buffer \(sampleBuffer.attachments)") + Logger.shared.log("Camera", "Dropped sample buffer \(sampleBuffer.attachments)") } } } diff --git a/submodules/Camera/Sources/PhotoCaptureContext.swift b/submodules/Camera/Sources/PhotoCaptureContext.swift index c61fe73602..3e34fc5904 100644 --- a/submodules/Camera/Sources/PhotoCaptureContext.swift +++ b/submodules/Camera/Sources/PhotoCaptureContext.swift @@ -56,6 +56,7 @@ final class PhotoCaptureContext: NSObject, AVCapturePhotoCaptureDelegate { } else { guard let photoPixelBuffer = photo.pixelBuffer else { print("Error occurred while capturing photo: Missing pixel buffer (\(String(describing: error)))") + self.pipe.putNext(.failed) return } diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index a2d3a7cb83..e3772d446f 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -204,9 +204,7 @@ private final class VideoRecorderImpl { let maxDate = Date(timeIntervalSinceNow: 0.05) RunLoop.current.run(until: maxDate) } - } - if let videoInput = self.videoInput { let time = CACurrentMediaTime() // if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime { // print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)") @@ -234,7 +232,9 @@ private final class VideoRecorderImpl { } else if self.orientation == .portraitUpsideDown { orientation = .left } - self.transitionImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: orientation) + Queue.mainQueue().async { + self.transitionImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: orientation) + } } else { self.savedTransitionImage = false } @@ -366,7 +366,7 @@ private final class VideoRecorderImpl { private func maybeFinish() { dispatchPrecondition(condition: .onQueue(self.queue)) - guard self.hasAllVideoBuffers && self.hasAllVideoBuffers && !self.stopped else { + guard self.hasAllVideoBuffers && (!self.configuration.hasAudio || self.hasAllAudioBuffers) && !self.stopped else { return } let _ = self._stopped.modify { _ in return true } @@ -377,21 +377,21 @@ private final class VideoRecorderImpl { dispatchPrecondition(condition: .onQueue(self.queue)) let completion = self.completion if self.recordingStopSampleTime == .invalid { - DispatchQueue.main.async { + Queue.mainQueue().async { completion(false, nil, nil) } return } if let _ = self.error.with({ $0 }) { - DispatchQueue.main.async { + Queue.mainQueue().async { completion(false, nil, nil) } return } if !self.tryAppendingPendingAudioBuffers() { - DispatchQueue.main.async { + Queue.mainQueue().async { completion(false, nil, nil) } return @@ -400,21 +400,21 @@ private final class VideoRecorderImpl { if self.assetWriter.status == .writing { self.assetWriter.finishWriting { if let _ = self.assetWriter.error { - DispatchQueue.main.async { + Queue.mainQueue().async { completion(false, nil, nil) } } else { - DispatchQueue.main.async { + Queue.mainQueue().async { completion(true, self.transitionImage, self.positionChangeTimestamps) } } } } else if let _ = self.assetWriter.error { - DispatchQueue.main.async { + Queue.mainQueue().async { completion(false, nil, nil) } } else { - DispatchQueue.main.async { + Queue.mainQueue().async { completion(false, nil, nil) } } diff --git a/submodules/ChatImportUI/BUILD b/submodules/ChatImportUI/BUILD index 2ee541d7da..eca35fbd14 100644 --- a/submodules/ChatImportUI/BUILD +++ b/submodules/ChatImportUI/BUILD @@ -14,7 +14,6 @@ swift_library( "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", "//submodules/TelegramPresentationData:TelegramPresentationData", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AppBundle:AppBundle", "//third-party/ZipArchive:ZipArchive", diff --git a/submodules/ChatInterfaceState/BUILD b/submodules/ChatInterfaceState/BUILD index 4d2c0bdd17..c53e3d1d26 100644 --- a/submodules/ChatInterfaceState/BUILD +++ b/submodules/ChatInterfaceState/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TextFormat:TextFormat", "//submodules/AccountContext:AccountContext", diff --git a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift index bd4ab384c5..a90930e9c5 100644 --- a/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift +++ b/submodules/ChatInterfaceState/Sources/ChatInterfaceState.swift @@ -1,6 +1,5 @@ import Foundation import UIKit -import Postbox import TelegramCore import TextFormat import AccountContext @@ -306,8 +305,8 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) - let resourceData = try container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: "r") - self.resource = LocalFileMediaResource(decoder: PostboxDecoder(buffer: MemoryBuffer(data: resourceData.data))) + let resourceData = try container.decode(EngineAdaptedPostboxDecoder.RawObjectData.self, forKey: "r") + self.resource = LocalFileMediaResource(decoder: EnginePostboxDecoder(buffer: EngineMemoryBuffer(data: resourceData.data))) self.fileSize = try container.decode(Int32.self, forKey: "s") @@ -333,7 +332,7 @@ public enum ChatInterfaceMediaDraftState: Codable, Equatable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) - try container.encode(PostboxEncoder().encodeObjectToRawData(self.resource), forKey: "r") + try container.encode(EnginePostboxEncoder().encodeObjectToRawData(self.resource), forKey: "r") try container.encode(self.fileSize, forKey: "s") try container.encode(self.duration, forKey: "dd") try container.encode(self.waveform.samples, forKey: "wd") @@ -499,11 +498,11 @@ public final class ChatInterfaceState: Codable, Equatable { } public struct PostSuggestionState: Codable, Equatable { - public var editingOriginalMessageId: MessageId? + public var editingOriginalMessageId: EngineMessage.Id? public var price: CurrencyAmount? public var timestamp: Int32? - public init(editingOriginalMessageId: MessageId?, price: CurrencyAmount?, timestamp: Int32?) { + public init(editingOriginalMessageId: EngineMessage.Id?, price: CurrencyAmount?, timestamp: Int32?) { self.editingOriginalMessageId = editingOriginalMessageId self.price = price self.timestamp = timestamp diff --git a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift index f11b5a043f..447193c426 100644 --- a/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift +++ b/submodules/ChatListSearchRecentPeersNode/Sources/ChatListSearchRecentPeersNode.swift @@ -79,8 +79,7 @@ private struct ChatListSearchRecentPeersEntry: Comparable, Identifiable { func item( accountPeerId: EnginePeer.Id, - postbox: Postbox, - network: Network, + stateManager: AccountStateManager, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, @@ -96,8 +95,7 @@ private struct ChatListSearchRecentPeersEntry: Comparable, Identifiable { strings: self.strings, mode: mode, accountPeerId: accountPeerId, - postbox: postbox, - network: network, + stateManager: stateManager, energyUsageSettings: energyUsageSettings, contentSettings: contentSettings, animationCache: animationCache, @@ -126,8 +124,7 @@ private struct ChatListSearchRecentNodeTransition { private func preparedRecentPeersTransition( accountPeerId: EnginePeer.Id, - postbox: Postbox, - network: Network, + stateManager: AccountStateManager, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, @@ -148,8 +145,7 @@ private func preparedRecentPeersTransition( let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item( accountPeerId: accountPeerId, - postbox: postbox, - network: network, + stateManager: stateManager, energyUsageSettings: energyUsageSettings, contentSettings: contentSettings, animationCache: animationCache, @@ -162,8 +158,7 @@ private func preparedRecentPeersTransition( ), directionHint: .Down) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item( accountPeerId: accountPeerId, - postbox: postbox, - network: network, + stateManager: stateManager, energyUsageSettings: energyUsageSettings, contentSettings: contentSettings, animationCache: animationCache, @@ -204,8 +199,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { public init( accountPeerId: EnginePeer.Id, - postbox: Postbox, - network: Network, + stateManager: AccountStateManager, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, @@ -238,7 +232,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { let peersDisposable = DisposableSet() - let recent: Signal<([EnginePeer], [EnginePeer.Id: (Int32, Bool)], [EnginePeer.Id : EnginePeer.Presence]), NoError> = _internal_recentPeers(accountPeerId: accountPeerId, postbox: postbox) + let recent: Signal<([EnginePeer], [EnginePeer.Id: (Int32, Bool)], [EnginePeer.Id : EnginePeer.Presence]), NoError> = _internal_recentPeers(accountPeerId: accountPeerId, postbox: stateManager.postbox) |> filter { value -> Bool in switch value { case .disabled: @@ -256,11 +250,11 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { peers.filter { !$0.isDeleted }.map { - postbox.peerView(id: $0.id) + stateManager.postbox.peerView(id: $0.id) } ) |> mapToSignal { peerViews -> Signal<([EnginePeer], [EnginePeer.Id: (Int32, Bool)], [EnginePeer.Id: EnginePeer.Presence]), NoError> in - return postbox.combinedView(keys: peerViews.map { item -> PostboxViewKey in + return stateManager.postbox.combinedView(keys: peerViews.map { item -> PostboxViewKey in let key = PostboxViewKey.unreadCounts(items: [UnreadMessageCountsItem.peer(id: item.peerId, handleThreads: true)]) return key }) @@ -324,8 +318,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { let transition = preparedRecentPeersTransition( accountPeerId: accountPeerId, - postbox: postbox, - network: network, + stateManager: stateManager, energyUsageSettings: energyUsageSettings, contentSettings: contentSettings, animationCache: animationCache, @@ -345,7 +338,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode { } })) if case .actionSheet = mode { - peersDisposable.add(_internal_managedUpdatedRecentPeers(accountPeerId: accountPeerId, postbox: postbox, network: network).startStrict()) + peersDisposable.add(_internal_managedUpdatedRecentPeers(accountPeerId: accountPeerId, postbox: stateManager.postbox, network: stateManager.network).startStrict()) } self.disposable.set(peersDisposable) } diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index ef9229872e..64b01eecac 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -331,7 +331,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } return filters }).startStandalone() - chatListController?.present(UndoOverlayController( presentationData: presentationData, content: .chatAddedToFolder(context: context, chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.rawAttributedString), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(context: context, chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title.rawAttributedString), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }) @@ -1023,7 +1023,7 @@ public func savedMessagesPeerMenuItems(context: AccountContext, threadId: Int64, } private func openCustomMute(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, baseController: ViewController) { - let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .mute, currentTime: nil, dismissByTapOutside: true, completion: { [weak baseController] value in + let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .mute, currentTime: nil, completion: { [weak baseController] value in let presentationData = context.sharedContext.currentPresentationData.with { $0 } if value <= 0 { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 0debd0179e..c9c94d3288 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -163,6 +163,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return false } } + + private var foldersCount: Int32 { + guard let tabContainerData = self.tabContainerData else { + return 0 + } + return Int32(tabContainerData.0.count(where: { entry in + if case .filter = entry { + return true + } else { + return false + } + })) + } private var hasDownloads: Bool = false private var activeDownloadsDisposable: Disposable? @@ -459,7 +472,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController var entry: FetchManagerEntrySummary var isRemoved: Bool = false var statusDisposable: Disposable? - var status: MediaResourceStatus? + var status: EngineMediaResource.FetchStatus? init(entry: FetchManagerEntrySummary) { self.entry = entry @@ -509,7 +522,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if context.statusDisposable == nil { - context.statusDisposable = (engine.account.postbox.mediaBox.resourceStatus(context.entry.resourceReference.resource) + context.statusDisposable = (engine.resources.status(resource: EngineMediaResource(context.entry.resourceReference.resource)) |> deliverOn(self.queue)).startStrict(next: { [weak self, weak context] status in guard let strongSelf = self, let context = context else { return @@ -736,9 +749,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.reloadFilters() } - self.storiesPostingAvailabilityDisposable = (self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + self.storiesPostingAvailabilityDisposable = (self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) |> map { view -> AppConfiguration in - let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + let appConfiguration: AppConfiguration = view?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue return appConfiguration } |> distinctUntilChanged @@ -1015,7 +1028,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if isDisabled { let context = self.context var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(self.tabContainerData?.0.count ?? 0), action: { + let controller = PremiumLimitScreen(context: context, subject: .folders, count: self.foldersCount, action: { let controller = PremiumIntroScreen(context: context, source: .folders) replaceImpl?(controller) return true @@ -1059,7 +1072,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if isDisabled { let context = self.context var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(self.tabContainerData?.0.count ?? 0), action: { + let controller = PremiumLimitScreen(context: context, subject: .folders, count: self.foldersCount, action: { let controller = PremiumIntroScreen(context: context, source: .folders) replaceImpl?(controller) return true @@ -1789,7 +1802,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) @@ -1888,16 +1901,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) - let contextController = makeContextController(presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined, canSelect: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined, canSelect: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } else { let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) chatListController.navigationPresentation = .master - let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } else if let peer = peer.peer, peer.id == strongSelf.context.account.peerId, peerData.displayAsTopicList { - if let peerInfoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let peerInfoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { let source: ContextContentSource if let location = location { source = .location(ChatListContextLocationContentSource(controller: strongSelf, location: location)) @@ -1905,7 +1918,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController source = .controller(ContextControllerContentSourceImpl(controller: peerInfoController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) } - let contextController = makeContextController(presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } else { @@ -1957,7 +1970,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) - let contextController = makeContextController(presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: isPinned, isClosed: threadInfo?.isClosed, chatListController: strongSelf, joined: joined, canSelect: true) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: isPinned, isClosed: threadInfo?.isClosed, chatListController: strongSelf, joined: joined, canSelect: true) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } @@ -1992,7 +2005,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if case let .channel(channel) = peer, channel.isForumOrMonoForum { let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) chatListController.navigationPresentation = .master - let contextController = makeContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = makeContextController(context: strongSelf.context, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } else { let contextContentSource: ContextContentSource @@ -2377,7 +2390,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } let context = strongSelf.context var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(strongSelf.tabContainerData?.0.count ?? 0), action: { + let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.foldersCount, action: { let controller = PremiumIntroScreen(context: context, source: .folders) replaceImpl?(controller) return true @@ -2698,11 +2711,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if !self.processedFeaturedFilters { - let initializedFeatured = self.context.account.postbox.preferencesView(keys: [ - PreferencesKeys.chatListFiltersFeaturedState - ]) + let initializedFeatured = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.chatListFiltersFeaturedState)) |> mapToSignal { view -> Signal in - if let entry = view.values[PreferencesKeys.chatListFiltersFeaturedState]?.get(ChatListFiltersFeaturedState.self) { + if let entry = view?.get(ChatListFiltersFeaturedState.self) { return .single(!entry.filters.isEmpty && !entry.isSeen) } else { return .complete() @@ -3295,7 +3306,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let self else { return } - guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { return } (self.navigationController as? NavigationController)?.pushViewController(controller) @@ -3303,7 +3314,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) }))) - let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: topSearchPeers) + let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: peer, peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: topSearchPeers) items.append(.action(ContextMenuActionItem(text: isMuted ? self.presentationData.strings.StoryFeed_ContextNotifyOn : self.presentationData.strings.StoryFeed_ContextNotifyOff, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in @@ -3781,7 +3792,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) |> deliverOnMainQueue).startStandalone(next: { peer in - guard let sourceController = sourceController, let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + guard let sourceController = sourceController, let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { return } (sourceController.navigationController as? NavigationController)?.pushViewController(controller) @@ -3801,7 +3812,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } let selectAddMemberDisposable = MetaDisposable() let addMemberDisposable = MetaDisposable() - context.sharedContext.openAddPeerMembers(context: context, updatedPresentationData: nil, parentController: sourceController, groupPeer: peer._asPeer(), selectAddMemberDisposable: selectAddMemberDisposable, addMemberDisposable: addMemberDisposable) + context.sharedContext.openAddPeerMembers(context: context, updatedPresentationData: nil, parentController: sourceController, groupPeer: peer, selectAddMemberDisposable: selectAddMemberDisposable, addMemberDisposable: addMemberDisposable) }) }))) } @@ -3953,13 +3964,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController resolvedItems = [] } - var wasEmpty = false - if let tabContainerData = strongSelf.tabContainerData { - wasEmpty = tabContainerData.0.count <= 1 || tabContainerData.1 - } else { - wasEmpty = true - } - let firstItem = countAndFilterItems.1.first?.0 ?? .allChats let firstItemEntryId: ChatListFilterTabEntryId switch firstItem { @@ -4028,17 +4032,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.initializedFilters = true } - let isEmpty = resolvedItems.count <= 1 - let animated = strongSelf.didSetupTabs strongSelf.didSetupTabs = true if let layout = strongSelf.validLayout { - if wasEmpty != isEmpty { - let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate - strongSelf.containerLayoutUpdated(layout, transition: transition) - (strongSelf.parent as? TabBarController)?.updateLayout(transition: transition) - } + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate + strongSelf.containerLayoutUpdated(layout, transition: transition) + (strongSelf.parent as? TabBarController)?.updateLayout(transition: transition) } if !notifiedFirstUpdate { @@ -4563,7 +4563,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { confirmDeleteFolder() }), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }) ]), in: .window(.root)) } else { @@ -6251,7 +6251,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if isDisabled { let context = strongSelf.context var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(strongSelf.tabContainerData?.0.count ?? 0), action: { + let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.foldersCount, action: { let controller = PremiumIntroScreen(context: context, source: .folders) replaceImpl?(controller) return true @@ -7177,9 +7177,6 @@ private final class ChatListLocationContext { titleContent = NetworkStatusTitle(text: presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) case let .connecting(proxy): let text = presentationData.strings.State_Connecting - /*if let layout = strongSelf.validLayout, proxy != nil && layout.metrics.widthClass != .regular && layout.size.width > 320.0 {*/ - //text = self.presentationData.strings.State_ConnectingToProxy - //} if let proxy = proxy, proxy.hasConnectionIssues { checkProxy = true } @@ -7290,7 +7287,7 @@ private final class ChatListLocationContext { dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, displayBackground: false, - content: .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, customSubtitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: nil, customMessageCount: nil, isEnabled: true), + content: .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, customSubtitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: nil, customMessageCount: nil, hidePeerStatus: false, isEnabled: true), activities: nil, networkState: nil, tapped: { [weak self] in @@ -7301,7 +7298,7 @@ private final class ChatListLocationContext { TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in - guard let self, let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + guard let self, let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { return } (self.parentController?.navigationController as? NavigationController)?.pushViewController(controller) @@ -7392,3 +7389,25 @@ private final class AdsInfoContextReferenceContentSource: ContextReferenceConten return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets) } } + +public struct ChatListNavigationTarget { + public let chatListController: ChatListControllerImpl + public let popToController: ViewController? +} + +public func resolveChatListNavigationTarget(navigationController: NavigationController, excluding excludedController: ViewController? = nil) -> ChatListNavigationTarget? { + for case let controller as ViewController in navigationController.viewControllers.reversed() { + if let excludedController, controller === excludedController { + continue + } + if let chatListController = controller as? ChatListControllerImpl, !chatListController.previewing { + return ChatListNavigationTarget(chatListController: chatListController, popToController: controller) + } + } + + if let controller = navigationController.viewControllers.first as? TabBarController, let chatListController = controller.currentController as? ChatListControllerImpl { + return ChatListNavigationTarget(chatListController: chatListController, popToController: nil) + } + + return nil +} diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 91c9505a72..826aa80f86 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1504,6 +1504,24 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { if self.controller?.tabContainerData != nil || !panels.isEmpty { var tabs: AnyComponent? if let tabContainerData = self.controller?.tabContainerData, tabContainerData.0.count > 1 { + let folderFilterIndex: (ChatListFilterTabEntryId, [ChatListFilterTabEntry]) -> Int? = { id, entries in + var index = 0 + for entry in entries { + switch entry { + case .all: + if entry.id == id { + return nil + } + case .filter: + if entry.id == id { + return index + } + index += 1 + } + } + return nil + } + let selectedTab: HorizontalTabsComponent.Tab.Id switch self.effectiveContainerNode.currentItemFilter { case .all: @@ -1553,10 +1571,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { var isDisabled = false if let filtersLimit = tabContainerData.2 { - guard let folderIndex = tabContainerData.0.firstIndex(where: { $0.id == mappedId }) else { - return + if let folderIndex = folderFilterIndex(mappedId, tabContainerData.0) { + isDisabled = !isPremium && folderIndex >= filtersLimit } - isDisabled = !isPremium && folderIndex >= filtersLimit } if isDisabled { @@ -1599,10 +1616,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { var isDisabled = false if let filtersLimit = tabContainerData.2 { - guard let folderIndex = tabContainerData.0.firstIndex(where: { $0.id == entry.id }) else { - return + if let folderIndex = folderFilterIndex(entry.id, tabContainerData.0) { + isDisabled = !isPremium && folderIndex >= filtersLimit } - isDisabled = !isPremium && folderIndex >= filtersLimit } self.controller?.tabContextGesture(id: mappedId, sourceNode: nil, sourceView: sourceView, gesture: gesture, keepInPlace: false, isDisabled: isDisabled) @@ -1949,7 +1965,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 @@ -1972,6 +2002,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { contentNode.dismissSearch = { [weak self] in self?.dismissSearch?() } + contentNode.dismissSearchImmediately = { [weak self] in + self?.controller?.deactivateSearch(animated: false) + } contentNode.openAdInfo = { [weak self] node, adPeer in self?.controller?.openAdInfo(node: node, adPeer: adPeer) } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index d85f03cf88..3d58d6a74c 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -1281,7 +1281,6 @@ private final class ChatListFilterPresetController: ItemListController { mode: .standard(.default), chatLocation: .peer(id: context.account.peerId), subject: nil, - peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, @@ -2076,7 +2075,7 @@ public func chatListFilterPresetController(context: AccountContext, currentPrese let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .text("___close"), style: .regular, enabled: true, action: { if let attemptNavigationImpl { attemptNavigationImpl({ value in if value { @@ -2087,7 +2086,7 @@ public func chatListFilterPresetController(context: AccountContext, currentPrese dismissImpl?() } }) - let rightNavigationButton = ItemListNavigationButton(content: .text(currentPreset == nil ? presentationData.strings.Common_Create : presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: { + let rightNavigationButton = ItemListNavigationButton(content: .text("___done"), style: .bold, enabled: state.isComplete, action: { applyImpl?(false, { dismissImpl?() }) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 2d595caf56..0d90835b6f 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -580,18 +580,18 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch pushControllerImpl?(controller) }) - let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState]) + let featuredFilters = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.chatListFiltersFeaturedState)) |> map { preferences -> [ChatListFeaturedFilter] in - guard let state = preferences.values[PreferencesKeys.chatListFiltersFeaturedState]?.get(ChatListFiltersFeaturedState.self) else { + guard let state = preferences?.get(ChatListFiltersFeaturedState.self) else { return [] } return state.filters } |> distinctUntilChanged - + let updatedFilterOrder = Promise<[Int32]?>(nil) - - let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings]) + + let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: ApplicationSpecificPreferencesKeys.chatListFilterSettings)) let previousDisplayTags = Atomic(value: nil) @@ -622,7 +622,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch case .default: leftNavigationButton = nil case .modal: - leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Close), style: .regular, enabled: true, action: { + leftNavigationButton = ItemListNavigationButton(content: .text("___close"), style: .regular, enabled: true, action: { dismissImpl?() }) } diff --git a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift index 0cac0a1bff..8cc7a4aabe 100644 --- a/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListRecentPeersListItem.swift @@ -122,8 +122,7 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { } else { peersNode = ChatListSearchRecentPeersNode( accountPeerId: item.context.account.peerId, - postbox: item.context.account.postbox, - network: item.context.account.network, + stateManager: item.context.account.stateManager, energyUsageSettings: item.context.sharedContext.energyUsageSettings, contentSettings: item.context.currentContentSettings.with { $0 }, animationCache: item.context.animationCache, diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 2495e589ee..09eec8961f 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 @@ -43,6 +42,7 @@ import ComponentDisplayAdapters private enum ChatListTokenId: Int32 { case archive + case folder case forum case filter case peer @@ -102,6 +102,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? @@ -109,6 +110,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo private let navigationController: NavigationController? var dismissSearch: (() -> Void)? + var dismissSearchImmediately: (() -> Void)? var openAdInfo: ((ASDisplayNode, AdPeer) -> Void)? private let edgeEffectView: EdgeEffectView @@ -165,16 +167,22 @@ 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 } + var folder = folder + folder = nil + self.context = context self.peersFilter = filter self.requestPeerType = requestPeerType self.location = location + + self.folder = folder + self.displaySearchFilters = displaySearchFilters self.hasDownloads = hasDownloads self.navigationController = navigationController @@ -194,6 +202,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 @@ -427,16 +440,23 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo suggestedPeers = .single([]) } - let accountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) + let accountPeer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> take(1) - + self.suggestedFiltersDisposable.set((combineLatest(suggestedPeers, self.suggestedDates.get(), self.selectedFilterPromise.get(), self.searchQuery.get(), accountPeer) |> mapToSignal { peers, dates, selectedFilter, searchQuery, accountPeer -> Signal<([EnginePeer], [(Date?, Date, String?)], ChatListSearchFilterEntryId?, String?, EnginePeer?), NoError> in if searchQuery?.isEmpty ?? true { - return .single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer))) + return .single((peers, dates, selectedFilter?.id, searchQuery, accountPeer)) } else { return (.complete() |> delay(0.25, queue: Queue.mainQueue())) - |> then(.single((peers, dates, selectedFilter?.id, searchQuery, EnginePeer(accountPeer)))) + |> then(.single((peers, dates, selectedFilter?.id, searchQuery, accountPeer))) } } |> map { peers, dates, selectedFilter, searchQuery, accountPeer -> ([ChatListSearchFilter], Bool) in var suggestedFilters: [ChatListSearchFilter] = [] @@ -588,7 +608,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 +629,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 +764,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 { @@ -809,6 +839,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() @@ -855,9 +887,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) } @@ -895,7 +927,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } var type: PeerType = .group for message in messages { - if let user = message.author?._asPeer() as? TelegramUser { + if case let .user(user) = message.author { if user.botInfo != nil && !user.id.isVerificationCodes { type = .bot } else { @@ -1034,7 +1066,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if paneKey == .downloads { let isCachedValue: Signal if let downloadResource = downloadResource { - isCachedValue = self.context.account.postbox.mediaBox.resourceStatus(MediaResourceId(downloadResource.id), resourceSize: downloadResource.size) + isCachedValue = self.context.engine.resources.status(id: EngineMediaResource.Id(downloadResource.id), resourceSize: downloadResource.size) |> map { status -> Bool in switch status { case .Local: @@ -1082,7 +1114,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo f(.default) return } - let _ = (strongSelf.context.account.postbox.mediaBox.removeCachedResources([MediaResourceId(downloadResource.id)], notify: true) + let _ = (strongSelf.context.engine.resources.removeCachedResources(ids: [EngineMediaResource.Id(downloadResource.id)], notify: true) |> deliverOnMainQueue).startStandalone(completed: { f(.dismissWithoutContent) }) @@ -1322,8 +1354,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?) { @@ -1373,7 +1405,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo } } - let _ = (strongSelf.context.account.postbox.mediaBox.removeCachedResources(Array(resourceIds), force: true, notify: true) + let _ = (strongSelf.context.engine.resources.removeCachedResources(ids: resourceIds.map { EngineMediaResource.Id($0) }, force: true, notify: true) |> deliverOnMainQueue).startStandalone(completed: { guard let strongSelf = self else { return @@ -1636,7 +1668,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo if let strongSelf = self { let proceed: (ChatController) -> Void = { chatController in chatController.purposefulAction = { [weak self] in - self?.cancel?() + self?.dismissSearchImmediately?() } if let navigationController = strongSelf.navigationController { var viewControllers = navigationController.viewControllers diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 7329c1f7ed..d702a7d420 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -150,7 +150,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { let primaryPeer: EnginePeer var chatPeer: EnginePeer? let maybeChatPeer = EnginePeer(peer.peer.peers[peer.peer.peerId]!) - if case .secretChat = maybeChatPeer, let associatedPeerId = maybeChatPeer._asPeer().associatedPeerId, let associatedPeer = peer.peer.peers[associatedPeerId] { + if case .secretChat = maybeChatPeer, let associatedPeerId = maybeChatPeer.associatedPeerId, let associatedPeer = peer.peer.peers[associatedPeerId] { primaryPeer = EnginePeer(associatedPeer) chatPeer = maybeChatPeer } else if case .channel = maybeChatPeer, let mainChannel = peer.peer.chatOrMonoforumMainPeer { @@ -164,9 +164,9 @@ private enum ChatListRecentEntry: Comparable, Identifiable { var enabled = true if filter.contains(.onlyWriteable) { if let peer = chatPeer { - enabled = canSendMessagesToPeer(peer._asPeer()) + enabled = canSendMessagesToPeer(peer) } else { - enabled = canSendMessagesToPeer(primaryPeer._asPeer()) + enabled = canSendMessagesToPeer(primaryPeer) } if requiresPremiumForMessaging { enabled = false @@ -753,7 +753,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { var enabled = true if filter.contains(.onlyWriteable) { if let peer = chatPeer { - enabled = canSendMessagesToPeer(peer._asPeer()) + enabled = canSendMessagesToPeer(peer) } else { enabled = false } @@ -892,7 +892,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { var enabled = true if filter.contains(.onlyWriteable) { if let peer = chatPeer { - enabled = canSendMessagesToPeer(peer._asPeer()) + enabled = canSendMessagesToPeer(peer) } else { enabled = false } @@ -1021,13 +1021,16 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } } if filter.contains(.onlyPrivateChats) { - if !(peer.peer is TelegramUser || peer.peer is TelegramSecretChat) { + switch peer.peer { + case .user, .secretChat: + break + default: enabled = false } } if filter.contains(.onlyGroups) { - if let _ = peer.peer as? TelegramGroup { - } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + if case .legacyGroup = peer.peer { + } else if case let .channel(channel) = peer.peer, case .group = channel.info { } else { enabled = false } @@ -1035,9 +1038,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable { var suffixString = "" if let subscribers = peer.subscribers, subscribers != 0 { - if peer.peer is TelegramUser { + if case .user = peer.peer { suffixString = ", \(strings.Conversation_StatusBotSubscribers(subscribers))" - } else if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { + } else if case let .channel(channel) = peer.peer, case .broadcast = channel.info { suffixString = ", \(strings.Conversation_StatusSubscribers(subscribers))" } else { suffixString = ", \(strings.Conversation_StatusMembers(subscribers))" @@ -1072,13 +1075,13 @@ public enum ChatListSearchEntry: Comparable, Identifiable { isSavedMessages = true } - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, isAd: false, action: { _ in - interaction.peerSelected(EnginePeer(peer.peer), nil, nil, nil, false) + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .generalSearch(isSavedMessages: isSavedMessages), peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .addressName(suffixString), badge: badge, requiresPremiumForMessaging: requiresPremiumForMessaging, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, searchQuery: query, isAd: false, action: { _ in + interaction.peerSelected(peer.peer, nil, nil, nil, false) }, disabledAction: { _ in - interaction.disabledPeerSelected(EnginePeer(peer.peer), nil, requiresPremiumForMessaging ? .premiumRequired : .generic) + interaction.disabledPeerSelected(peer.peer, nil, requiresPremiumForMessaging ? .premiumRequired : .generic) }, contextAction: peerContextAction.flatMap { peerContextAction in return { node, gesture, location in - peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture, location) + peerContextAction(peer.peer, .search(nil), node, gesture, location) } }, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer, storyStats: storyStats.flatMap { stats in return (stats.totalCount, stats.unseenCount, stats.hasUnseenCloseFriends, stats.hasLiveItems) @@ -1401,7 +1404,7 @@ private struct ChatListSearchListPaneNodeState: Equatable { private func doesPeerMatchFilter(peer: EnginePeer, filter: ChatListNodePeersFilter) -> Bool { var enabled = true - if filter.contains(.onlyWriteable), !canSendMessagesToPeer(peer._asPeer()) { + if filter.contains(.onlyWriteable), !canSendMessagesToPeer(peer) { enabled = false } if filter.contains(.onlyPrivateChats) { @@ -1448,17 +1451,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) } } @@ -1492,14 +1500,14 @@ private func filteredPeerSearchQueryResults(value: ([FoundPeer], [FoundPeer]), s case .channels: return ( value.0.filter { peer in - if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { + if case let .channel(channel) = peer.peer, case .broadcast = channel.info { return true } else { return false } }, value.1.filter { peer in - if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info { + if case let .channel(channel) = peer.peer, case .broadcast = channel.info { return true } else { return false @@ -2065,7 +2073,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), groupId: nil, tags: nil, minDate: nil, maxDate: nil, folderId: nil), query: "", state: nil, limit: 50) |> map { resultData -> ChatListSearchMessagesResult in let (result, updatedState) = resultData @@ -2089,7 +2097,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) } @@ -2187,7 +2195,16 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } - let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) |> take(1) + let accountPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + |> map { $0._asPeer() } + |> take(1) let foundLocalPeers: Signal<(peers: [EngineRenderedPeer], unread: [EnginePeer.Id: (Int32, Bool)], recentlySearchedPeerIds: Set), NoError> if case .savedMessagesChats = location { @@ -2244,7 +2261,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 +2586,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 +2618,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), groupId: nil, 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, groupId: nil, 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)] + if case let .chatList(groupId) = location { + searchLocations = [.general(scope: searchScope, groupId: groupId._asGroup(), tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1, folderId: options.folder?.0)] } else { - searchLocations = [.general(scope: searchScope, tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)] + searchLocations = [.general(scope: searchScope, groupId: nil, 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, groupId: nil, 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)] - } else if case let .chatList(groupId) = location, case .archive = groupId { - searchLocations = [.group(groupId: groupId._asGroup(), 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, groupId: nil, tags: tagMask, minDate: nil, maxDate: nil, folderId: nil)] + } else if case let .chatList(groupId) = location { + searchLocations = [.general(scope: searchScope, groupId: groupId._asGroup(), tags: tagMask, minDate: nil, maxDate: nil, folderId: nil)] } else { - searchLocations = [.general(scope: searchScope, tags: tagMask, minDate: nil, maxDate: nil)] + searchLocations = [.general(scope: searchScope, groupId: nil, tags: tagMask, minDate: nil, maxDate: nil, folderId: nil)] } } @@ -2704,7 +2738,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), groupId: nil, 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) @@ -2800,9 +2834,6 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { |> mapToSignal { resolvedUrl -> Signal in if case let .channelMessage(_, messageId, _) = resolvedUrl { return context.engine.messages.downloadMessage(messageId: messageId) - |> map { message -> EngineMessage? in - return message.flatMap(EngineMessage.init) - } } else { return .single(nil) } @@ -3057,7 +3088,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } for peer in foundRemotePeers.0 { - if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) { + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, EnginePeer(accountPeer)) { existingPeerIds.insert(peer.peer.id) totalNumberOfLocalPeers += 1 } @@ -3065,7 +3096,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var totalNumberOfGlobalPeers = 0 for peer in foundRemotePeers.1 { - if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) { + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, EnginePeer(accountPeer)) { totalNumberOfGlobalPeers += 1 } } @@ -3183,9 +3214,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { break } - if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) { + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, EnginePeer(accountPeer)) { existingPeerIds.insert(peer.peer.id) - entries.append(.localPeer(EnginePeer(peer.peer), nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType, nil, false, false)) + entries.append(.localPeer(peer.peer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType, nil, false, false)) index += 1 numberOfLocalPeers += 1 } @@ -3210,7 +3241,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { break } - if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) { + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer, EnginePeer(accountPeer)) { existingPeerIds.insert(peer.peer.id) entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil, false, finalQuery)) @@ -3689,7 +3720,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } case let .globalPeer(foundPeer, _, _, _, _, _, _, _, _, _, _): storyStatsIds.append(foundPeer.peer.id) - if let user = foundPeer.peer as? TelegramUser, user.flags.contains(.requirePremium) { + if case let .user(user) = foundPeer.peer, user.flags.contains(.requirePremium) { requiresPremiumForMessagingPeerIds.append(foundPeer.peer.id) } case let .message(_, peer, _, _, _, _, _, _, _, _, _, _, _, _, _): @@ -4139,7 +4170,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { result.append(.peer( index: result.count, peer: RecentlySearchedPeer( - peer: RenderedPeer(peer: peer._asPeer()), + peer: RenderedPeer(peer: peer), presence: nil, notificationSettings: peerNotificationSettings.flatMap({ $0._asNotificationSettings() }), unreadCount: unreadCount, @@ -4179,7 +4210,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { result.append(.peer( index: result.count, peer: RecentlySearchedPeer( - peer: RenderedPeer(peer: peer._asPeer()), + peer: RenderedPeer(peer: peer), presence: nil, notificationSettings: peerNotificationSettings.flatMap({ $0._asNotificationSettings() }), unreadCount: 0, @@ -4302,7 +4333,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { result.append(.peer( index: result.count, peer: RecentlySearchedPeer( - peer: RenderedPeer(peer: peer._asPeer()), + peer: RenderedPeer(peer: peer), presence: nil, notificationSettings: peerNotificationSettings.flatMap({ $0._asNotificationSettings() }), unreadCount: unreadCount, @@ -4337,7 +4368,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { result.append(.peer( index: result.count, peer: RecentlySearchedPeer( - peer: RenderedPeer(peer: peer._asPeer()), + peer: RenderedPeer(peer: peer), presence: nil, notificationSettings: peerNotificationSettings.flatMap({ $0._asNotificationSettings() }), unreadCount: 0, @@ -4430,7 +4461,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { keepStack: .always )) case .info: - if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { navigationController.pushViewController(peerInfoScreen) } case .openApp: diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index ec2a65f95a..abc01321fb 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 @@ -1969,6 +1969,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { profilePhoto = maybePhoto isKnown = true } + if profilePhoto == nil, case let .known(maybePhoto) = cachedPeerData.fallbackPhoto { + profilePhoto = maybePhoto + isKnown = true + } } if isKnown { @@ -2384,11 +2388,8 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { var currentStatusIconContent: EmojiStatusComponent.Content? var currentStatusIconParticleColor: UIColor? var currentSecretIconImage: UIImage? - var currentForwardedIcon: UIImage? - var currentStoryIcon: UIImage? - var currentGiftIcon: UIImage? - var currentLocationIcon: UIImage? - var currentPollIcon: UIImage? + var currentMessageTypeIcon: UIImage? + var currentMessageTypeIconOffset: CGPoint = .zero var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? @@ -2559,12 +2560,27 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { var contentImageSpecs: [ContentImageSpec] = [] var avatarContentImageSpec: ContentImageSpec? var forumThread: (id: Int64, title: String, iconId: Int64?, iconColor: Int32, threadPeer: EnginePeer?, isUnread: Bool)? - - var displayForwardedIcon = false - var displayStoryReplyIcon = false - var displayGiftIcon = false - var displayLocationIcon = false - var displayPollIcon = false + + enum MessageTypeIcon { + enum CallType { + case voice + case video + } + enum CallDirection { + case incoming + case outgoing + } + case call(CallType, CallDirection) + case forward + case story + case gift + case location + case poll + case todo + case game + case voiceMessage + } + var messageTypeIcon: MessageTypeIcon? var ignoreForwardedIcon = false switch contentData { @@ -2573,6 +2589,10 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if case .user = itemPeer.chatMainPeer { isUser = true } + var isGuestChatAuthor = false + if case let .user(user) = messages.last?.author, let botInfo = user.botInfo, botInfo.flags.contains(.isGuestChat) { + isGuestChatAuthor = true + } var peerText: String? if case .savedMessagesChats = item.chatListLocation { @@ -2590,14 +2610,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if let message = messages.last, let forwardInfo = message.forwardInfo, let author = forwardInfo.author { peerText = EnginePeer(author).compactDisplayTitle } - } else if !isUser { + } else if !isUser || isGuestChatAuthor { if case let .channel(peer) = peer, case .broadcast = peer.info { } else if !displayAsMessage { if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), let authorSignature = forwardInfo.authorSignature { peerText = authorSignature } else { peerText = author.id == account.peerId ? item.presentationData.strings.DialogList_You : EnginePeer(author).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) - authorIsCurrentChat = author.id == peer.id + authorIsCurrentChat = !isGuestChatAuthor && author.id == peer.id } } } @@ -2722,7 +2742,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 { @@ -2873,24 +2894,33 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if !ignoreForwardedIcon { if case .savedMessagesChats = item.chatListLocation { - displayForwardedIcon = false } else if let forwardInfo = message.forwardInfo, !forwardInfo.flags.contains(.isImported) && !message.id.peerId.isVerificationCodes { - displayForwardedIcon = true + messageTypeIcon = .forward } else if let _ = message.attributes.first(where: { $0 is ReplyStoryAttribute }) { - displayStoryReplyIcon = true + messageTypeIcon = .story } else { for media in message.media { - if let _ = media as? TelegramMediaPoll { - displayPollIcon = true + if let file = media as? TelegramMediaFile { + if file.isVoice { + messageTypeIcon = .voiceMessage + } + } else if let _ = media as? TelegramMediaPoll { + messageTypeIcon = .poll + } else if let _ = media as? TelegramMediaTodo { + messageTypeIcon = .todo + } else if let _ = media as? TelegramMediaGame { + messageTypeIcon = .game } else if let _ = media as? TelegramMediaMap { - displayLocationIcon = true + messageTypeIcon = .location } else if let action = media as? TelegramMediaAction { switch action.action { + case let .phoneCall(_, _, _, isVideo): + messageTypeIcon = .call(isVideo ? .video : .voice, message.flags.contains(.Incoming) ? .incoming : .outgoing) case .giftPremium, .giftStars, .starGift, .starGiftUnique: - displayGiftIcon = true + messageTypeIcon = .gift case let .giftCode(_, _, _, boostPeerId, _, _, _, _, _, _, _): if boostPeerId == nil { - displayGiftIcon = true + messageTypeIcon = .gift } default: break @@ -3047,68 +3077,57 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { attributedText = textString } - if displayForwardedIcon { - currentForwardedIcon = PresentationResourcesChatList.forwardedIcon(item.presentationData.theme) - } - - if displayStoryReplyIcon { - currentStoryIcon = PresentationResourcesChatList.storyReplyIcon(item.presentationData.theme) - } - - if displayGiftIcon { - currentGiftIcon = PresentationResourcesChatList.giftIcon(item.presentationData.theme) - } - - if displayLocationIcon { - currentLocationIcon = PresentationResourcesChatList.locationIcon(item.presentationData.theme) - } - - if displayPollIcon { - currentPollIcon = PresentationResourcesChatList.pollIcon(item.presentationData.theme) - } - - if let currentForwardedIcon { - textLeftCutout += currentForwardedIcon.size.width - if !contentImageSpecs.isEmpty { - textLeftCutout += forwardedIconSpacing - } else { - textLeftCutout += contentImageTrailingSpace + switch messageTypeIcon { + case let .call(type, direction): + switch type { + case .voice: + switch direction { + case .incoming: + currentMessageTypeIcon = PresentationResourcesChatList.callIncomingIcon(item.presentationData.theme) + case .outgoing: + currentMessageTypeIcon = PresentationResourcesChatList.callOutgoingIcon(item.presentationData.theme) + } + case .video: + switch direction { + case .incoming: + currentMessageTypeIcon = PresentationResourcesChatList.callVideoIncomingIcon(item.presentationData.theme) + case .outgoing: + currentMessageTypeIcon = PresentationResourcesChatList.callVideoOutgoingIcon(item.presentationData.theme) + } } + case .forward: + currentMessageTypeIcon = PresentationResourcesChatList.forwardedIcon(item.presentationData.theme) + currentMessageTypeIconOffset.y = 3.0 + case .story: + currentMessageTypeIcon = PresentationResourcesChatList.storyReplyIcon(item.presentationData.theme) + case .gift: + currentMessageTypeIcon = PresentationResourcesChatList.giftIcon(item.presentationData.theme) + currentMessageTypeIconOffset.y = -2.0 - UIScreenPixel + case .location: + currentMessageTypeIcon = PresentationResourcesChatList.locationIcon(item.presentationData.theme) + currentMessageTypeIconOffset.y = -1.0 - UIScreenPixel + case .poll: + currentMessageTypeIcon = PresentationResourcesChatList.pollIcon(item.presentationData.theme) + currentMessageTypeIconOffset.y = -1.0 + case .todo: + currentMessageTypeIcon = PresentationResourcesChatList.todoIcon(item.presentationData.theme) + currentMessageTypeIconOffset.y = -1.0 + case .game: + currentMessageTypeIcon = PresentationResourcesChatList.gameIcon(item.presentationData.theme) + currentMessageTypeIconOffset.y = -1.0 + case .voiceMessage: + currentMessageTypeIcon = PresentationResourcesChatList.voiceMessageIcon(item.presentationData.theme) + currentMessageTypeIconOffset.y = -1.0 + default: + break } - if let currentStoryIcon { - textLeftCutout += currentStoryIcon.size.width + if let currentMessageTypeIcon { + textLeftCutout += currentMessageTypeIcon.size.width if !contentImageSpecs.isEmpty { textLeftCutout += forwardedIconSpacing } else { - textLeftCutout += contentImageTrailingSpace - } - } - - if let currentGiftIcon { - textLeftCutout += currentGiftIcon.size.width - if !contentImageSpecs.isEmpty { - textLeftCutout += forwardedIconSpacing - } else { - textLeftCutout += contentImageTrailingSpace - } - } - - if let currentLocationIcon { - textLeftCutout += currentLocationIcon.size.width - if !contentImageSpecs.isEmpty { - textLeftCutout += forwardedIconSpacing - } else { - textLeftCutout += contentImageTrailingSpace - } - } - - if let currentPollIcon { - textLeftCutout += currentPollIcon.size.width - if !contentImageSpecs.isEmpty { - textLeftCutout += forwardedIconSpacing - } else { - textLeftCutout += contentImageTrailingSpace + textLeftCutout += contentImageTrailingSpace - 1.0 } } @@ -3247,45 +3266,42 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { } if unreadCount.unread { - if !isPeerGroup, let message = messages.last, message.tags.contains(.unseenPersonalMessage), unreadCount.count == 1 { + let badgeTextColor: UIColor + if unreadCount.muted { + if unreadCount.isProvisonal, case .forum = item.chatListLocation { + badgeTextColor = theme.unreadBadgeInactiveBackgroundColor + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: badgeDiameter) + currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter) + } else { + badgeTextColor = theme.unreadBadgeInactiveTextColor + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter) + currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: avatarBadgeDiameter) + } } else { - let badgeTextColor: UIColor - if unreadCount.muted { - if unreadCount.isProvisonal, case .forum = item.chatListLocation { - badgeTextColor = theme.unreadBadgeInactiveBackgroundColor - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: badgeDiameter) - currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter) - } else { - badgeTextColor = theme.unreadBadgeInactiveTextColor - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter) - currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: avatarBadgeDiameter) - } + if unreadCount.isProvisonal, case .forum = item.chatListLocation { + badgeTextColor = theme.unreadBadgeActiveBackgroundColor + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: badgeDiameter) + currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter) } else { - if unreadCount.isProvisonal, case .forum = item.chatListLocation { - badgeTextColor = theme.unreadBadgeActiveBackgroundColor - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: badgeDiameter) - currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter) - } else { - badgeTextColor = theme.unreadBadgeActiveTextColor - currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: badgeDiameter) - currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: avatarBadgeDiameter) - } - } - let unreadCountText = compactNumericCountString(Int(unreadCount.count), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) - if unreadCount.count > 0 { - badgeContent = .text(NSAttributedString(string: unreadCountText, font: badgeFont, textColor: badgeTextColor)) - } else if isPeerGroup { - badgeContent = .none - } else { - badgeContent = .blank - } - - if let mutedCount = unreadCount.mutedCount, mutedCount > 0 { - let mutedUnreadCountText = compactNumericCountString(Int(mutedCount), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) - currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter) - mentionBadgeContent = .text(NSAttributedString(string: mutedUnreadCountText, font: badgeFont, textColor: theme.unreadBadgeInactiveTextColor)) + badgeTextColor = theme.unreadBadgeActiveTextColor + currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: badgeDiameter) + currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: avatarBadgeDiameter) } } + let unreadCountText = compactNumericCountString(Int(unreadCount.count), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + if unreadCount.count > 0 { + badgeContent = .text(NSAttributedString(string: unreadCountText, font: badgeFont, textColor: badgeTextColor)) + } else if isPeerGroup { + badgeContent = .none + } else { + badgeContent = .blank + } + + if let mutedCount = unreadCount.mutedCount, mutedCount > 0 { + let mutedUnreadCountText = compactNumericCountString(Int(mutedCount), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) + currentMentionBadgeImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter) + mentionBadgeContent = .text(NSAttributedString(string: mutedUnreadCountText, font: badgeFont, textColor: theme.unreadBadgeInactiveTextColor)) + } } if !isPeerGroup { @@ -4767,31 +4783,16 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { var mediaPreviewOffset = textNodeFrame.origin.offsetBy(dx: 1.0, dy: 1.0 + floor((measureLayout.size.height - contentImageSize.height) / 2.0)) - var messageTypeIcon: UIImage? - var messageTypeIconOffset = mediaPreviewOffset - if let currentForwardedIcon { - messageTypeIcon = currentForwardedIcon - messageTypeIconOffset.y += 3.0 - } else if let currentStoryIcon { - messageTypeIcon = currentStoryIcon - } else if let currentGiftIcon { - messageTypeIcon = currentGiftIcon - messageTypeIconOffset.y -= 2.0 - UIScreenPixel - } else if let currentLocationIcon { - messageTypeIcon = currentLocationIcon - messageTypeIconOffset.y -= 2.0 - UIScreenPixel - } else if let currentPollIcon { - messageTypeIcon = currentPollIcon - messageTypeIconOffset.y -= 2.0 - UIScreenPixel - } + let messageTypeIconImage = currentMessageTypeIcon + let messageTypeIconOffset = CGPoint(x: mediaPreviewOffset.x + currentMessageTypeIconOffset.x, y: mediaPreviewOffset.y + currentMessageTypeIconOffset.y) - if let messageTypeIcon { - strongSelf.forwardedIconNode.image = messageTypeIcon + if let messageTypeIconImage { + strongSelf.forwardedIconNode.image = messageTypeIconImage if strongSelf.forwardedIconNode.supernode == nil { strongSelf.mainContentContainerNode.addSubnode(strongSelf.forwardedIconNode) } - transition.updateFrame(node: strongSelf.forwardedIconNode, frame: CGRect(origin: messageTypeIconOffset, size: messageTypeIcon.size)) - mediaPreviewOffset.x += messageTypeIcon.size.width + forwardedIconSpacing + transition.updateFrame(node: strongSelf.forwardedIconNode, frame: CGRect(origin: messageTypeIconOffset, size: messageTypeIconImage.size)) + mediaPreviewOffset.x += messageTypeIconImage.size.width + forwardedIconSpacing } else if strongSelf.forwardedIconNode.supernode != nil { strongSelf.forwardedIconNode.removeFromSupernode() } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift index e82cfbb84d..b3b24b0048 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItemStrings.swift @@ -225,15 +225,13 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: break inner case let .Audio(isVoice, _, title, performer, _): if !message.text.isEmpty { - messageText = "🎤 \(messageText)" - processed = true - } else if isVoice { - if message.text.isEmpty { - messageText = strings.Message_Audio - } else { + if enableMediaEmoji { messageText = "🎤 \(messageText)" } processed = true + } else if isVoice { + messageText = strings.Message_Audio + processed = true break inner } else { let descriptionString: String @@ -287,7 +285,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 @@ -309,7 +309,7 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: case _ as TelegramMediaContact: messageText = strings.Message_Contact case let game as TelegramMediaGame: - messageText = "🎮 \(game.title)" + messageText = game.title case let invoice as TelegramMediaInvoice: messageText = invoice.title case let action as TelegramMediaAction: @@ -436,15 +436,11 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder: messageText = content.displayUrl } case let todo as TelegramMediaTodo: - let pollPrefix = "☑️ " - let entityOffset = (pollPrefix as NSString).length - messageText = "\(pollPrefix)\(todo.text)" + messageText = todo.text + customEmojiRanges = [] for entity in todo.textEntities { if case let .CustomEmoji(_, fileId) = entity.type { - if customEmojiRanges == nil { - customEmojiRanges = [] - } - let range = NSRange(location: entityOffset + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) + let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) let attribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: message.associatedMedia[EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile) customEmojiRanges?.append((range, attribute)) } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 4236fb048a..63c74cdd62 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) { + 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] { @@ -842,7 +864,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL } else { if filter.contains(.onlyWriteable) { if let peer = peer.peers[peer.peerId] { - if !canSendMessagesToPeer(peer._asPeer()) { + if !canSendMessagesToPeer(peer) { enabled = false } if peerEntry.requiresPremiumForMessaging { @@ -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,19 +1883,23 @@ 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 { - savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) - |> map(Optional.init) - |> map { peer in - return peer.flatMap(EnginePeer.init) + if case let .peers(filter, _, _, _, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location, self.chatListFilter == nil { + savedMessagesPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } } + |> map(Optional.init) } else { savedMessagesPeer = .single(nil) } - let hideArchivedFolderByDefault = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatArchiveSettings]) + let hideArchivedFolderByDefault = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: ApplicationSpecificPreferencesKeys.chatArchiveSettings)) |> map { view -> Bool in - let settings: ChatArchiveSettings = view.values[ApplicationSpecificPreferencesKeys.chatArchiveSettings]?.get(ChatArchiveSettings.self) ?? .default + let settings: ChatArchiveSettings = view?.get(ChatArchiveSettings.self) ?? .default return settings.isHiddenByDefault } |> distinctUntilChanged @@ -2102,7 +2150,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 } @@ -2178,7 +2226,7 @@ public final class ChatListNode: ListViewImpl { if filter.contains(.onlyWriteable) && filter.contains(.excludeDisabled) { if let peer = peer.peers[peer.peerId] { - if !canSendMessagesToPeer(peer._asPeer()) { + if !canSendMessagesToPeer(peer) { return false } } else { @@ -2538,9 +2586,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 +3440,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/ChatMessageBackground/BUILD b/submodules/ChatMessageBackground/BUILD index 84fd9a5074..51dc28d4b1 100644 --- a/submodules/ChatMessageBackground/BUILD +++ b/submodules/ChatMessageBackground/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/WallpaperBackgroundNode:WallpaperBackgroundNode", ], diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift index f554ef297b..c705171903 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPresentationInterfaceState.swift @@ -541,7 +541,6 @@ public final class ChatPresentationInterfaceState: Equatable { public let hasScheduledMessages: Bool public let autoremoveTimeout: Int32? public let subject: ChatControllerSubject? - public let peerNearbyData: ChatPeerNearbyData? public let greetingData: ChatGreetingData? public let pendingUnpinnedAllMessages: Bool public let activeGroupCallInfo: ChatActiveGroupCallInfo? @@ -590,7 +589,7 @@ public final class ChatPresentationInterfaceState: Equatable { public let viewForumAsMessages: Bool public let hasTopics: Bool - public init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, preferredGlassType: GlassType, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, chatLocation: ChatLocation, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, threadData: ThreadData?, isGeneralThreadClosed: Bool?, replyMessage: Message?, accountPeerColor: AccountPeerColor?, businessIntro: TelegramBusinessIntro?) { + public init(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, preferredGlassType: GlassType, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, chatLocation: ChatLocation, subject: ChatControllerSubject?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, threadData: ThreadData?, isGeneralThreadClosed: Bool?, replyMessage: Message?, accountPeerColor: AccountPeerColor?, businessIntro: TelegramBusinessIntro?) { self.interfaceState = ChatInterfaceState() self.inputTextPanelState = ChatTextInputPanelState() self.editMessageState = nil @@ -639,7 +638,6 @@ public final class ChatPresentationInterfaceState: Equatable { self.hasScheduledMessages = false self.autoremoveTimeout = nil self.subject = subject - self.peerNearbyData = peerNearbyData self.greetingData = greetingData self.pendingUnpinnedAllMessages = pendingUnpinnedAllMessages self.activeGroupCallInfo = activeGroupCallInfo @@ -740,7 +738,6 @@ public final class ChatPresentationInterfaceState: Equatable { hasScheduledMessages: Bool, autoremoveTimeout: Int32?, subject: ChatControllerSubject?, - peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, @@ -837,7 +834,6 @@ public final class ChatPresentationInterfaceState: Equatable { self.hasScheduledMessages = hasScheduledMessages self.autoremoveTimeout = autoremoveTimeout self.subject = subject - self.peerNearbyData = peerNearbyData self.greetingData = greetingData self.pendingUnpinnedAllMessages = pendingUnpinnedAllMessages self.activeGroupCallInfo = activeGroupCallInfo @@ -1041,9 +1037,6 @@ public final class ChatPresentationInterfaceState: Equatable { if lhs.subject != rhs.subject { return false } - if lhs.peerNearbyData != rhs.peerNearbyData { - return false - } if lhs.greetingData != rhs.greetingData { return false } @@ -1189,43 +1182,43 @@ public final class ChatPresentationInterfaceState: Equatable { } public func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedFocusedPollAddOptionMessageId(_ focusedPollAddOptionMessageId: MessageId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedChatLocation(_ chatLocation: ChatLocation) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedPeer(_ f: (RenderedPeer?) -> RenderedPeer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: f(self.renderedPeer), isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: f(self.renderedPeer), isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedIsNotAccessible(_ isNotAccessible: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedExplicitelyCanPinMessages(_ explicitelyCanPinMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedContactStatus(_ contactStatus: ChatContactStatus?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedIsManagedBot(_ isManagedBot: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedHasBots(_ hasBots: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedIsArchived(_ isArchived: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedInputQueryResult(queryKind: ChatPresentationInputQueryKind, _ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { @@ -1242,323 +1235,323 @@ public final class ChatPresentationInterfaceState: Equatable { inputQueryResults.removeValue(forKey: queryKind) } - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedInputTextPanelState(_ f: (ChatTextInputPanelState) -> ChatTextInputPanelState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: f(self.inputTextPanelState), editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: f(self.inputTextPanelState), editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedEditMessageState(_ editMessageState: ChatEditInterfaceMessageState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedInputMode(_ f: (ChatInputMode) -> ChatInputMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedTitlePanelContext(_ f: ([ChatTitlePanelContext]) -> [ChatTitlePanelContext]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedKeyboardButtonsMessage(_ message: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedPinnedMessageId(_ pinnedMessageId: MessageId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedPinnedMessage(_ pinnedMessage: ChatPinnedMessage?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedPeerIsBlocked(_ peerIsBlocked: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedPeerIsMuted(_ peerIsMuted: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedPeerDiscussionId(_ peerDiscussionId: PeerId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedPeerGeoLocation(_ peerGeoLocation: PeerGeoLocation?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedCallsAvailable(_ callsAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedCallsPrivate(_ callsPrivate: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedSlowmodeState(_ slowmodeState: ChatSlowmodeState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedBotStartPayload(_ botStartPayload: String?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedChatHistoryState(_ chatHistoryState: ChatHistoryNodeHistoryState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedUrlPreview(_ urlPreview: UrlPreview?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedEditingUrlPreview(_ editingUrlPreview: UrlPreview?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedSearch(_ search: ChatSearchData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedSearchQuerySuggestionResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: f(self.searchQuerySuggestionResult), historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedHistoryFilter(_ historyFilter: HistoryFilter?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedDisplayHistoryFilterAsList(_ displayHistoryFilterAsList: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedMode(_ mode: ChatControllerPresentationMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedPresentationReady(_ presentationReady: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedTheme(_ theme: PresentationTheme) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedPreferredGlassType(_ glassType: GlassType) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: glassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: glassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedStrings(_ strings: PresentationStrings) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedDateTimeFormat(_ dateTimeFormat: PresentationDateTimeFormat) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedChatWallpaper(_ chatWallpaper: TelegramWallpaper) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedBubbleCorners(_ bubbleCorners: PresentationChatBubbleCorners) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedHasScheduledMessages(_ hasScheduledMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedSubject(_ subject: ChatControllerSubject?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedAutoremoveTimeout(_ autoremoveTimeout: Int32?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedPendingUnpinnedAllMessages(_ pendingUnpinnedAllMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedActiveGroupCallInfo(_ activeGroupCallInfo: ChatActiveGroupCallInfo?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedHasActiveGroupCall(_ hasActiveGroupCall: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedReportReason(_ reportReason: ReportReasonData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedShowCommands(_ showCommands: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedHasBotCommands(_ hasBotCommands: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedShowSendAsPeers(_ showSendAsPeers: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedSendAsPeers(_ sendAsPeers: [SendAsPeer]?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedCurrentSendAsPeerId(_ currentSendAsPeerId: PeerId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedBotMenuButton(_ botMenuButton: BotMenuButton) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedShowWebView(_ showWebView: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedCopyProtectionEnabled(_ copyProtectionEnabled: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedMyCopyProtectionEnabled(_ myCopyProtectionEnabled: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedHasAtLeast3Messages(_ hasAtLeast3Messages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedHasPlentyOfMessages(_ hasPlentyOfMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedIsPremium(_ isPremium: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedPremiumGiftOptions(_ premiumGiftOptions: [CachedPremiumGiftOption]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedSuggestPremiumGift(_ suggestPremiumGift: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedForceInputCommandsHidden(_ forceInputCommandsHidden: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedVoiceMessagesAvailable(_ voiceMessagesAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedCustomEmojiAvailable(_ customEmojiAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedThreadData(_ threadData: ThreadData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedForumTopicData(_ forumTopicData: ThreadData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedIsGeneralThreadClosed(_ isGeneralThreadClosed: Bool?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedTranslationState(_ translationState: ChatPresentationTranslationState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedReplyMessage(_ replyMessage: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedAccountPeerColor(_ accountPeerColor: AccountPeerColor?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedSavedMessagesTopicPeer(_ savedMessagesTopicPeer: EnginePeer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedHasSearchTags(_ hasSearchTags: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedIsPremiumRequiredForMessaging(_ isPremiumRequiredForMessaging: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedSendPaidMessageStars(_ sendPaidMessageStars: StarsAmount?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedAcknowledgedPaidMessage(_ acknowledgedPaidMessage: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedHasSavedChats(_ hasSavedChats: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedAppliedBoosts(_ appliedBoosts: Int32?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedBoostsToUnrestrict(_ boostsToUnrestrict: Int32?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedBusinessIntro(_ businessIntro: TelegramBusinessIntro?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedHasBirthdayToday(_ hasBirthdayToday: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedAdMessage(_ adMessage: Message?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedPeerVerification(_ peerVerification: PeerVerification?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedStarGiftsAvailable(_ starGiftsAvailable: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedAlwaysShowGiftButton(_ alwaysShowGiftButton: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedDisallowedGifts(_ disallowedGifts: TelegramDisallowedGifts?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedPersistentData(_ persistentData: PersistentPeerData) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedRemovePaidMessageFeeData(_ removePaidMessageFeeData: RemovePaidMessageFeeData?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedViewForumAsMessages(_ viewForumAsMessages: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: viewForumAsMessages, hasTopics: self.hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: viewForumAsMessages, hasTopics: self.hasTopics) } public func updatedHasTopics(_ hasTopics: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: hasTopics) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, isManagedBot: self.isManagedBot, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, focusedPollAddOptionMessageId: self.focusedPollAddOptionMessageId, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, historyFilter: self.historyFilter, displayHistoryFilterAsList: self.displayHistoryFilterAsList, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, preferredGlassType: self.preferredGlassType, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, myCopyProtectionEnabled: self.myCopyProtectionEnabled, hasAtLeast3Messages: self.hasAtLeast3Messages, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, forumTopicData: self.forumTopicData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState, replyMessage: self.replyMessage, accountPeerColor: self.accountPeerColor, savedMessagesTopicPeer: self.savedMessagesTopicPeer, hasSearchTags: self.hasSearchTags, isPremiumRequiredForMessaging: self.isPremiumRequiredForMessaging, sendPaidMessageStars: self.sendPaidMessageStars, acknowledgedPaidMessage: self.acknowledgedPaidMessage, hasSavedChats: self.hasSavedChats, appliedBoosts: self.appliedBoosts, boostsToUnrestrict: self.boostsToUnrestrict, businessIntro: self.businessIntro, hasBirthdayToday: self.hasBirthdayToday, adMessage: self.adMessage, peerVerification: self.peerVerification, starGiftsAvailable: self.starGiftsAvailable, alwaysShowGiftButton: self.alwaysShowGiftButton, disallowedGifts: self.disallowedGifts, persistentData: self.persistentData, removePaidMessageFeeData: self.removePaidMessageFeeData, viewForumAsMessages: self.viewForumAsMessages, hasTopics: hasTopics) } } @@ -1579,7 +1572,7 @@ public func canBypassRestrictions(chatPresentationInterfaceState: ChatPresentati public func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bool { if let peer = state.renderedPeer?.peer { let canBypassRestrictions = canBypassRestrictions(chatPresentationInterfaceState: state) - if canSendMessagesToPeer(peer, ignoreDefault: canBypassRestrictions) { + if canSendMessagesToPeer(EnginePeer(peer), ignoreDefault: canBypassRestrictions) { return true } else { return false @@ -1598,3 +1591,22 @@ public func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bo return false } } + +public func canSendReactionsToChat(_ state: ChatPresentationInterfaceState) -> Bool { + if let peer = state.renderedPeer?.peer { + let canBypassRestrictions = canBypassRestrictions(chatPresentationInterfaceState: state) + return canSendReactionsToPeer(EnginePeer(peer), ignoreDefault: canBypassRestrictions) + } else if case .customChatContents = state.chatLocation { + if case let .customChatContents(contents) = state.subject { + if case .hashTagSearch = contents.kind { + return false + } else { + return true + } + } else { + return true + } + } else { + return false + } +} diff --git a/submodules/ChatSendMessageActionUI/BUILD b/submodules/ChatSendMessageActionUI/BUILD index 58b4cdab9e..d2d4bb1471 100644 --- a/submodules/ChatSendMessageActionUI/BUILD +++ b/submodules/ChatSendMessageActionUI/BUILD @@ -14,7 +14,6 @@ swift_library( "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", "//submodules/TelegramCore:TelegramCore", - "//submodules/Postbox", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/ContextUI:ContextUI", diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index 7e783df785..81b7efae46 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -7,7 +7,6 @@ import TelegramPresentationData import AccountContext import ContextUI import TelegramCore -import Postbox import TextFormat import ReactionSelectionNode import ViewControllerComponent @@ -970,7 +969,7 @@ final class ChatSendMessageContextScreenComponent: Component { }) } - var customEffectResource: (FileMediaReference, MediaResource)? + var customEffectResource: (FileMediaReference, TelegramMediaResource)? if let effectAnimation = messageEffect.effectAnimation?._parse() { customEffectResource = (FileMediaReference.standalone(media: effectAnimation), effectAnimation.resource) } else { @@ -988,7 +987,7 @@ final class ChatSendMessageContextScreenComponent: Component { loadEffectAnimationSignal = Signal { subscriber in let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: customEffectResourceFileReference, resource: customEffectResource).start() - let dataDisposabke = (context.account.postbox.mediaBox.resourceStatus(customEffectResource) + let dataDisposabke = (context.engine.resources.status(resource: EngineMediaResource(customEffectResource)) |> filter { status in if status == .Local { return true @@ -1051,7 +1050,7 @@ final class ChatSendMessageContextScreenComponent: Component { standaloneReactionAnimation.updateLayout(size: effectFrame.size) self.addSubnode(standaloneReactionAnimation) - let pathPrefix = component.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(customEffectResource.id) + let pathPrefix = component.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(customEffectResource.id)) let source = AnimatedStickerResourceSource(account: component.context.account, resource: customEffectResource, fitzModifier: nil) standaloneReactionAnimation.setup(source: source, width: Int(effectSize.width * effectiveScale), height: Int(effectSize.height * effectiveScale), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix)) standaloneReactionAnimation.completed = { [weak self, weak standaloneReactionAnimation] _ in diff --git a/submodules/ChatTextLinkEditUI/BUILD b/submodules/ChatTextLinkEditUI/BUILD index 39f5f7832e..be81825492 100644 --- a/submodules/ChatTextLinkEditUI/BUILD +++ b/submodules/ChatTextLinkEditUI/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/AccountContext", "//submodules/TelegramPresentationData", diff --git a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift index 4229f74605..87e357334b 100644 --- a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift +++ b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift @@ -92,5 +92,18 @@ public func chatTextLinkEditController( dismissImpl = { [weak alertController] in alertController?.dismiss(completion: nil) } + + if link == nil { + Queue.mainQueue().after(0.1, { + let pasteboard = UIPasteboard.general + if pasteboard.hasURLs { + if inputState.value.string.isEmpty, let url = pasteboard.url?.absoluteString, !url.isEmpty { + let value = NSAttributedString(string: url) + inputState.setValue(value, selectionRange: 0 ..< value.length) + } + } + }) + } + return alertController } diff --git a/submodules/CloudData/Sources/CloudData.swift b/submodules/CloudData/Sources/CloudData.swift index 12bb67f42b..94b898dcbe 100644 --- a/submodules/CloudData/Sources/CloudData.swift +++ b/submodules/CloudData/Sources/CloudData.swift @@ -12,9 +12,6 @@ private enum FetchError { @available(iOS 10.0, *) private func fetchRawData(prefix: String) -> Signal { return Signal { subscriber in - #if targetEnvironment(simulator) - return EmptyDisposable - #else let container = CKContainer.default() let publicDatabase = container.database(with: .public) let recordId = CKRecord.ID(recordName: "emergency-datacenter-\(prefix)") @@ -47,7 +44,6 @@ private func fetchRawData(prefix: String) -> Signal { return ActionDisposable { } - #endif } } diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 57b5f847e0..620dd7fdc8 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -67,6 +67,8 @@ private extension ComponentTransition.Animation.Curve { switch self { case .easeInOut: return CAMediaTimingFunction(name: .easeInEaseOut) + case .easeIn: + return CAMediaTimingFunction(name: .easeIn) case .linear: return CAMediaTimingFunction(name: .linear) case let .custom(a, b, c, d): @@ -82,6 +84,8 @@ private extension ComponentTransition.Animation.Curve { return [.curveLinear] case .easeInOut: return [.curveEaseInOut] + case .easeIn: + return [.curveEaseIn] case .spring: return UIView.AnimationOptions(rawValue: 7 << 16) case .custom: @@ -141,6 +145,7 @@ public struct ComponentTransition { public enum Animation { public enum Curve { case easeInOut + case easeIn case spring case linear case custom(Float, Float, Float, Float) @@ -150,6 +155,8 @@ public struct ComponentTransition { switch self { case .easeInOut: return listViewAnimationCurveEaseInOut(offset) + case .easeIn: + return listViewAnimationCurveEaseIn(offset) case .spring: return listViewAnimationCurveSystem(offset) case .linear: diff --git a/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift b/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift index 7e2e790e57..29ae9b147f 100644 --- a/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift +++ b/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift @@ -10,6 +10,8 @@ public extension ComponentTransition.Animation.Curve { self = .linear case .easeInOut: self = .easeInOut + case .easeIn: + self = .easeIn case let .custom(a, b, c, d): self = .custom(a, b, c, d) case .customSpring: @@ -25,6 +27,8 @@ public extension ComponentTransition.Animation.Curve { return .linear case .easeInOut: return .easeInOut + case .easeIn: + return .easeIn case .spring: return .spring case let .custom(a, b, c, d): diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index 4b11ff0e5d..5d480fd29f 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 @@ -1089,6 +1103,27 @@ public final class PagerComponent View { diff --git a/submodules/Components/ReactionButtonListComponent/BUILD b/submodules/Components/ReactionButtonListComponent/BUILD index 19425cd17d..c72042368a 100644 --- a/submodules/Components/ReactionButtonListComponent/BUILD +++ b/submodules/Components/ReactionButtonListComponent/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/Display:Display", "//submodules/ComponentFlow:ComponentFlow", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift index 9c4dc38f91..5d73c83e7c 100644 --- a/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift +++ b/submodules/Components/ReactionImageComponent/Sources/ReactionImageComponent.swift @@ -18,7 +18,7 @@ public let sharedReactionStaticImage = Queue(name: "SharedReactionStaticImage", public func reactionStaticImage(context: AccountContext, animation: TelegramMediaFile, pixelSize: CGSize, queue: Queue) -> Signal { return context.engine.resources.custom(id: "\(animation.resource.id.stringRepresentation):reaction-static-\(pixelSize.width)x\(pixelSize.height)-v10", fetch: EngineMediaResource.Fetch { return Signal { subscriber in - let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: MediaResourceReference.standalone(resource: animation.resource)).start() + let fetchDisposable = context.engine.resources.fetch(reference: MediaResourceReference.standalone(resource: animation.resource), userLocation: .other, userContentType: .image).start() var customColor: UIColor? if animation.isCustomTemplateEmoji { @@ -158,13 +158,13 @@ public final class ReactionImageNode: ASDisplayNode { super.init() - self.disposable = (context.account.postbox.mediaBox.resourceData(file.resource) + self.disposable = (context.engine.resources.data(resource: EngineMediaResource(file.resource)) |> deliverOnMainQueue).start(next: { [weak self] data in guard let strongSelf = self else { return } - - if data.complete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + + if data.isComplete, let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { if let image = WebP.convert(fromWebP: dataValue) { strongSelf.iconNode.image = image } diff --git a/submodules/Components/ReactionListContextMenuContent/BUILD b/submodules/Components/ReactionListContextMenuContent/BUILD index 08714ce9a7..d91f85c4ab 100644 --- a/submodules/Components/ReactionListContextMenuContent/BUILD +++ b/submodules/Components/ReactionListContextMenuContent/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/Display:Display", "//submodules/ComponentFlow:ComponentFlow", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", @@ -21,6 +20,7 @@ swift_library( "//submodules/AnimatedAvatarSetNode:AnimatedAvatarSetNode", "//submodules/ContextUI:ContextUI", "//submodules/AvatarNode:AvatarNode", + "//submodules/Components/MultilineTextComponent:MultilineTextComponent", "//submodules/Components/ReactionImageComponent:ReactionImageComponent", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", diff --git a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift index 42d00f8d85..980ca8e473 100644 --- a/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift +++ b/submodules/Components/ReactionListContextMenuContent/Sources/ReactionListContextMenuContent.swift @@ -2,6 +2,7 @@ import Foundation import AsyncDisplayKit import Display import ComponentFlow +import MultilineTextComponent import SwiftSignalKit import TelegramCore import AccountContext @@ -373,18 +374,26 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let availableReactions: AvailableReactions? let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer + private let highlightBackgroundView: UIView let avatarNode: AvatarNode let titleLabelNode: ImmediateTextNode let textLabelNode: ImmediateTextNode let readIconView: UIImageView var credibilityIconView: ComponentView? - + private var reactionLayer: InlineStickerItemLayer? private var iconFrame: CGRect? private var file: TelegramMediaFile? private var fileDisposable: Disposable? + private var longTapRecognizer: UILongPressGestureRecognizer? + private var skipNextTapAction = false let action: () -> Void + var longTapAction: (() -> Void)? { + didSet { + self.longTapRecognizer?.isEnabled = self.longTapAction != nil + } + } private var item: EngineMessageReactionListContext.Item? @@ -412,16 +421,41 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.readIconView = UIImageView(image: readIconImage) + self.highlightBackgroundView = UIView() + self.highlightBackgroundView.alpha = 0.0 + self.highlightBackgroundView.isUserInteractionEnabled = false + super.init() - + self.isAccessibilityElement = true - + + self.view.insertSubview(self.highlightBackgroundView, at: 0) + self.addSubnode(self.avatarNode) self.addSubnode(self.titleLabelNode) self.addSubnode(self.textLabelNode) self.view.addSubview(self.readIconView) self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + self.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.highlightBackgroundView.layer.removeAnimation(forKey: "opacity") + self.highlightBackgroundView.alpha = 0.1 + } else { + let currentAlpha = self.highlightBackgroundView.alpha + self.highlightBackgroundView.alpha = 0.0 + self.highlightBackgroundView.layer.animateAlpha(from: currentAlpha, to: 0.0, duration: 0.3) + } + } + + let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longTapGesture(_:))) + longTapRecognizer.isEnabled = false + longTapRecognizer.cancelsTouchesInView = true + self.view.addGestureRecognizer(longTapRecognizer) + self.longTapRecognizer = longTapRecognizer } deinit { @@ -429,9 +463,30 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } @objc private func pressed() { + if self.skipNextTapAction { + self.skipNextTapAction = false + return + } self.action() } + @objc private func longTapGesture(_ recognizer: UILongPressGestureRecognizer) { + switch recognizer.state { + case .began: + guard let item = self.item, item.reaction != nil else { + return + } + self.skipNextTapAction = true + self.longTapAction?() + case .cancelled, .ended, .failed: + DispatchQueue.main.async { [weak self] in + self?.skipNextTapAction = false + } + default: + break + } + } + private func updateReactionLayer() { guard let file = self.file else { return @@ -503,7 +558,12 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let sideInset: CGFloat = 16.0 let reaction: MessageReaction.Reaction? = item.reaction - + + self.highlightBackgroundView.backgroundColor = presentationData.theme.overallDarkAppearance ? UIColor.white : UIColor.black + self.highlightBackgroundView.setMonochromaticEffect(tintColor: self.highlightBackgroundView.backgroundColor) + self.highlightBackgroundView.frame = CGRect(origin: CGPoint(x: 10.0, y: 5.0), size: CGSize(width: max(0.0, size.width - 20.0), height: size.height - 10.0)) + self.highlightBackgroundView.layer.cornerRadius = self.highlightBackgroundView.frame.height * 0.5 + if self.displayReactionIcon, reaction != self.item?.reaction { if let reaction = reaction { switch reaction { @@ -771,10 +831,12 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private let requestUpdate: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void private let requestUpdateApparentHeight: (ReactionsTabNode, ContainedViewLayoutTransition) -> Void private let openPeer: (EnginePeer, Bool) -> Void + private let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)? private var hasMore: Bool = false private let scrollNode: ASScrollNode + private let separatorNode: ASDisplayNode private var ignoreScrolling: Bool = false private var animateIn: Bool = false private var bottomScrollInset: CGFloat = 0.0 @@ -791,7 +853,9 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private var placeholderItemImage: UIImage? private var placeholderLayers: [Int: SimpleLayer] = [:] - + + private let deleteReactionInfoText: ComponentView + init( context: AccountContext, displayReadTimestamps: Bool, @@ -804,7 +868,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent readStats: MessageReadStats?, requestUpdate: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void, requestUpdateApparentHeight: @escaping (ReactionsTabNode, ContainedViewLayoutTransition) -> Void, - openPeer: @escaping (EnginePeer, Bool) -> Void + openPeer: @escaping (EnginePeer, Bool) -> Void, + deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)? ) { self.context = context self.displayReadTimestamps = displayReadTimestamps @@ -816,11 +881,15 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.requestUpdate = requestUpdate self.requestUpdateApparentHeight = requestUpdateApparentHeight self.openPeer = openPeer + self.deleteReaction = deleteReaction self.listContext = context.engine.messages.messageReactionList(message: message, readStats: readStats, reaction: reaction) self.state = ItemsState(listState: EngineMessageReactionListContext.State(message: message, readStats: readStats, reaction: reaction), readStats: readStats) self.scrollNode = ASScrollNode() + self.separatorNode = ASDisplayNode() + self.deleteReactionInfoText = ComponentView() + self.scrollNode.canCancelAllTouchesInViews = true self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.showsVerticalScrollIndicator = false @@ -834,6 +903,10 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.addSubnode(self.scrollNode) self.scrollNode.view.delegate = self.wrappedScrollViewDelegate + self.separatorNode.isHidden = true + self.separatorNode.isUserInteractionEnabled = false + self.scrollNode.addSubnode(self.separatorNode) + self.clipsToBounds = true self.stateDisposable = (self.listContext.state @@ -928,6 +1001,14 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } itemNode.update(size: itemFrame.size, presentationData: presentationData, item: item, isLast: self.state.item(at: index + 1) == nil, syncronousLoad: syncronousLoad) + if let deleteReaction = self.deleteReaction, let reaction = item.reaction, item.peer.id != self.context.account.peerId { + let peer = item.peer + itemNode.longTapAction = { + deleteReaction(peer, reaction) + } + } else { + itemNode.longTapAction = nil + } itemNode.frame = itemFrame } else if index < self.state.totalCount { validPlaceholderIds.insert(index) @@ -1034,7 +1115,70 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent } self.presentationData = presentationData - let size = CGSize(width: constrainedSize.width, height: topInset + CGFloat(self.state.totalCount) * itemHeight + topInset) + let baseContentHeight = topInset + CGFloat(self.state.totalCount) * itemHeight + topInset + var footerHeight: CGFloat = 0.0 + let displayDeleteReactionInfoFooter: Bool + if self.deleteReaction != nil { + displayDeleteReactionInfoFooter = self.state.mergedItems.contains(where: { item in + return item.reaction != nil && item.peer.id != self.context.account.peerId + }) + } else { + displayDeleteReactionInfoFooter = false + } + + if displayDeleteReactionInfoFooter { + let separatorHeight: CGFloat = 20.0 + let horizontalInset: CGFloat = 18.0 + let textTopInset: CGFloat = 5.0 + let textBottomInset: CGFloat = 18.0 + let textFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0)) + + self.separatorNode.isHidden = false + self.separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor + + var animateIn = false + var footerTransition = transition + if self.deleteReactionInfoText.view?.superview == nil, footerTransition.isAnimated { + footerTransition = .immediate + animateIn = true + } + footerTransition.updateFrame(node: self.separatorNode, frame: CGRect( + origin: CGPoint(x: horizontalInset, y: baseContentHeight + floorToScreenPixels((separatorHeight - UIScreenPixel) * 0.5)), + size: CGSize(width: max(0.0, constrainedSize.width - horizontalInset * 2.0), height: 1.0) + )) + + let textSize = self.deleteReactionInfoText.update( + transition: ComponentTransition(footerTransition), + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: presentationData.strings.Chat_DeleteReactionInfo, + font: textFont, + textColor: presentationData.theme.contextMenu.primaryColor + )), + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: max(0.0, constrainedSize.width - horizontalInset * 2.0), height: .greatestFiniteMagnitude) + ) + if let textView = self.deleteReactionInfoText.view { + if textView.superview == nil { + self.scrollNode.view.addSubview(textView) + textView.isUserInteractionEnabled = false + } + footerTransition.updateFrame(view: textView, frame: CGRect(origin: CGPoint(x: horizontalInset, y: baseContentHeight + separatorHeight + textTopInset), size: textSize)) + + if animateIn { + self.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + textView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + + footerHeight = separatorHeight + textTopInset + textSize.height + textBottomInset + } else { + self.separatorNode.isHidden = true + self.deleteReactionInfoText.view?.removeFromSuperview() + } + let size = CGSize(width: constrainedSize.width, height: baseContentHeight + footerHeight) let containerSize = CGSize(width: size.width, height: min(constrainedSize.height, size.height)) self.currentSize = containerSize @@ -1079,6 +1223,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent private let reactions: [(MessageReaction.Reaction?, Int)] private let requestUpdate: (ContainedViewLayoutTransition) -> Void private let requestUpdateApparentHeight: (ContainedViewLayoutTransition) -> Void + private let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)? private var presentationData: PresentationData @@ -1111,7 +1256,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void, back: (() -> Void)?, - openPeer: @escaping (EnginePeer, Bool) -> Void + openPeer: @escaping (EnginePeer, Bool) -> Void, + deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)? ) { self.context = context self.displayReadTimestamps = displayReadTimestamps @@ -1126,6 +1272,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.requestUpdate = requestUpdate self.requestUpdateApparentHeight = requestUpdateApparentHeight + self.deleteReaction = deleteReaction if let back = back { self.backButtonNode = BackButtonNode() @@ -1332,7 +1479,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent strongSelf.requestUpdateApparentHeight(transition) } }, - openPeer: self.openPeer + openPeer: self.openPeer, + deleteReaction: self.deleteReaction ) self.addSubnode(tabNode) self.visibleTabNodes[index] = tabNode @@ -1426,6 +1574,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent let readStats: MessageReadStats? let back: (() -> Void)? let openPeer: (EnginePeer, Bool) -> Void + let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)? public init( context: AccountContext, @@ -1437,7 +1586,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent reaction: MessageReaction.Reaction?, readStats: MessageReadStats?, back: (() -> Void)?, - openPeer: @escaping (EnginePeer, Bool) -> Void + openPeer: @escaping (EnginePeer, Bool) -> Void, + deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)? = nil ) { self.context = context self.displayReadTimestamps = displayReadTimestamps @@ -1449,6 +1599,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent self.readStats = readStats self.back = back self.openPeer = openPeer + self.deleteReaction = deleteReaction } public func node( @@ -1467,7 +1618,8 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent requestUpdate: requestUpdate, requestUpdateApparentHeight: requestUpdateApparentHeight, back: self.back, - openPeer: self.openPeer + openPeer: self.openPeer, + deleteReaction: self.deleteReaction ) } } diff --git a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift index 852881033d..8437d60aab 100644 --- a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift +++ b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift @@ -13,7 +13,7 @@ public final class ResizableSheetComponentEnvironment: Equatable { public let bounds: CGRect public let isInteractive: Bool } - + public let theme: PresentationTheme public let statusBarHeight: CGFloat public let safeInsets: UIEdgeInsets @@ -26,7 +26,7 @@ public final class ResizableSheetComponentEnvironment: Equatable { public let regularMetricsSize: CGSize? public let dismiss: (Bool) -> Void public let boundsUpdated: ActionSlot - + public init( theme: PresentationTheme, statusBarHeight: CGFloat, @@ -54,7 +54,7 @@ public final class ResizableSheetComponentEnvironment: Equatable { self.dismiss = dismiss self.boundsUpdated = boundsUpdated } - + public static func ==(lhs: ResizableSheetComponentEnvironment, rhs: ResizableSheetComponentEnvironment) -> Bool { if lhs.theme != rhs.theme { return false @@ -92,19 +92,24 @@ public final class ResizableSheetComponentEnvironment: Equatable { public final class ResizableSheetComponent: Component { public typealias EnvironmentType = (ChildEnvironmentType, ResizableSheetComponentEnvironment) - + public class ExternalState { public fileprivate(set) var contentHeight: CGFloat - + fileprivate var trackedScrollViewUpdated: ((UIScrollView?) -> Void)? + public init() { self.contentHeight = 0.0 } + + public func setTrackedScrollView(_ scrollView: UIScrollView?) { + self.trackedScrollViewUpdated?(scrollView) + } } - + public enum BackgroundColor: Equatable { case color(UIColor) } - + public let content: AnyComponent public let titleItem: AnyComponent? public let leftItem: AnyComponent? @@ -113,9 +118,10 @@ public final class ResizableSheetComponent? public let backgroundColor: BackgroundColor public let isFullscreen: Bool + public let defaultHeight: CGFloat? public let externalState: ExternalState? public let animateOut: ActionSlot> - + public init( content: AnyComponent, titleItem: AnyComponent? = nil, @@ -125,6 +131,7 @@ public final class ResizableSheetComponent? = nil, backgroundColor: BackgroundColor, isFullscreen: Bool = false, + defaultHeight: CGFloat? = nil, externalState: ExternalState? = nil, animateOut: ActionSlot>, ) { @@ -136,10 +143,11 @@ public final class ResizableSheetComponent Bool { if lhs.content != rhs.content { return false @@ -165,12 +173,15 @@ public final class ResizableSheetComponent UIView? { return super.hitTest(point, with: event) } } - + public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView, UIGestureRecognizerDelegate { public final class Tag { public init() { } } - + public func matches(tag: Any) -> Bool { if let _ = tag as? Tag { return true @@ -210,6 +221,10 @@ public final class ResizableSheetComponent - + private var titleItemView: ComponentView? private var leftItemView: ComponentView? private var rightItemView: ComponentView? private var bottomItemView: ComponentView? - + private let backgroundHandleView: UIImageView - + private var ignoreScrolling: Bool = false private var isDismissingInteractively: Bool = false private var dismissTranslation: CGFloat = 0.0 private var dismissStartTranslation: CGFloat? private var dismissPanGesture: UIPanGestureRecognizer? - + private var component: ResizableSheetComponent? private weak var state: EmptyComponentState? private var isUpdating: Bool = false private var environment: ResizableSheetComponentEnvironment? private var itemLayout: ItemLayout? - + private var registeredExternalState: ExternalState? + private weak var trackedScrollView: UIScrollView? + private var trackedScrollViewWasAtTopOnGestureBegan = false + override init(frame: CGRect) { self.dimView = UIView() self.containerView = UIView() - + self.containerView.clipsToBounds = true self.containerView.layer.cornerRadius = 40.0 self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - + self.backgroundLayer = SimpleLayer() self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] self.backgroundLayer.cornerRadius = 40.0 - + self.backgroundHandleView = UIImageView() - + self.navigationBarContainer = SparseContainerView() self.bottomContainer = SparseContainerView() - + self.scrollView = ScrollView() - + self.scrollContentClippingView = SparseContainerView() self.scrollContentClippingView.clipsToBounds = true - + self.scrollContentView = UIView() - + self.topEdgeEffectView = EdgeEffectView() self.topEdgeEffectView.clipsToBounds = true self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] @@ -275,15 +293,16 @@ public final class ResizableSheetComponent UIView? { if !self.bounds.contains(point) { return nil @@ -331,7 +355,7 @@ public final class ResizableSheetComponent Bool { if gestureRecognizer === self.dismissPanGesture { let pan = gestureRecognizer as! UIPanGestureRecognizer @@ -352,22 +376,25 @@ public final class ResizableSheetComponent Bool { if gestureRecognizer === self.dismissPanGesture { if otherGestureRecognizer === self.scrollView.panGestureRecognizer { return true } + if otherGestureRecognizer === self.trackedScrollView?.panGestureRecognizer { + return true + } } return false } - + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.dismissAnimated() } } - + public func dismissAnimated() { guard let environment = self.environment else { return @@ -375,16 +402,16 @@ public final class ResizableSheetComponent threshold || velocityY > 1000.0 - + self.isDismissingInteractively = false self.scrollView.isScrollEnabled = !component.isFullscreen - + if shouldDismiss { let animateOffset = self.bounds.height - self.backgroundLayer.frame.minY let initialVelocity = animateOffset > 0.0 ? max(0.0, velocityY) / animateOffset : 0.0 @@ -448,73 +475,170 @@ public final class ResizableSheetComponent CGFloat { + return -scrollView.contentInset.top + } + + private func isSheetFullyExpanded(itemLayout: ItemLayout) -> Bool { + return itemLayout.topInset <= 0.5 || self.scrollView.contentOffset.y >= itemLayout.topInset - 0.5 + } + + private func pinTrackedScrollViewToTop(_ scrollView: UIScrollView) { + let topOffset = self.trackedScrollViewTopOffset(scrollView) + if abs(scrollView.contentOffset.y - topOffset) > 0.5 { + scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: topOffset) + } + } + + private func updateTrackedScrollViewLock() { + guard let component = self.component, let itemLayout = self.itemLayout, let trackedScrollView = self.trackedScrollView else { + return + } + trackedScrollView.isScrollEnabled = true + if component.isFullscreen { + return + } + if !self.isSheetFullyExpanded(itemLayout: itemLayout) || self.isDismissingInteractively { + self.pinTrackedScrollViewToTop(trackedScrollView) + } + } + + private func setTrackedScrollView(_ scrollView: UIScrollView?) { + if self.trackedScrollView === scrollView { + self.updateTrackedScrollViewLock() + return + } + + if let trackedScrollView = self.trackedScrollView { + trackedScrollView.panGestureRecognizer.removeTarget(self, action: #selector(self.trackedScrollViewPanGesture(_:))) + trackedScrollView.isScrollEnabled = true + } + + self.trackedScrollView = scrollView + + if let scrollView = scrollView { + scrollView.panGestureRecognizer.addTarget(self, action: #selector(self.trackedScrollViewPanGesture(_:))) + } + + self.trackedScrollViewWasAtTopOnGestureBegan = false + self.updateTrackedScrollViewLock() + } + + @objc private func trackedScrollViewPanGesture(_ recognizer: UIPanGestureRecognizer) { + guard let component = self.component, let itemLayout = self.itemLayout, let trackedScrollView = recognizer.view as? UIScrollView else { + return + } + guard !component.isFullscreen, itemLayout.topInset > 0.5 else { + return + } + + let topOffset = self.trackedScrollViewTopOffset(trackedScrollView) + let isAtTop = trackedScrollView.contentOffset.y <= topOffset + 8.0 + + switch recognizer.state { + case .began, .changed: + if recognizer.state == .began { + self.trackedScrollViewWasAtTopOnGestureBegan = isAtTop + } + let translation = recognizer.translation(in: trackedScrollView) + let currentSheetOffset = min(max(0.0, self.scrollView.contentOffset.y), itemLayout.topInset) + let shouldExpandSheet = self.trackedScrollViewWasAtTopOnGestureBegan && currentSheetOffset < itemLayout.topInset - 0.5 + + if translation.y < 0.0 && shouldExpandSheet { + let consumedOffset = min(itemLayout.topInset - currentSheetOffset, -translation.y) + if consumedOffset > 0.0 { + self.scrollView.contentOffset = CGPoint(x: self.scrollView.contentOffset.x, y: currentSheetOffset + consumedOffset) + self.pinTrackedScrollViewToTop(trackedScrollView) + recognizer.setTranslation(.zero, in: trackedScrollView) + } + } else if translation.y > 0.0 && isAtTop && currentSheetOffset > 0.5 { + let consumedOffset = min(currentSheetOffset, translation.y) + if consumedOffset > 0.0 { + self.scrollView.contentOffset = CGPoint(x: self.scrollView.contentOffset.x, y: currentSheetOffset - consumedOffset) + self.pinTrackedScrollViewToTop(trackedScrollView) + recognizer.setTranslation(.zero, in: trackedScrollView) + } + } else if self.isDismissingInteractively || (translation.y > 0.0 && isAtTop && currentSheetOffset <= 0.5) { + self.pinTrackedScrollViewToTop(trackedScrollView) + } + case .ended, .cancelled, .failed: + self.trackedScrollViewWasAtTopOnGestureBegan = false + if !self.isSheetFullyExpanded(itemLayout: itemLayout) || self.isDismissingInteractively { + self.pinTrackedScrollViewToTop(trackedScrollView) + } + default: + break + } + } + private func updateScrolling(transition: ComponentTransition) { - guard let itemLayout = self.itemLayout, let component = self.component else { + guard let itemLayout = self.itemLayout, let component = self.component, let environment = self.environment else { return } var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset topOffset = max(0.0, topOffset) transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) - + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) - + var topOffsetFraction = self.scrollView.bounds.minY / 100.0 topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) - - if component.isFullscreen { + + if component.isFullscreen || environment.inputHeight > 0.0 { topOffsetFraction = 1.0 } - #if DEBUG// && false - if "".isEmpty { - topOffsetFraction = 1.0 - } - #endif - +// #if DEBUG// && false +// if "".isEmpty { +// topOffsetFraction = 1.0 +// } +// #endif + let minScale: CGFloat = itemLayout.isTablet ? 1.0 : (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width let minScaledTranslation: CGFloat = itemLayout.isTablet ? 0.0 : (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0 let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius - + let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction) let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction - + var containerTransform = CATransform3DIdentity containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0) containerTransform = CATransform3DScale(containerTransform, scale, scale, scale) containerTransform = CATransform3DTranslate(containerTransform, 0.0, self.dismissTranslation, 0.0) transition.setTransform(view: self.containerView, transform: containerTransform) transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius) - + if component.isFullscreen { transition.setBounds(view: self.scrollView, bounds: CGRect(origin: .zero, size: self.scrollView.bounds.size)) self.scrollView.isScrollEnabled = false } else { self.scrollView.isScrollEnabled = !self.isDismissingInteractively } - + var bounds = self.scrollView.bounds bounds.size.width = itemLayout.fillingSize self.environment?.boundsUpdated.invoke(ResizableSheetComponentEnvironment.BoundsUpdate(bounds: bounds, isInteractive: self.scrollView.isTracking)) + self.updateTrackedScrollViewLock() } - + private var didPlayAppearanceAnimation = false func animateIn() { self.didPlayAppearanceAnimation = true - + self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY self.containerView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } - + func animateOut(initialVelocity: CGFloat? = nil, completion: @escaping () -> Void) { let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - + self.dimView.layer.animateAlpha(from: self.dimView.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false) if let initialVelocity = initialVelocity { let transition = ContainedViewLayoutTransition.animated(duration: 0.35, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) - + transition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.layer.position.x, y: self.containerView.layer.position.y + animateOffset), completion: { _ in completion() }) @@ -525,25 +649,26 @@ public final class ResizableSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } - + let sheetEnvironment = environment[ResizableSheetComponentEnvironment.self].value component.animateOut.connect { [weak self] completion in guard let self else { return } + self.endEditing(true) self.animateOut { completion(Void()) } } - + let resetScrolling = self.scrollView.bounds.width != availableSize.width - + let fillingSize: CGFloat if case .regular = sheetEnvironment.metrics.widthClass { fillingSize = min(availableSize.width, 414.0) - sheetEnvironment.safeInsets.left * 2.0 @@ -555,18 +680,30 @@ public final class ResizableSheetComponent 0.0 { + initialContentHeight = contentHeight + } else if let defaultHeight = component.defaultHeight { + initialContentHeight = min(contentHeight, max(0.0, defaultHeight)) + } else { + initialContentHeight = contentHeight + } + let edgeEffectHeight: CGFloat = 80.0 let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight)) transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame) @@ -615,7 +759,7 @@ public final class ResizableSheetComponent if let current = self.titleItemView { @@ -624,12 +768,12 @@ public final class ResizableSheetComponent() self.titleItemView = titleItemView } - + let titleItemSize = titleItemView.update( transition: transition, component: titleItem, environment: {}, - containerSize: CGSize(width: containerSize.width - 66.0 * 2.0, height: 66.0) + containerSize: CGSize(width: containerSize.width - 72.0 * 2.0, height: 66.0) ) let titleItemFrame = CGRect(origin: CGPoint(x: rawSideInset + floorToScreenPixels((containerSize.width - titleItemSize.width)) / 2.0, y: floorToScreenPixels(38.0 - titleItemSize.height * 0.5)), size: titleItemSize) if let view = titleItemView.view { @@ -642,7 +786,7 @@ public final class ResizableSheetComponent @@ -653,7 +797,7 @@ public final class ResizableSheetComponent() self.leftItemView = leftItemView } - + let leftItemSize = leftItemView.update( transition: leftItemTransition, component: leftItem, @@ -664,7 +808,7 @@ public final class ResizableSheetComponent @@ -694,7 +838,7 @@ public final class ResizableSheetComponent() self.rightItemView = rightItemView } - + let rightItemSize = rightItemView.update( transition: rightItemTransition, component: rightItem, @@ -705,7 +849,7 @@ public final class ResizableSheetComponent 0.0 { bottomInsets.left = 16.0 bottomInsets.right = 16.0 bottomInsets.bottom = sheetEnvironment.inputHeight + 8.0 } - + + var bottomEdgeEffectHeight = edgeEffectHeight if let bottomItem = component.bottomItem { var bottomItemTransition = transition let bottomItemView: ComponentView @@ -742,7 +887,7 @@ public final class ResizableSheetComponent() self.bottomItemView = bottomItemView } - + let bottomItemSize = bottomItemView.update( transition: bottomItemTransition, component: bottomItem, @@ -753,7 +898,7 @@ public final class ResizableSheetComponent View { return View(frame: CGRect()) } - + public 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) } diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index fc9c4aaadf..33844f7452 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -403,7 +403,8 @@ public final class SheetComponent: C if availableSize.width < availableSize.height { glassInset = 6.0 } - bottomCornerRadius = sheetEnvironment.deviceMetrics.screenCornerRadius - glassInset + let containerCornerRadius = max(24.0, sheetEnvironment.deviceMetrics.screenCornerRadius) + bottomCornerRadius = containerCornerRadius - 2.0 case .legacy: topCornerRadius = 12.0 bottomCornerRadius = 12.0 diff --git a/submodules/ContactListUI/Sources/ContactContextMenus.swift b/submodules/ContactListUI/Sources/ContactContextMenus.swift index 2a04b9af4c..290b996321 100644 --- a/submodules/ContactListUI/Sources/ContactContextMenus.swift +++ b/submodules/ContactListUI/Sources/ContactContextMenus.swift @@ -39,7 +39,7 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { return } (contactsController?.navigationController as? NavigationController)?.pushViewController(controller) @@ -47,7 +47,7 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con }) }))) - let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: topSearchPeers) + let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: peer, peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: topSearchPeers) items.append(.action(ContextMenuActionItem(text: isMuted ? strings.StoryFeed_ContextNotifyOn : strings.StoryFeed_ContextNotifyOff, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 3c6ce36fbb..69e68d1fbe 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -4,7 +4,6 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore -import Postbox import TelegramPresentationData import TelegramUIPreferences import DeviceAccess @@ -180,11 +179,11 @@ private enum ContactListNodeEntry: Comparable, Identifiable { if isGlobal, let _ = peer.addressName { status = .addressName("") } else { - if let _ = peer as? TelegramUser { + if case .user = peer { status = .presence(presence ?? EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0), dateTimeFormat) - } else if let group = peer as? TelegramGroup { + } else if case let .legacyGroup(group) = peer { status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(Int32(group.participantCount))), multiline: false, isActive: false, icon: nil) - } else if let channel = peer as? TelegramChannel { + } else if case let .channel(channel) = peer { if case .group = channel.info { if let participantCount = participantCount, participantCount != 0 { status = .custom(string: NSAttributedString(string: strings.Conversation_StatusMembers(participantCount)), multiline: false, isActive: false, icon: nil) @@ -202,7 +201,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { status = .none } } - itemPeer = .peer(peer: EnginePeer(peer), chatPeer: EnginePeer(peer)) + itemPeer = .peer(peer: peer, chatPeer: peer) case let .deviceContact(id, contact): status = .none itemPeer = .deviceContact(stableId: id, contact: contact) @@ -250,7 +249,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable { interaction.openPeer(peer, .generic, nil, nil) }, disabledAction: { _ in if case let .peer(peer, _, _) = peer { - interaction.openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic) + interaction.openDisabledPeer(peer, requiresPremiumForMessaging ? .premiumRequired : .generic) } }, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction, storyStats: nil, openStories: { peer, sourceNode in if case let .peer(peerValue, _) = peer, let peerValue { @@ -436,7 +435,7 @@ private func contactListNodeEntries( displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set, peerRequiresPremiumForMessaging: [EnginePeer.Id: Bool], - peersWithStories: [EnginePeer.Id: PeerStoryStats], + peersWithStories: [EnginePeer.Id: EngineChatList.StoryStats], authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, @@ -515,7 +514,7 @@ private func contactListNodeEntries( } case let .natural(options, _, _): let sortedPeers = peers.sorted(by: { lhs, rhs in - let result = EnginePeer.IndexName(lhs.indexName).isLessThan(other: EnginePeer.IndexName(rhs.indexName), ordering: sortOrder) + let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: sortOrder) if result == .orderedSame { if case let .peer(lhsPeer, _, _) = lhs, case let .peer(rhsPeer, _, _) = rhs { return lhsPeer.id < rhsPeer.id @@ -630,7 +629,7 @@ private func contactListNodeEntries( } let presence = presences[peer.id] - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, nil)) + entries.append(.peer(index, .peer(peer: peer, isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, nil)) index += 1 } @@ -688,7 +687,7 @@ private func contactListNodeEntries( } let presence = presences[peer.id] - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, hasActions, true, nil, false, nil)) + entries.append(.peer(index, .peer(peer: peer, isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, hasActions, true, nil, false, nil)) index += 1 } @@ -699,7 +698,7 @@ private func contactListNodeEntries( if showSelf, let accountPeer { if let peer = topPeers.first(where: { $0.id == accountPeer.id }) { let header = ChatListSearchItemHeader(type: .text(strings.Premium_Gift_ContactSelection_ThisIsYou.uppercased(), AnyHashable(10)), theme: theme, strings: strings) - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, selfSubtitle)) + entries.append(.peer(index, .peer(peer: peer, isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, nil, false, selfSubtitle)) existingPeerIds.insert(.peer(peer.id)) } } @@ -745,7 +744,7 @@ private func contactListNodeEntries( } let presence = presences[peer.id] - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, peersWithStories[peer.id].flatMap { + entries.append(.peer(index, .peer(peer: peer, isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, false, true, peersWithStories[peer.id].flatMap { ContactListNodeEntry.StoryData(count: $0.totalCount, unseenCount: $0.unseenCount, hasUnseenCloseFriends: $0.hasUnseenCloseFriends) }, false, nil)) @@ -763,7 +762,7 @@ private func contactListNodeEntries( let header: ListViewItemHeader? = ChatListSearchItemHeader(type: .text("HIDDEN STORIES", AnyHashable(0)), theme: theme, strings: strings) for item in storySubscriptions.items { - entries.append(.peer(index, .peer(peer: item.peer._asPeer(), isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, ContactListNodeEntry.StoryData(count: item.storyCount, unseenCount: item.unseenCount, hasUnseenCloseFriends: item.hasUnseenCloseFriends))) + entries.append(.peer(index, .peer(peer: item.peer, isGlobal: false, participantCount: nil), nil, header, .none, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, ContactListNodeEntry.StoryData(count: item.storyCount, unseenCount: item.unseenCount, hasUnseenCloseFriends: item.hasUnseenCloseFriends))) index += 1 }*/ } @@ -842,7 +841,7 @@ private func contactListNodeEntries( enabled = false } - if let isPeerEnabled, !isPeerEnabled(EnginePeer(peer)) { + if let isPeerEnabled, !isPeerEnabled(peer) { enabled = false } default: @@ -1032,7 +1031,7 @@ public final class ContactListNode: ASDisplayNode { } private var didSetReady = false - private let contactPeersViewPromise = Promise<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: PeerStoryStats])>() + private let contactPeersViewPromise = Promise<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: EngineChatList.StoryStats])>() let storySubscriptions = Promise(nil) private let selectionStatePromise = Promise(nil) @@ -1115,7 +1114,7 @@ public final class ContactListNode: ASDisplayNode { contactsWithPremiumRequired = .single([:]) } - let contactsWithStories: Signal<[EnginePeer.Id: PeerStoryStats], NoError> = self.context.engine.data.subscribe( + let contactsWithStories: Signal<[EnginePeer.Id: EngineChatList.StoryStats], NoError> = self.context.engine.data.subscribe( TelegramEngine.EngineData.Item.Contacts.List(includePresences: false) ) |> map { contacts -> Set in @@ -1126,14 +1125,14 @@ public final class ContactListNode: ASDisplayNode { return result } |> distinctUntilChanged - |> mapToSignal { peerIds -> Signal<[EnginePeer.Id: PeerStoryStats], NoError> in + |> mapToSignal { peerIds -> Signal<[EnginePeer.Id: EngineChatList.StoryStats], NoError> in return context.engine.data.subscribe( EngineDataMap( peerIds.map(TelegramEngine.EngineData.Item.Peer.StoryStats.init(id:)) ) ) - |> map { result -> [EnginePeer.Id: PeerStoryStats] in - var filtered: [EnginePeer.Id: PeerStoryStats] = [:] + |> map { result -> [EnginePeer.Id: EngineChatList.StoryStats] in + var filtered: [EnginePeer.Id: EngineChatList.StoryStats] = [:] for (id, value) in result { if let value { filtered[id] = value @@ -1152,7 +1151,7 @@ public final class ContactListNode: ASDisplayNode { contactsWithPremiumRequired, contactsWithStories ) - |> mapToThrottled { next, contactsWithPremiumRequired, contactsWithStories -> Signal<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: PeerStoryStats]), NoError> in + |> mapToThrottled { next, contactsWithPremiumRequired, contactsWithStories -> Signal<(EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: EngineChatList.StoryStats]), NoError> in return .single((next.0, next.1, contactsWithPremiumRequired, contactsWithStories)) |> then( .complete() @@ -1165,7 +1164,7 @@ public final class ContactListNode: ASDisplayNode { TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.engine.account.peerId) ), contactsWithPremiumRequired, contactsWithStories) - |> map { next, contactsWithPremiumRequired, contactsWithStories -> (EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: PeerStoryStats]) in + |> map { next, contactsWithPremiumRequired, contactsWithStories -> (EngineContactList, EnginePeer?, [EnginePeer.Id: Bool], [EnginePeer.Id: EngineChatList.StoryStats]) in return (next.0, next.1, contactsWithPremiumRequired, contactsWithStories) } |> take(1)) @@ -1249,9 +1248,8 @@ public final class ContactListNode: ASDisplayNode { let contactsWarningSuppressed = Promise<(Bool, Bool)>() contactsWarningSuppressed.set(.single((false, false)) |> then( - combineLatest(context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.permissionWarningKey(permission: .contacts)!), context.account.postbox.preferencesView(keys: [PreferencesKeys.contactsSettings])) - |> map { noticeView, preferences -> (Bool, Bool) in - let settings: ContactsSettings = preferences.values[PreferencesKeys.contactsSettings]?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings + combineLatest(context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.permissionWarningKey(permission: .contacts)!), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ContactsSettings())) + |> map { noticeView, settings -> (Bool, Bool) in let synchronizeDeviceContacts: Bool = settings.synchronizeContacts let suppressed: Bool let timestamp = noticeView.value.flatMap({ ApplicationSpecificNotice.getTimestampValue($0) }) @@ -1364,7 +1362,7 @@ public final class ContactListNode: ASDisplayNode { state = state.withToggledPeerId(.peer(peer.id)) } if value { - selectedPeerMap[id] = .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil) + selectedPeerMap[id] = .peer(peer: peer, isGlobal: false, participantCount: nil) } else { selectedPeerMap.removeValue(forKey: id) } @@ -1448,23 +1446,23 @@ public final class ContactListNode: ASDisplayNode { |> mapToSignal { query in let foundLocalContacts: Signal<([FoundPeer], [EnginePeer.Id: EnginePeer.Presence]), NoError> if searchChatList { - let foundChatListPeers = context.account.postbox.searchPeers(query: query.lowercased()) + let foundChatListPeers = context.engine.contacts.searchLocalPeers(query: query.lowercased()) foundLocalContacts = foundChatListPeers |> mapToSignal { peers -> Signal<([FoundPeer], [EnginePeer.Id: EnginePeer.Presence]), NoError> in var resultPeers: [FoundPeer] = [] - + for peer in peers { if !displaySavedMessages { if peer.peerId == context.account.peerId { continue } } - + if searchGroups || searchChannels { let mainPeer = peer.chatMainPeer - if let _ = mainPeer as? TelegramUser { - } else if let _ = mainPeer as? TelegramGroup { - } else if let channel = mainPeer as? TelegramChannel { + if case .user = mainPeer { + } else if case .legacyGroup = mainPeer { + } else if case let .channel(channel) = mainPeer { if case .broadcast = channel.info { if !searchChannels { continue @@ -1481,7 +1479,7 @@ public final class ContactListNode: ASDisplayNode { if let mainPeer = peer.chatMainPeer { var matches = true if let isPeerEnabled = isPeerEnabled { - matches = isPeerEnabled(EnginePeer(mainPeer)) + matches = isPeerEnabled(mainPeer) } if matches { resultPeers.append(FoundPeer(peer: mainPeer, subscribers: nil)) @@ -1500,7 +1498,7 @@ public final class ContactListNode: ASDisplayNode { if let maybePresence = presenceMap[peer.peer.id], let presence = maybePresence { resultPresences[peer.peer.id] = presence } - if let _ = peer.peer as? TelegramChannel { + if case .channel = peer.peer { var subscribers: Int32? if let maybeMemberCount = participantCountMap[peer.peer.id], let memberCount = maybeMemberCount { subscribers = Int32(memberCount) @@ -1516,7 +1514,7 @@ public final class ContactListNode: ASDisplayNode { } else { foundLocalContacts = context.engine.contacts.searchContacts(query: query.lowercased()) |> map { peers, presences -> ([FoundPeer], [EnginePeer.Id: EnginePeer.Presence]) in - return (peers.map({ FoundPeer(peer: $0._asPeer(), subscribers: nil) }), presences) + return (peers.map({ FoundPeer(peer: $0, subscribers: nil) }), presences) } } var foundRemoteContacts: Signal<([FoundPeer], [FoundPeer]), NoError> = .single(([], [])) @@ -1535,7 +1533,7 @@ public final class ContactListNode: ASDisplayNode { foundDeviceContacts = .single([:]) } - let accountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) + let accountPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> take(1) struct FoundPeers { @@ -1562,18 +1560,18 @@ public final class ContactListNode: ASDisplayNode { var result = Set() for peer in foundPeers.foundLocalContacts.0 { - if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) { + if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) { result.insert(user.id) } } for peer in foundPeers.foundRemoteContacts.0 { - if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) { + if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) { result.insert(user.id) } } for peer in foundPeers.foundRemoteContacts.1 { - if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) { + if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) { result.insert(user.id) } } @@ -1642,7 +1640,7 @@ public final class ContactListNode: ASDisplayNode { } } - if !excludeSelf && !existingPeerIds.contains(accountPeer.id) { + if let accountPeer = accountPeer, !excludeSelf && !existingPeerIds.contains(accountPeer.id) { let lowercasedQuery = query.lowercased() if presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery) { existingPeerIds.insert(accountPeer.id) @@ -1657,14 +1655,14 @@ public final class ContactListNode: ASDisplayNode { existingPeerIds.insert(peer.peer.id) peers.append(.peer(peer: peer.peer, isGlobal: false, participantCount: peer.subscribers)) if searchDeviceContacts, - let user = peer.peer as? TelegramUser, + case let .user(user) = peer.peer, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } } for peer in remotePeers.0 { let matches: Bool - if let user = peer.peer as? TelegramUser { + if case let .user(user) = peer.peer { let phone = user.phone ?? "" if requirePhoneNumbers && phone.isEmpty { matches = false @@ -1672,9 +1670,9 @@ public final class ContactListNode: ASDisplayNode { matches = true } } else if searchGroups || searchChannels { - if peer.peer is TelegramGroup && searchGroups { + if case .legacyGroup = peer.peer, searchGroups { matches = true - } else if let channel = peer.peer as? TelegramChannel { + } else if case let .channel(channel) = peer.peer { if case .group = channel.info { matches = searchGroups } else { @@ -1694,7 +1692,7 @@ public final class ContactListNode: ASDisplayNode { existingPeerIds.insert(peer.peer.id) peers.append(.peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers)) if searchDeviceContacts, - let user = peer.peer as? TelegramUser, + case let .user(user) = peer.peer, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } @@ -1702,7 +1700,7 @@ public final class ContactListNode: ASDisplayNode { } for peer in remotePeers.1 { let matches: Bool - if let user = peer.peer as? TelegramUser { + if case let .user(user) = peer.peer { let phone = user.phone ?? "" if requirePhoneNumbers && phone.isEmpty { matches = false @@ -1710,9 +1708,9 @@ public final class ContactListNode: ASDisplayNode { matches = true } } else if searchGroups || searchChannels { - if peer.peer is TelegramGroup { + if case .legacyGroup = peer.peer { matches = searchGroups - } else if let channel = peer.peer as? TelegramChannel { + } else if case let .channel(channel) = peer.peer { if case .group = channel.info { matches = searchGroups } else { @@ -1732,7 +1730,7 @@ public final class ContactListNode: ASDisplayNode { existingPeerIds.insert(peer.peer.id) peers.append(.peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers)) if searchDeviceContacts, - let user = peer.peer as? TelegramUser, + case let .user(user) = peer.peer, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } @@ -1941,9 +1939,9 @@ public final class ContactListNode: ASDisplayNode { context.account.viewTracker.refreshCanSendMessagesForPeerIds(peerIds: Array(view.2.keys)) } - var peers = view.0.peers.map({ ContactListPeer.peer(peer: $0._asPeer(), isGlobal: false, participantCount: nil) }) + var peers = view.0.peers.map({ ContactListPeer.peer(peer: $0, isGlobal: false, participantCount: nil) }) for (peer, memberCount) in chatListPeers { - peers.append(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: memberCount)) + peers.append(.peer(peer: peer, isGlobal: false, participantCount: memberCount)) } var existingPeerIds = Set() var disabledPeerIds = Set() @@ -1967,7 +1965,7 @@ public final class ContactListNode: ASDisplayNode { switch contact { case let .peer(peer, _, _): if requirePhoneNumbers, - let user = peer as? TelegramUser { + case let .user(user) = peer { let phone = user.phone ?? "" if phone.isEmpty { return false diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index be63c37d30..dfee64ae07 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -192,9 +192,9 @@ public class ContactsController: ViewController { }).strict() if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - self.authorizationDisposable = (combineLatest(DeviceAccess.authorizationStatus(subject: .contacts), combineLatest(context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.permissionWarningKey(permission: .contacts)!), context.account.postbox.preferencesView(keys: [PreferencesKeys.contactsSettings]), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings])) + self.authorizationDisposable = (combineLatest(DeviceAccess.authorizationStatus(subject: .contacts), combineLatest(context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.permissionWarningKey(permission: .contacts)!), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.contactsSettings)), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings])) |> map { noticeView, preferences, sharedData -> (Bool, ContactsSortOrder) in - let settings: ContactsSettings = preferences.values[PreferencesKeys.contactsSettings]?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings + let settings: ContactsSettings = preferences?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings let synchronizeDeviceContacts: Bool = settings.synchronizeContacts let contactsSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]?.get(ContactSynchronizationSettings.self) @@ -291,7 +291,7 @@ public class ContactsController: ViewController { scrollToEndIfExists = true } - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), purposefulAction: { [weak self] in + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), purposefulAction: { [weak self] in if fromSearch { self?.deactivateSearch(animated: false) self?.switchToChatsController?() @@ -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) @@ -437,7 +437,7 @@ public class ContactsController: ViewController { strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) default: let presentationData = strongSelf.presentationData - strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { self?.context.sharedContext.applicationBindings.openSettings() })]), in: .window(.root)) strongSelf.contactsNode.contactListNode.listNode.clearHighlightAnimated(true) @@ -465,7 +465,14 @@ public class ContactsController: ViewController { let controller = QrCodeScanScreen(context: strongSelf.context, subject: .peer) controller.showMyCode = { [weak self, weak controller] in if let strongSelf = self { - let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId) + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in if let strongSelf = self, let controller = controller { controller.present(strongSelf.context.sharedContext.makeChatQrCodeScreen(context: strongSelf.context, peer: peer, threadId: nil, temporary: false), in: .window(.root)) @@ -757,6 +764,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 @@ -765,7 +774,7 @@ public class ContactsController: ViewController { } if let peer { Queue.mainQueue().async { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController { navigationController.pushViewController(infoController) } @@ -785,7 +794,7 @@ public class ContactsController: ViewController { default: let presentationData = strongSelf.presentationData if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController, let topController = navigationController.topViewController as? ViewController { - topController.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + topController.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { self?.context.sharedContext.applicationBindings.openSettings() })]), in: .window(.root)) } diff --git a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift index 01e8689d63..d3b7db7a98 100644 --- a/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsSearchContainerNode.swift @@ -161,8 +161,8 @@ private enum ContactListSearchEntry: Comparable, Identifiable { let peerItem: ContactsPeerItemPeer switch peer { case let .peer(peer, _, _): - peerItem = .peer(peer: EnginePeer(peer), chatPeer: EnginePeer(peer)) - nativePeer = EnginePeer(peer) + peerItem = .peer(peer: peer, chatPeer: peer) + nativePeer = peer case let .deviceContact(stableId, contact): peerItem = .deviceContact(stableId: stableId, contact: contact) } @@ -178,7 +178,7 @@ private enum ContactListSearchEntry: Comparable, Identifiable { openPeer(peer, .generic) }, disabledAction: { _ in if case let .peer(peer, _, _) = peer { - openDisabledPeer(EnginePeer(peer), requiresPremiumForMessaging ? .premiumRequired : .generic) + openDisabledPeer(peer, requiresPremiumForMessaging ? .premiumRequired : .generic) } }, contextAction: contextAction.flatMap { contextAction in return nativePeer.flatMap { nativePeer in @@ -404,12 +404,12 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo if let foundRemoteContacts = foundPeers.foundRemoteContacts { for peer in foundRemoteContacts.0 { - if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) { + if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) { result.insert(user.id) } } for peer in foundRemoteContacts.1 { - if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) { + if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) { result.insert(user.id) } } @@ -485,13 +485,13 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo var enabled = true var requiresPremiumForMessaging = false if onlyWriteable { - enabled = canSendMessagesToPeer(peer._asPeer()) + enabled = canSendMessagesToPeer(peer) if let value = peerRequiresPremiumForMessaging[peer.id], value { requiresPremiumForMessaging = true enabled = false } } - entries.append(.peer(index: index, theme: themeAndStrings.0, strings: themeAndStrings.1, peer: .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence: localPeersAndPresences.1[peer.id], group: .contacts, enabled: enabled, requiresPremiumForMessaging: requiresPremiumForMessaging, displayCallIcons: displayCallIcons)) + entries.append(.peer(index: index, theme: themeAndStrings.0, strings: themeAndStrings.1, peer: .peer(peer: peer, isGlobal: false, participantCount: nil), presence: localPeersAndPresences.1[peer.id], group: .contacts, enabled: enabled, requiresPremiumForMessaging: requiresPremiumForMessaging, displayCallIcons: displayCallIcons)) if searchDeviceContacts, case let .user(user) = peer, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } @@ -499,14 +499,13 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo } if let remotePeers = remotePeers { for peer in remotePeers.0 { - if !(peer.peer is TelegramUser) { - if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info, categories.contains(.channels) { - } else { - continue - } + if case .user = peer.peer { + } else if case let .channel(channel) = peer.peer, case .broadcast = channel.info, categories.contains(.channels) { + } else { + continue } - if let user = peer.peer as? TelegramUser { + if case let .user(user) = peer.peer { if requirePhoneNumbers { let phone = user.phone ?? "" if phone.isEmpty { @@ -517,12 +516,12 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo if user.botInfo != nil { continue } - } + } } - + if !existingPeerIds.contains(peer.peer.id) { existingPeerIds.insert(peer.peer.id) - + var enabled = true var requiresPremiumForMessaging = false if onlyWriteable { @@ -532,32 +531,31 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo enabled = false } } - + entries.append(.peer(index: index, theme: themeAndStrings.0, strings: themeAndStrings.1, peer: .peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers), presence: nil, group: .global, enabled: enabled, requiresPremiumForMessaging: requiresPremiumForMessaging, displayCallIcons: displayCallIcons)) - if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone { + if searchDeviceContacts, case let .user(user) = peer.peer, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } index += 1 } } for peer in remotePeers.1 { - if !(peer.peer is TelegramUser) { - if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info, categories.contains(.channels) { - } else { - continue - } + if case .user = peer.peer { + } else if case let .channel(channel) = peer.peer, case .broadcast = channel.info, categories.contains(.channels) { + } else { + continue } - - if let user = peer.peer as? TelegramUser, requirePhoneNumbers { + + if case let .user(user) = peer.peer, requirePhoneNumbers { let phone = user.phone ?? "" if phone.isEmpty { continue } } - + if !existingPeerIds.contains(peer.peer.id) { existingPeerIds.insert(peer.peer.id) - + var enabled = true var requiresPremiumForMessaging = false if onlyWriteable { @@ -567,9 +565,9 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo enabled = false } } - + entries.append(.peer(index: index, theme: themeAndStrings.0, strings: themeAndStrings.1, peer: .peer(peer: peer.peer, isGlobal: true, participantCount: peer.subscribers), presence: nil, group: .global, enabled: enabled, requiresPremiumForMessaging: requiresPremiumForMessaging, displayCallIcons: displayCallIcons)) - if searchDeviceContacts, let user = peer.peer as? TelegramUser, let phone = user.phone { + if searchDeviceContacts, case let .user(user) = peer.peer, let phone = user.phone { existingNormalizedPhoneNumbers.insert(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone))) } index += 1 diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index d46b90abf3..a6a17f1b19 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -475,6 +475,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { private var credibilityIconComponent: EmojiStatusComponent? private var verifiedIconView: ComponentHostView? private var verifiedIconComponent: EmojiStatusComponent? + private var emojiStatusIconView: ComponentHostView? + private var emojiStatusIconComponent: EmojiStatusComponent? public let statusNode: TextNodeWithEntities private var statusIconNode: ASImageNode? private var badgeBackgroundNode: ASImageNode? @@ -552,6 +554,14 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { containerSize: verifiedIconView.bounds.size ) } + if let emojiStatusIconView = self.emojiStatusIconView, let emojiStatusIconComponent = self.emojiStatusIconComponent { + let _ = emojiStatusIconView.update( + transition: .immediate, + component: AnyComponent(emojiStatusIconComponent.withVisibleForAnimations(self.visibilityStatus)), + environment: {}, + containerSize: emojiStatusIconView.bounds.size + ) + } if let avatarIconView = self.avatarIconView, let avatarIconComponent = self.avatarIconComponent { let _ = avatarIconView.update( transition: .immediate, @@ -840,27 +850,29 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 }) - var credibilityIcon: EmojiStatusComponent.Content? - var credibilityParticleColor: UIColor? + var credibilityStatusIcon: EmojiStatusComponent.Content? var verifiedIcon: EmojiStatusComponent.Content? + var emojiStatusIcon: EmojiStatusComponent.Content? + var emojiStatusParticleColor: UIColor? + switch item.peer { case let .peer(peer, _): if let peer = peer, (peer.id != item.context.account.peerId || item.peerMode == .memberList || item.aliasHandling == .standard) { if peer.isScam { - credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) + credibilityStatusIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_ScamAccount.uppercased()) } else if peer.isFake { - credibilityIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) + credibilityStatusIcon = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) } else if let emojiStatus = peer.emojiStatus, !item.isAd { - credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) + emojiStatusIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) if let color = emojiStatus.color { - credibilityParticleColor = UIColor(rgb: UInt32(bitPattern: color)) + emojiStatusParticleColor = UIColor(rgb: UInt32(bitPattern: color)) } } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { - credibilityIcon = .premium(color: item.presentationData.theme.list.itemAccentColor) + credibilityStatusIcon = .premium(color: item.presentationData.theme.list.itemAccentColor) } if peer.isVerified { - credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) + credibilityStatusIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) } if let verificationIconFileId = peer.verificationIconFileId { verifiedIcon = .animation(content: .customEmoji(fileId: verificationIconFileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(0)) @@ -1105,9 +1117,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { additionalTitleInset += 16.0 } } - if let credibilityIcon { + if let credibilityStatusIcon { additionalTitleInset += 3.0 - switch credibilityIcon { + switch credibilityStatusIcon { case let .text(_, string): let textString = NSAttributedString(string: string, font: Font.bold(10.0), textColor: .black, paragraphAlignment: .center) let stringRect = textString.boundingRect(with: CGSize(width: 100.0, height: 16.0), options: .usesLineFragmentOrigin, context: nil) @@ -1116,6 +1128,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { additionalTitleInset += 16.0 } } + if let _ = emojiStatusIcon { + additionalTitleInset += 3.0 + additionalTitleInset += 16.0 + } if let actionButtons = actionButtons { additionalTitleInset += 3.0 for actionButton in actionButtons { @@ -1587,7 +1603,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { } } - if let credibilityIcon { + if let credibilityStatusIcon { let animationCache = item.context.animationCache let animationRenderer = item.context.animationRenderer @@ -1604,8 +1620,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { context: item.context, animationCache: animationCache, animationRenderer: animationRenderer, - content: credibilityIcon, - particleColor: credibilityParticleColor, + content: credibilityStatusIcon, + particleColor: nil, isVisibleForAnimations: strongSelf.visibilityStatus, action: nil, emojiFileUpdated: nil @@ -1627,6 +1643,46 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { credibilityIconView.removeFromSuperview() } + if let emojiStatusIcon { + let animationCache = item.context.animationCache + let animationRenderer = item.context.animationRenderer + + let emojiStatusIconView: ComponentHostView + if let current = strongSelf.emojiStatusIconView { + emojiStatusIconView = current + } else { + emojiStatusIconView = ComponentHostView() + strongSelf.offsetContainerNode.view.addSubview(emojiStatusIconView) + strongSelf.emojiStatusIconView = emojiStatusIconView + } + + let emojiStatusIconComponent = EmojiStatusComponent( + context: item.context, + animationCache: animationCache, + animationRenderer: animationRenderer, + content: emojiStatusIcon, + particleColor: emojiStatusParticleColor, + isVisibleForAnimations: strongSelf.visibilityStatus, + action: nil, + emojiFileUpdated: nil + ) + strongSelf.emojiStatusIconComponent = emojiStatusIconComponent + + let iconSize = emojiStatusIconView.update( + transition: .immediate, + component: AnyComponent(emojiStatusIconComponent), + environment: {}, + containerSize: CGSize(width: 16.0, height: 16.0) + ) + + nextIconX += 4.0 + transition.updateFrame(view: emojiStatusIconView, frame: CGRect(origin: CGPoint(x: nextIconX, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0)), size: iconSize)) + nextIconX += iconSize.width + } else if let emojiStatusIconView = strongSelf.emojiStatusIconView { + strongSelf.emojiStatusIconView = nil + emojiStatusIconView.removeFromSuperview() + } + if let (titleBadgeLayout, titleBadgeApply) = titleBadgeLayoutAndApply { let titleBadgeNode = titleBadgeApply() let backgroundView: UIImageView diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 6959fa0430..9c6b20390a 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -602,6 +602,7 @@ public enum ContextControllerTip: Equatable { case starsReactions(topCount: Int) case videoProcessing case collageReordering + case deleteReaction public static func ==(lhs: ContextControllerTip, rhs: ContextControllerTip) -> Bool { switch lhs { @@ -611,6 +612,12 @@ public enum ContextControllerTip: Equatable { } else { return false } + case .deleteReaction: + if case .deleteReaction = rhs { + return true + } else { + return false + } case .quoteSelection: if case .quoteSelection = rhs { return true diff --git a/submodules/DebugSettingsUI/Sources/DebugAccountsController.swift b/submodules/DebugSettingsUI/Sources/DebugAccountsController.swift index cf3dfd46c0..13729fcd29 100644 --- a/submodules/DebugSettingsUI/Sources/DebugAccountsController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugAccountsController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import ItemListUI diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index d1bc366fcd..76a88cf387 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -98,6 +98,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case fakeGlass(Bool) case forceClearGlass(Bool) case debugRipple(Bool) + case debugRichText(Bool) case browserExperiment(Bool) case allForumsHaveTabs(Bool) case enableReactionOverrides(Bool) @@ -137,7 +138,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.web.rawValue case .keepChatNavigationStack, .skipReadHistory, .alwaysDisplayTyping, .debugRatingLayout, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .fakeGlass, .forceClearGlass, .debugRipple, .browserExperiment, .allForumsHaveTabs, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .enableUpdates, .pwa, .enableLocalTranslation: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .fakeGlass, .forceClearGlass, .debugRipple, .debugRichText, .browserExperiment, .allForumsHaveTabs, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .enableUpdates, .pwa, .enableLocalTranslation: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -234,44 +235,46 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 40 case .debugRipple: return 41 - case .browserExperiment: + case .debugRichText: return 42 - case .allForumsHaveTabs: + case .browserExperiment: return 43 - case .enableReactionOverrides: + case .allForumsHaveTabs: return 44 - case .restorePurchases: + case .enableReactionOverrides: return 45 - case .logTranslationRecognition: + case .restorePurchases: return 46 - case .resetTranslationStates: + case .logTranslationRecognition: return 47 - case .compressedEmojiCache: + case .resetTranslationStates: return 48 - case .storiesJpegExperiment: + case .compressedEmojiCache: return 49 - case .disableReloginTokens: + case .storiesJpegExperiment: return 50 - case .checkSerializedData: + case .disableReloginTokens: return 51 - case .enableQuickReactionSwitch: + case .checkSerializedData: return 52 - case .liveStreamV2: + case .enableQuickReactionSwitch: return 53 - case .experimentalCallMute: + case .liveStreamV2: return 54 - case .playerV2: + case .experimentalCallMute: return 55 - case .devRequests: + case .playerV2: return 56 - case .pwa: + case .devRequests: return 57 - case .enableLocalTranslation: + case .pwa: return 58 - case .enableUpdates: + case .enableLocalTranslation: return 59 + case .enableUpdates: + return 60 case let .preferredVideoCodec(index, _, _, _): - return 60 + index + return 61 + index case .disableVideoAspectScaling: return 100 case .enableNetworkFramework: @@ -358,7 +361,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) - context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(fileResource.id), data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) @@ -438,7 +441,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(logData.count), isSecretRelated: false) - context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: logData) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(fileResource.id), data: logData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(logData.count), attributes: [.FileName(fileName: "Log-iOS-Short.txt")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) @@ -524,7 +527,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) - context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(fileResource.id), data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) @@ -608,7 +611,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) - context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(fileResource.id), data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) @@ -693,7 +696,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) - context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(fileResource.id), data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) @@ -855,7 +858,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) - context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(fileResource.id), data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-All.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) @@ -910,7 +913,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(allStatsData.count), isSecretRelated: false) - context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: allStatsData) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(fileResource.id), data: allStatsData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/zip", size: Int64(allStatsData.count), attributes: [.FileName(fileName: "StorageReport.txt")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) @@ -1305,6 +1308,16 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) + case let .debugRichText(value): + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: "Debug Text", value: value, sectionId: self.section, style: .blocks, updated: { value in + let _ = arguments.sharedContext.accountManager.transaction ({ transaction in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in + var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings + settings.debugRichText = value + return PreferencesEntry(settings) + }) + }).start() + }) case let .browserExperiment(value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: "Inline UI", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in @@ -1583,6 +1596,7 @@ private func debugControllerEntries(context: AccountContext?, sharedContext: Sha entries.append(.fakeGlass(experimentalSettings.fakeGlass)) entries.append(.forceClearGlass(experimentalSettings.forceClearGlass)) entries.append(.debugRipple(experimentalSettings.debugRipple)) + entries.append(.debugRichText(experimentalSettings.debugRichText)) #if DEBUG entries.append(.browserExperiment(experimentalSettings.browserExperiment)) #else @@ -1667,10 +1681,9 @@ public func debugController(sharedContext: SharedAccountContext, context: Accoun hasLegacyAppData = FileManager.default.fileExists(atPath: statusPath) } - let preferencesSignal: Signal + let preferencesSignal: Signal if let context = context { - preferencesSignal = context.account.postbox.preferencesView(keys: [PreferencesKeys.networkSettings]) - |> map(Optional.init) + preferencesSignal = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.networkSettings)) } else { preferencesSignal = .single(nil) } @@ -1693,7 +1706,7 @@ public func debugController(sharedContext: SharedAccountContext, context: Accoun let experimentalSettings: ExperimentalUISettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings - let networkSettings: NetworkSettings? = preferences?.values[PreferencesKeys.networkSettings]?.get(NetworkSettings.self) + let networkSettings: NetworkSettings? = preferences?.get(NetworkSettings.self) var leftNavigationButton: ItemListNavigationButton? if modal { @@ -1779,7 +1792,7 @@ public func triggerDebugSendLogsUI(context: AccountContext, additionalInfo: Stri let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(gzippedData.count), isSecretRelated: false) - context.account.postbox.mediaBox.storeResourceData(fileResource.id, data: gzippedData) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(fileResource.id), data: gzippedData) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(gzippedData.count), attributes: [.FileName(fileName: "Log-iOS-Full.txt.zip")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) diff --git a/submodules/DeviceAccess/Sources/DeviceAccess.swift b/submodules/DeviceAccess/Sources/DeviceAccess.swift index 625bd10f41..5ac46798f5 100644 --- a/submodules/DeviceAccess/Sources/DeviceAccess.swift +++ b/submodules/DeviceAccess/Sources/DeviceAccess.swift @@ -336,7 +336,7 @@ public final class DeviceAccess { case .ageVerification: text = presentationData.strings.AccessDenied_AgeVerificationCamera } - present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } @@ -363,7 +363,7 @@ public final class DeviceAccess { text = presentationData.strings.AccessDenied_AgeVerificationCamera } } - present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } @@ -392,7 +392,7 @@ public final class DeviceAccess { case .voiceCall: text = presentationData.strings.AccessDenied_CallMicrophone } - present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) if case .voiceCall = microphoneSubject { @@ -421,7 +421,7 @@ public final class DeviceAccess { case .qrCode: text = presentationData.strings.AccessDenied_QrCode } - present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } @@ -462,7 +462,7 @@ public final class DeviceAccess { completion(false) if let presentationData = presentationData { let text = presentationData.strings.AccessDenied_LocationPreciseDenied - present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } @@ -477,7 +477,7 @@ public final class DeviceAccess { completion(false) if let presentationData = presentationData { let text = presentationData.strings.AccessDenied_LocationAlwaysDenied - present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } @@ -498,7 +498,7 @@ public final class DeviceAccess { } else { text = presentationData.strings.AccessDenied_LocationDisabled } - present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } @@ -558,7 +558,7 @@ public final class DeviceAccess { } case .cellularData: if let presentationData = presentationData { - present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.Permissions_CellularDataTitle_v0, text: presentationData.strings.Permissions_CellularDataText_v0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.Permissions_CellularDataTitle_v0, text: presentationData.strings.Permissions_CellularDataText_v0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { openSettings() })]), nil) } diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 30add28045..8ec45f3bb9 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -12,6 +12,7 @@ extension CGRect { public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable { case linear case easeInOut + case easeIn case spring case customSpring(damping: CGFloat, initialVelocity: CGFloat) case custom(Float, Float, Float, Float) @@ -28,6 +29,8 @@ public extension ContainedViewLayoutTransitionCurve { return offset case .easeInOut: return listViewAnimationCurveEaseInOut(offset) + case .easeIn: + return listViewAnimationCurveEaseIn(offset) case .spring: return listViewAnimationCurveSystem(offset) case .customSpring: @@ -45,6 +48,8 @@ public extension ContainedViewLayoutTransitionCurve { return CAMediaTimingFunctionName.linear.rawValue case .easeInOut: return CAMediaTimingFunctionName.easeInEaseOut.rawValue + case .easeIn: + return CAMediaTimingFunctionName.easeIn.rawValue case .spring: return kCAMediaTimingFunctionSpring case let .customSpring(damping, initialVelocity): @@ -60,6 +65,8 @@ public extension ContainedViewLayoutTransitionCurve { return nil case .easeInOut: return nil + case .easeIn: + return nil case .spring: return nil case .customSpring: @@ -75,6 +82,8 @@ public extension ContainedViewLayoutTransitionCurve { return [.curveLinear] case .easeInOut: return [.curveEaseInOut] + case .easeIn: + return [.curveEaseIn] case .spring: return UIView.AnimationOptions(rawValue: 7 << 16) case .customSpring: @@ -399,7 +408,7 @@ public extension ContainedViewLayoutTransition { } } - func updateBounds(layer: CALayer, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + func updateBounds(layer: CALayer, bounds: CGRect, beginWithCurrentState: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) { if layer.bounds.equalTo(bounds) && !force { completion?(true) } else { @@ -411,7 +420,12 @@ public extension ContainedViewLayoutTransition { completion(true) } case let .animated(duration, curve): - let previousBounds = layer.bounds + let previousBounds: CGRect + if beginWithCurrentState, layer.animation(forKey: "position") != nil, let presentation = layer.presentation() { + previousBounds = presentation.bounds + } else { + previousBounds = layer.bounds + } layer.bounds = bounds layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in if let completion = completion { diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index c22f8975d5..9380ecba8a 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -307,7 +307,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { var keyboardHeight = _keyboardHeight(inLandscape: inLandscape) if #available(iOS 26.0, *) { if !inLandscape { - keyboardHeight += 9.0 + keyboardHeight -= 1.0 } } return keyboardHeight diff --git a/submodules/Display/Source/Font.swift b/submodules/Display/Source/Font.swift index 9a93be6b1c..4f1a4c0cf8 100644 --- a/submodules/Display/Source/Font.swift +++ b/submodules/Display/Source/Font.swift @@ -224,7 +224,7 @@ public struct Font { } else { let font: UIFont switch design { - case .regular: + case .regular, .round: if traits.contains(.italic) { if let descriptor = UIFont.systemFont(ofSize: size, weight: weight.weight).fontDescriptor.withSymbolicTraits([.traitItalic]) { font = UIFont(descriptor: descriptor, size: size) @@ -254,8 +254,6 @@ public struct Font { } else { font = UIFont(name: "Menlo", size: size - 1.0) ?? UIFont.systemFont(ofSize: size) } - case .round: - font = UIFont(name: ".SFCompactRounded-Semibold", size: size) ?? UIFont.systemFont(ofSize: size) case .camera: func encodeText(string: String, key: Int16) -> String { let nsString = string as NSString diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index 0a78640967..e3e2dba497 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -212,6 +212,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur public final var rotated = false public final var experimentalSnapScrollToItem = false + public final var experimentalSnapScrollToPinnedItem = false public final var useMainQueueTransactions = false public final var scrollEnabled: Bool = true { @@ -863,6 +864,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur self.snapToBottomInsetUntilFirstInteraction = false } self.scrolledToItem = nil + self.experimentalSnapScrollToPinnedItem = false self.scroller.forceDecelerating = false self.isDragging = true @@ -1073,22 +1075,49 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur if remainingFactor.isLessThanOrEqualTo(0.0) { break } - + let itemFactor: CGFloat if CGFloat(1.0).isLessThanOrEqualTo(remainingFactor) { itemFactor = 1.0 } else { itemFactor = remainingFactor } - + additionalInverseTopInset += floor(itemNode.apparentBounds.height * itemFactor) - + remainingFactor -= 1.0 } } return additionalInverseTopInset } - + + private func calculatePinToEdgeTopInset() -> CGFloat { + var lowestPinnedIndex: Int = Int.max + for itemNode in self.itemNodes { + guard let index = itemNode.index, index >= 0, index < self.items.count else { continue } + if index < lowestPinnedIndex && self.items[index].pinToEdgeWithInset { + lowestPinnedIndex = index + } + } + guard lowestPinnedIndex != Int.max else { return 0.0 } + + var totalAboveAndPinned: CGFloat = 0.0 + var sawIndexZero = false + for itemNode in self.itemNodes { + guard let index = itemNode.index else { continue } + if index == 0 { + sawIndexZero = true + } + if index <= lowestPinnedIndex { + totalAboveAndPinned += itemNode.apparentBounds.height + } + } + guard sawIndexZero else { return 0.0 } + + let visibleArea = self.visibleSize.height - self.insets.top - self.insets.bottom + return max(0.0, visibleArea - totalAboveAndPinned) + } + private func areAllItemsOnScreen() -> Bool { if self.itemNodes.count == 0 { return true @@ -1183,7 +1212,11 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur let additionalInverseTopInset = self.calculateAdditionalTopInverseInset() effectiveInsets.top = max(effectiveInsets.top, self.visibleSize.height - additionalInverseTopInset) } - + let pinToEdgeTopInset = self.calculatePinToEdgeTopInset() + if pinToEdgeTopInset > 0.0 { + effectiveInsets.top = max(effectiveInsets.top, self.insets.top + pinToEdgeTopInset) + } + if topItemFound { topItemEdge = self.itemNodes[0].apparentFrame.origin.y - self.tempTopInset } @@ -1614,7 +1647,11 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur let additionalInverseTopInset = self.calculateAdditionalTopInverseInset() effectiveInsets.top = max(effectiveInsets.top, self.visibleSize.height - additionalInverseTopInset) } - + let pinToEdgeTopInset = self.calculatePinToEdgeTopInset() + if pinToEdgeTopInset > 0.0 { + effectiveInsets.top = max(effectiveInsets.top, self.insets.top + pinToEdgeTopInset) + } + completeHeight = effectiveInsets.top + effectiveInsets.bottom if let index = self.itemNodes[self.itemNodes.count - 1].index, index == self.items.count - 1 { @@ -1819,7 +1856,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur }) } - public func transaction(deleteIndices: [ListViewDeleteItem], insertIndicesAndItems: [ListViewInsertItem], updateIndicesAndItems: [ListViewUpdateItem], options: ListViewDeleteAndInsertOptions, scrollToItem: ListViewScrollToItem? = nil, additionalScrollDistance: CGFloat = 0.0, updateSizeAndInsets: ListViewUpdateSizeAndInsets? = nil, stationaryItemRange: (Int, Int)? = nil, updateOpaqueState: Any?, completion: @escaping (ListViewDisplayedItemRange) -> Void = { _ in }) { + public func transaction(deleteIndices: [ListViewDeleteItem], insertIndicesAndItems: [ListViewInsertItem], updateIndicesAndItems: [ListViewUpdateItem], options: ListViewDeleteAndInsertOptions, scrollToItem: ListViewScrollToItem? = nil, additionalScrollDistance: CGFloat = 0.0, updateSizeAndInsets: ListViewUpdateSizeAndInsets? = nil, stationaryItemRange: (Int, Int)? = nil, customAnimationTransition: ControlledTransition? = nil, updateOpaqueState: Any?, completion: @escaping (ListViewDisplayedItemRange) -> Void = { _ in }) { if deleteIndices.isEmpty && insertIndicesAndItems.isEmpty && updateIndicesAndItems.isEmpty && scrollToItem == nil && updateSizeAndInsets == nil && additionalScrollDistance.isZero { if let updateOpaqueState = updateOpaqueState { self.opaqueTransactionState = updateOpaqueState @@ -1831,7 +1868,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur self.transactionQueue.addTransaction({ [weak self] transactionCompletion in if let strongSelf = self { strongSelf.transactionOffset = 0.0 - strongSelf.deleteAndInsertItemsTransaction(deleteIndices: deleteIndices, insertIndicesAndItems: insertIndicesAndItems, updateIndicesAndItems: updateIndicesAndItems, options: options, scrollToItem: scrollToItem, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: stationaryItemRange, updateOpaqueState: updateOpaqueState, customAnimationTransition: updateSizeAndInsets?.customAnimationTransition, completion: { [weak strongSelf] in + strongSelf.deleteAndInsertItemsTransaction(deleteIndices: deleteIndices, insertIndicesAndItems: insertIndicesAndItems, updateIndicesAndItems: updateIndicesAndItems, options: options, scrollToItem: scrollToItem, additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: stationaryItemRange, updateOpaqueState: updateOpaqueState, customAnimationTransition: customAnimationTransition ?? updateSizeAndInsets?.customAnimationTransition, completion: { [weak strongSelf] in completion(strongSelf?.immediateDisplayedItemRange() ?? ListViewDisplayedItemRange(loadedRange: nil, visibleRange: nil)) transactionCompletion() @@ -2627,6 +2664,29 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur } } + private var nextAnimationId: Int = 0 + private func takeNextAnimationId() -> Int { + let value = self.nextAnimationId + self.nextAnimationId += 1 + return value + } + + public func isStrictlyScrolledToPinToEdgeItem() -> Bool { + if self.calculatePinToEdgeTopInset() <= 0.0 { + return false + } + guard let targetIndex = self.items.firstIndex(where: { $0.pinToEdgeWithInset }) else { + return false + } + for itemNode in self.itemNodes { + if itemNode.index == targetIndex { + let expectedMaxY = (self.visibleSize.height - self.insets.bottom) + itemNode.scrollPositioningInsets.bottom + return abs(itemNode.apparentFrame.maxY - expectedMaxY) < 0.5 + } + } + return false + } + private func replayOperations(animated: Bool, animateAlpha: Bool, animateCrossfade: Bool, animateFullTransition: Bool, customAnimationTransition: ControlledTransition?, synchronous: Bool, synchronousLoads: Bool, animateTopItemVerticalOrigin: Bool, operations: [ListViewStateOperation], requestItemInsertionAnimationsIndices: Set, scrollToItem originalScrollToItem: ListViewScrollToItem?, additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemIndex: Int?, updateOpaqueState: Any?, forceInvertOffsetDirection: Bool = false, completion: () -> Void) { var scrollToItem: ListViewScrollToItem? var isExperimentalSnapToScrollToItem = false @@ -2635,6 +2695,9 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur if self.experimentalSnapScrollToItem { self.scrolledToItem = (originalScrollToItem.index, originalScrollToItem.position) } + if originalScrollToItem.index < self.items.count && self.items[originalScrollToItem.index].pinToEdgeWithInset { + self.experimentalSnapScrollToPinnedItem = true + } } else if let scrolledToItem = self.scrolledToItem, self.experimentalSnapScrollToItem { var curve: ListViewAnimationCurve = .Default(duration: nil) var animated = false @@ -2644,6 +2707,23 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur } scrollToItem = ListViewScrollToItem(index: scrolledToItem.0, position: scrolledToItem.1, animated: animated, curve: curve, directionHint: .Down) isExperimentalSnapToScrollToItem = true + } else if self.experimentalSnapScrollToPinnedItem { + if let index = self.items.firstIndex(where: { $0.pinToEdgeWithInset }) { + isExperimentalSnapToScrollToItem = true + var curve: ListViewAnimationCurve = .Default(duration: nil) + var animated = false + if let updateSizeAndInsets = updateSizeAndInsets { + curve = updateSizeAndInsets.curve + animated = !updateSizeAndInsets.duration.isZero + } + scrollToItem = ListViewScrollToItem(index: index, position: self.rotated ? .bottom(0.0) : .top(0.0), animated: animated, curve: curve, directionHint: .Down) + } + } + + if scrollToItem == nil { + if self.itemNodes.isEmpty, self.items.contains(where: { $0.pinToEdgeWithInset }) { + scrollToItem = ListViewScrollToItem(index: 0, position: self.rotated ? .bottom(0.0) : .top(0.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Down) + } } weak var highlightedItemNode: ListViewItemNode? @@ -3022,13 +3102,31 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur if let scrollToItem = scrollToItem, !self.areAllItemsOnScreen() || !sizeOrInsetsUpdated { self.stopScrolling() - + for itemNode in self.itemNodes { if let index = itemNode.index, index == scrollToItem.index { let insets = self.insets// updateSizeAndInsets?.insets ?? self.insets - + + var isPinToEdgeTarget = false + if self.calculatePinToEdgeTopInset() > 0.0, + index >= 0, index < self.items.count, + self.items[index].pinToEdgeWithInset { + isPinToEdgeTarget = true + for otherNode in self.itemNodes { + guard let otherIndex = otherNode.index else { continue } + guard otherIndex >= 0, otherIndex < self.items.count else { continue } + if otherIndex < index, self.items[otherIndex].pinToEdgeWithInset { + isPinToEdgeTarget = false + break + } + } + } + var offset: CGFloat - switch scrollToItem.position { + if isPinToEdgeTarget { + offset = (self.visibleSize.height - insets.bottom) - itemNode.apparentFrame.maxY + itemNode.scrollPositioningInsets.bottom + } else { + switch scrollToItem.position { case let .bottom(additionalOffset): offset = (self.visibleSize.height - insets.bottom) - itemNode.apparentFrame.maxY + itemNode.scrollPositioningInsets.bottom + additionalOffset case let .top(additionalOffset): @@ -3066,8 +3164,9 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur offset = 0.0 } } + } } - + for itemNode in self.itemNodes { var frame = itemNode.frame frame.origin.y += offset @@ -3126,37 +3225,36 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur if let updateSizeAndInsets = updateSizeAndInsets { if self.insets != updateSizeAndInsets.insets || self.headerInsets != updateSizeAndInsets.headerInsets || !self.visibleSize.height.isEqual(to: updateSizeAndInsets.size.height) { + let previousPinToEdgeTopInset = self.calculatePinToEdgeTopInset() let previousVisibleSize = self.visibleSize self.visibleSize = updateSizeAndInsets.size - + var offsetFix: CGFloat + var offsetFixUsesEffectiveTopInset = false let insetDeltaOffsetFix: CGFloat = 0.0 if (self.isTracking && !self.allowInsetFixWhileTracking) || isExperimentalSnapToScrollToItem { offsetFix = 0.0 } else if self.snapToBottomInsetUntilFirstInteraction { offsetFix = -updateSizeAndInsets.insets.bottom + self.insets.bottom } else { - /*if let visualInsets = self.visualInsets, animated, (visualInsets.top == updateSizeAndInsets.insets.top || visualInsets.top == self.insets.top) { - offsetFix = 0.0 - } else {*/ - offsetFix = updateSizeAndInsets.insets.top - self.insets.top - //} + offsetFix = updateSizeAndInsets.insets.top - self.insets.top + offsetFixUsesEffectiveTopInset = true } - + offsetFix += additionalScrollDistance - - /*if let topItemNode = self.itemNodes.first(where: { $0.index == 0 }) { - let topEdge = self.scroller.contentOffset.y + updateSizeAndInsets.insets.top - offsetFix = -(topEdge - topItemNode.apparentFrame.minY) - }*/ - + self.insets = updateSizeAndInsets.insets self.headerInsets = updateSizeAndInsets.headerInsets ?? self.insets self.scrollIndicatorInsets = updateSizeAndInsets.scrollIndicatorInsets ?? self.insets self.itemOffsetInsets = updateSizeAndInsets.itemOffsetInsets self.ensureTopInsetForOverlayHighlightedItems = updateSizeAndInsets.ensureTopInsetForOverlayHighlightedItems self.visibleSize = updateSizeAndInsets.size - + + if offsetFixUsesEffectiveTopInset { + let updatedPinToEdgeTopInset = self.calculatePinToEdgeTopInset() + offsetFix += updatedPinToEdgeTopInset - previousPinToEdgeTopInset + } + for itemNode in self.itemNodes { itemNode.updateFrame(itemNode.frame.offsetBy(dx: 0.0, dy: offsetFix), within: self.visibleSize, transition: customAnimationTransition) } @@ -3239,16 +3337,18 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur animation = basicAnimation } - deferredUpdateVisible = true - animation.completion = { [weak self] _ in - self?.updateItemNodesVisibilities(onlyPositive: false) - } - self.layer.add(animation, forKey: nil) - if !completeOffset.isZero { - for itemNode in self.itemNodes { - itemNode.applyAbsoluteOffset(value: CGPoint(x: 0.0, y: -completeOffset), animationCurve: animationCurve, duration: animationDuration) + if customAnimationTransition == nil { + deferredUpdateVisible = true + animation.completion = { [weak self] _ in + self?.updateItemNodesVisibilities(onlyPositive: false) + } + self.layer.add(animation, forKey: "animation-\(self.takeNextAnimationId())") + if !completeOffset.isZero { + for itemNode in self.itemNodes { + itemNode.applyAbsoluteOffset(value: CGPoint(x: 0.0, y: -completeOffset), animationCurve: animationCurve, duration: animationDuration) + } + self.didScrollWithOffset?(-completeOffset, ContainedViewLayoutTransition.animated(duration: animationDuration, curve: animationCurve), nil, self.isTrackingOrDecelerating) } - self.didScrollWithOffset?(-completeOffset, ContainedViewLayoutTransition.animated(duration: animationDuration, curve: animationCurve), nil, self.isTrackingOrDecelerating) } } else { self.didScrollWithOffset?(-completeOffset, .immediate, nil, self.isTrackingOrDecelerating) @@ -3670,7 +3770,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur headerNode.removeFromSupernode() } } - self.layer.add(animation, forKey: nil) + self.layer.add(animation, forKey: "animation-\(self.takeNextAnimationId()))") } for itemNode in self.itemNodes { @@ -3988,6 +4088,12 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur } else { headerNode.layer.animateBoundsOriginAdditive(from: offset, to: CGPoint(), duration: duration, mediaTimingFunction: CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)) } + case .easeIn: + if transition.1 { + headerNode.layer.animateBoundsOriginAdditive(from: offset, to: CGPoint(), duration: duration, mediaTimingFunction: ContainedViewLayoutTransitionCurve.slide.mediaTimingFunction) + } else { + headerNode.layer.animateBoundsOriginAdditive(from: offset, to: CGPoint(), duration: duration, mediaTimingFunction: CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)) + } } } @@ -4790,7 +4896,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur } if requestUpdateVisibleItems { - self.enqueueUpdateVisibleItems(synchronous: false) + self.enqueueUpdateVisibleItems(synchronous: self.experimentalSnapScrollToPinnedItem) } if scrollingForReorder { diff --git a/submodules/Display/Source/ListViewAnimation.swift b/submodules/Display/Source/ListViewAnimation.swift index 3ee91fdd68..52e3b4c9c8 100644 --- a/submodules/Display/Source/ListViewAnimation.swift +++ b/submodules/Display/Source/ListViewAnimation.swift @@ -117,6 +117,10 @@ public let listViewAnimationCurveEaseInOut: (CGFloat) -> CGFloat = { t in return bezierPoint(0.42, 0.0, 0.58, 1.0, t) } +public let listViewAnimationCurveEaseIn: (CGFloat) -> CGFloat = { t in + return bezierPoint(0.42, 0.0, 1.0, 1.0, t) +} + #if os(iOS) public func listViewAnimationCurveFromAnimationOptions(animationOptions: UIView.AnimationOptions) -> (CGFloat) -> CGFloat { if animationOptions.rawValue == UInt(7 << 16) { @@ -215,6 +219,8 @@ public func listViewAnimationDurationAndCurve(transition: ContainedViewLayoutTra return (animationDuration, .Default(duration: animationDuration)) case .easeInOut: return (animationDuration, .Default(duration: animationDuration)) + case .easeIn: + return (animationDuration, .Default(duration: animationDuration)) case .spring, .customSpring: return (animationDuration, .Spring(duration: animationDuration)) case let .custom(c1x, c1y, c2x, c2y): diff --git a/submodules/Display/Source/ListViewItem.swift b/submodules/Display/Source/ListViewItem.swift index 1228c055ac..7e022b561a 100644 --- a/submodules/Display/Source/ListViewItem.swift +++ b/submodules/Display/Source/ListViewItem.swift @@ -69,7 +69,7 @@ public final class ListViewItemApply { } } -public protocol ListViewItem { +public protocol ListViewItem: AnyObject { func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) @@ -77,6 +77,7 @@ public protocol ListViewItem { var headerAccessoryItem: ListViewAccessoryItem? { get } var selectable: Bool { get } var approximateHeight: CGFloat { get } + var pinToEdgeWithInset: Bool { get } func selected(listView: ListView) } @@ -98,6 +99,10 @@ public extension ListViewItem { return 44.0 } + var pinToEdgeWithInset: Bool { + return false + } + func selected(listView: ListView) { } diff --git a/submodules/Display/Source/ListViewProtocol.swift b/submodules/Display/Source/ListViewProtocol.swift index 51a838d7e4..dbfac0920a 100644 --- a/submodules/Display/Source/ListViewProtocol.swift +++ b/submodules/Display/Source/ListViewProtocol.swift @@ -67,6 +67,7 @@ public protocol ListView: ASDisplayNode { additionalScrollDistance: CGFloat, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemRange: (Int, Int)?, + customAnimationTransition: ControlledTransition?, updateOpaqueState: Any?, completion: @escaping (ListViewDisplayedItemRange) -> Void ) @@ -102,6 +103,7 @@ public extension ListView { additionalScrollDistance: CGFloat = 0.0, updateSizeAndInsets: ListViewUpdateSizeAndInsets? = nil, stationaryItemRange: (Int, Int)? = nil, + customAnimationTransition: ControlledTransition? = nil, updateOpaqueState: Any?, completion: @escaping (ListViewDisplayedItemRange) -> Void = { _ in } ) { @@ -114,6 +116,7 @@ public extension ListView { additionalScrollDistance: additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: stationaryItemRange, + customAnimationTransition: customAnimationTransition, updateOpaqueState: updateOpaqueState, completion: completion ) diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index 7f7f88679f..d8a004fcd0 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -498,7 +498,7 @@ open class NavigationController: UINavigationController, ContainableController, } if let globalScrollToTopNode = self.globalScrollToTopNode { - globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1.0)) + globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1)) } var overlayContainerLayout = layout diff --git a/submodules/Display/Source/Navigation/NavigationSplitContainer.swift b/submodules/Display/Source/Navigation/NavigationSplitContainer.swift index 1f1f795837..42192baf34 100644 --- a/submodules/Display/Source/Navigation/NavigationSplitContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationSplitContainer.swift @@ -87,7 +87,7 @@ final class NavigationSplitContainer: ASDisplayNode { let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0)) let detailWidth = layout.size.width - masterWidth - self.masterScrollToTopView.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: masterWidth, height: 1.0)) + self.masterScrollToTopView.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: masterWidth, height: 10.0)) self.detailScrollToTopView.frame = CGRect(origin: CGPoint(x: masterWidth, y: -1.0), size: CGSize(width: detailWidth, height: 1.0)) transition.updateFrame(node: self.masterContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: layout.size.height))) diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 63f19d6cde..00d2a9c12b 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -28,10 +28,13 @@ public final class NavigationBarTheme { public let badgeStrokeColor: UIColor public let badgeTextColor: UIColor public let edgeEffectColor: UIColor? + public let accentButtonColor: UIColor + public let accentDisabledButtonColor: 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, accentDisabledButtonColor: UIColor, accentForegroundColor: UIColor, style: NavigationBar.Style = .legacy, glassStyle: NavigationBar.GlassStyle = .default) { self.overallDarkAppearance = overallDarkAppearance self.buttonColor = buttonColor self.disabledButtonColor = disabledButtonColor @@ -44,16 +47,19 @@ public final class NavigationBarTheme { self.badgeStrokeColor = badgeStrokeColor self.badgeTextColor = badgeTextColor self.edgeEffectColor = edgeEffectColor + self.accentButtonColor = accentButtonColor + self.accentDisabledButtonColor = accentDisabledButtonColor + 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, accentDisabledButtonColor: self.accentDisabledButtonColor, 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, accentDisabledButtonColor: self.accentDisabledButtonColor, accentForegroundColor: self.accentForegroundColor, style: self.style, glassStyle: self.glassStyle) } } @@ -173,6 +179,7 @@ public protocol NavigationBar: ASDisplayNode { var leftButtonNode: NavigationButtonNode { get } var rightButtonNode: NavigationButtonNode { get } var additionalContentNode: SparseNode { get } + var edgeEffectView: UIView? { get } func reattachAdditionalContentNode() diff --git a/submodules/Display/Source/ScrollToTopProxyView.swift b/submodules/Display/Source/ScrollToTopProxyView.swift index b7aa0d8d9c..ba727f4d11 100644 --- a/submodules/Display/Source/ScrollToTopProxyView.swift +++ b/submodules/Display/Source/ScrollToTopProxyView.swift @@ -8,11 +8,11 @@ class ScrollToTopView: UIScrollView, UIScrollViewDelegate { super.init(frame: frame) self.isOpaque = false - self.backgroundColor = .clear self.delegate = self self.scrollsToTop = true - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.contentInsetAdjustmentBehavior = .never + self.contentInsetAdjustmentBehavior = .never + if #available(iOS 17.0, *) { + self.allowsKeyboardScrolling = false } } @@ -23,8 +23,8 @@ class ScrollToTopView: UIScrollView, UIScrollViewDelegate { override var frame: CGRect { didSet { let frame = self.frame - self.contentSize = CGSize(width: frame.width, height: frame.height + 1.0) - self.contentOffset = CGPoint(x: 0.0, y: 1.0) + self.contentSize = CGSize(width: frame.width, height: frame.height + 1000.0) + self.contentOffset = CGPoint(x: 0.0, y: 1000.0) } } @@ -35,6 +35,10 @@ class ScrollToTopView: UIScrollView, UIScrollViewDelegate { return false } + + func scrollViewDidScrollToTop(_ scrollView: UIScrollView) { + print("scrollViewDidScrollToTop") + } } class ScrollToTopNode: ASDisplayNode { diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 452bff9c44..ecc22273d0 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1216,11 +1216,11 @@ open class TextNode: ASDisplayNode, TextNodeProtocol { public static let all: RenderContentTypes = [.text, .emoji] } - final class DrawingParameters: NSObject { + public final class DrawingParameters: NSObject { let cachedLayout: TextNodeLayout? let renderContentTypes: RenderContentTypes - init(cachedLayout: TextNodeLayout?, renderContentTypes: RenderContentTypes) { + public init(cachedLayout: TextNodeLayout?, renderContentTypes: RenderContentTypes) { self.cachedLayout = cachedLayout self.renderContentTypes = renderContentTypes @@ -1295,7 +1295,7 @@ open class TextNode: ASDisplayNode, TextNodeProtocol { } } - private static func calculateLayoutV2( + public static func calculateLayoutV2( attributedString: NSAttributedString, minimumNumberOfLines: Int, maximumNumberOfLines: Int, @@ -1685,7 +1685,7 @@ open class TextNode: ASDisplayNode, TextNodeProtocol { ) } - static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?) -> TextNodeLayout { + public static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool, displayEmbeddedItemsUnderSpoilers: Bool, customTruncationToken: NSAttributedString?) -> TextNodeLayout { guard let attributedString else { return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displaySpoilers: displaySpoilers) } diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index bf254de0c4..413d8a6de0 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -306,14 +306,7 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { return self._ready } - private var scrollToTopView: ScrollToTopView? - public var scrollToTop: (() -> Void)? { - didSet { - if self.isViewLoaded { - self.updateScrollToTopView() - } - } - } + public var scrollToTop: (() -> Void)? public var scrollToTopWithTabBar: (() -> Void)? public var longTapWithTabBar: (() -> Void)? @@ -357,24 +350,6 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { open func didAppearInContextPreview() { } - private func updateScrollToTopView() { - /*if self.scrollToTop != nil { - if let displayNode = self._displayNode , self.scrollToTopView == nil { - let scrollToTopView = ScrollToTopView(frame: CGRect(x: 0.0, y: -1.0, width: displayNode.bounds.size.width, height: 1.0)) - scrollToTopView.action = { [weak self] in - if let scrollToTop = self?.scrollToTop { - scrollToTop() - } - } - self.scrollToTopView = scrollToTopView - self.view.addSubview(scrollToTopView) - } - } else*/ if let scrollToTopView = self.scrollToTopView { - scrollToTopView.removeFromSuperview() - self.scrollToTopView = nil - } - } - public var titleSignal: Signal { return Signal { [weak self] subscriber in guard let self else { @@ -514,10 +489,6 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { if self.automaticallyControlPresentationContextLayout { self.presentationContext.containerLayoutUpdated(layout, transition: transition) } - - if let scrollToTopView = self.scrollToTopView { - scrollToTopView.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 10.0) - } } open func navigationStackConfigurationUpdated(next: [ViewController]) { @@ -541,7 +512,6 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { } open func displayNodeDidLoad() { - self.updateScrollToTopView() if let backgroundColor = self.displayNode.backgroundColor, backgroundColor.alpha.isEqual(to: 1.0) { self.blocksBackgroundWhenInOverlay = true self.isOpaqueWhenInOverlay = true diff --git a/submodules/DrawingUI/BUILD b/submodules/DrawingUI/BUILD index b2be9d65ae..e95073b967 100644 --- a/submodules/DrawingUI/BUILD +++ b/submodules/DrawingUI/BUILD @@ -64,7 +64,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/LegacyComponents:LegacyComponents", diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 52827a4718..2678fbe440 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -6,7 +6,6 @@ import Display import ComponentFlow import LegacyComponents import TelegramCore -import Postbox import SwiftSignalKit import TelegramPresentationData import AccountContext @@ -825,7 +824,7 @@ private final class DrawingScreenComponent: CombinedComponent { let tools = self.drawingState.tools let _ = (self.context.sharedContext.accountManager.transaction { transaction -> Void in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.drawingSettings, { _ in - return PreferencesEntry(DrawingSettings(tools: tools, colors: [])) + return EnginePreferencesEntry(DrawingSettings(tools: tools, colors: [])) }) }).start() } @@ -2893,13 +2892,13 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U var stickers: [Any] = [] for entity in self.entitiesView.entities { if let sticker = entity as? DrawingStickerEntity, case let .file(file, _) = sticker.content { - let coder = PostboxEncoder() + let coder = EnginePostboxEncoder() coder.encodeRootObject(file.media) stickers.append(coder.makeData()) } else if let text = entity as? DrawingTextEntity, let subEntities = text.renderSubEntities { for sticker in subEntities { if let sticker = sticker as? DrawingStickerEntity, case let .file(file, _) = sticker.content { - let coder = PostboxEncoder() + let coder = EnginePostboxEncoder() coder.encodeRootObject(file.media) stickers.append(coder.makeData()) } diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index 9b6e92d61f..9ad46ba877 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -478,7 +478,7 @@ public class DrawingStickerEntityView: DrawingEntityView { let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm") - let pathPrefix = self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let pathPrefix = self.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.resource.id)) let playbackMode: AnimatedStickerPlaybackMode = .loop self.animationNode?.setup(source: source, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: playbackMode, mode: .direct(cachePathPrefix: pathPrefix)) diff --git a/submodules/Emoji/Sources/EmojiUtils.swift b/submodules/Emoji/Sources/EmojiUtils.swift index 4b5033e7b8..b31549fcd6 100644 --- a/submodules/Emoji/Sources/EmojiUtils.swift +++ b/submodules/Emoji/Sources/EmojiUtils.swift @@ -57,6 +57,18 @@ public extension UnicodeScalar { private final class FrameworkClass: NSObject { } +private let allowedEmojiLikeSymbols: Set = [ + "\u{2640}", + "\u{2640}\u{FE0E}", + "\u{2640}\u{FE0F}", + "\u{2642}", + "\u{2642}\u{FE0E}", + "\u{2642}\u{FE0F}", + "\u{26A7}", + "\u{26A7}\u{FE0E}", + "\u{26A7}\u{FE0F}" +] + public extension String { func trimmingTrailingSpaces() -> String { var t = self @@ -74,6 +86,20 @@ public extension String { return self.contains { $0.isEmoji } } + var containsGraphicEmoji: Bool { + var containsEmoji = false + self.enumerateSubstrings(in: self.startIndex ..< self.endIndex, options: .byComposedCharacterSequences) { substring, _, _, stop in + guard let substring else { + return + } + if substring.containsEmoji && !allowedEmojiLikeSymbols.contains(substring) { + containsEmoji = true + stop = true + } + } + return containsEmoji + } + var containsOnlyEmoji: Bool { return !self.isEmpty && !self.contains { !$0.isEmoji } } diff --git a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFrame.h b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFrame.h index f1749fabbb..29d8632f65 100644 --- a/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFrame.h +++ b/submodules/FFMpegBinding/Public/FFMpegBinding/FFMpegAVFrame.h @@ -9,7 +9,8 @@ typedef NS_ENUM(NSUInteger, FFMpegAVFrameColorRange) { typedef NS_ENUM(NSUInteger, FFMpegAVFramePixelFormat) { FFMpegAVFramePixelFormatYUV, - FFMpegAVFramePixelFormatYUVA + FFMpegAVFramePixelFormatYUVA, + FFMpegAVFramePixelFormatUnsupported }; typedef NS_ENUM(NSUInteger, FFMpegAVFrameNativePixelFormat) { @@ -29,7 +30,7 @@ typedef NS_ENUM(NSUInteger, FFMpegAVFrameNativePixelFormat) { @property (nonatomic, readonly) FFMpegAVFramePixelFormat pixelFormat; - (instancetype)init; -- (instancetype)initWithPixelFormat:(FFMpegAVFramePixelFormat)pixelFormat width:(int32_t)width height:(int32_t)height; +- (instancetype _Nullable)initWithPixelFormat:(FFMpegAVFramePixelFormat)pixelFormat width:(int32_t)width height:(int32_t)height; - (void *)impl; - (FFMpegAVFrameNativePixelFormat)nativePixelFormat; diff --git a/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m b/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m index fda8224dc7..d5bf139206 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m +++ b/submodules/FFMpegBinding/Sources/FFMpegAVFrame.m @@ -29,6 +29,8 @@ case FFMpegAVFramePixelFormatYUVA: _impl->format = AV_PIX_FMT_YUVA420P; break; + case FFMpegAVFramePixelFormatUnsupported: + return nil; } _impl->width = width; _impl->height = height; @@ -101,8 +103,11 @@ switch (_impl->format) { case AV_PIX_FMT_YUVA420P: return FFMpegAVFramePixelFormatYUVA; - default: + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUVJ420P: return FFMpegAVFramePixelFormatYUV; + default: + return FFMpegAVFramePixelFormatUnsupported; } } diff --git a/submodules/FFMpegBinding/Sources/FFMpegSWResample.m b/submodules/FFMpegBinding/Sources/FFMpegSWResample.m index c6771b6db3..a2391be99a 100644 --- a/submodules/FFMpegBinding/Sources/FFMpegSWResample.m +++ b/submodules/FFMpegBinding/Sources/FFMpegSWResample.m @@ -111,22 +111,37 @@ return nil; } + int64_t outSamples = (int64_t)frameImpl->nb_samples * (int64_t)_ratio; + // Cap at 8M samples per frame: ~10× larger than any legitimate codec + // frame × resample ratio, bounds the per-frame buffer to ~32 MB (s16 stereo), + // and rejects pathological inputs (e.g. 1 Hz source with a 65 KB FLAC block). + if (outSamples <= 0 || outSamples > 8 * 1024 * 1024) { + return nil; + } + int bufSize = av_samples_get_buffer_size(NULL, (int)_destinationChannelCount, - frameImpl->nb_samples * (int)_ratio, + (int)outSamples, (enum AVSampleFormat)_destinationSampleFormat, 1); - - if (!_buffer || _bufferSize < bufSize) { - _bufferSize = bufSize; - _buffer = realloc(_buffer, _bufferSize); + if (bufSize <= 0) { + return nil; } - + + if (!_buffer || _bufferSize < bufSize) { + void *newBuffer = realloc(_buffer, bufSize); + if (!newBuffer) { + return nil; + } + _buffer = newBuffer; + _bufferSize = bufSize; + } + Byte *outbuf[2] = { _buffer, 0 }; - + int numFrames = swr_convert(_context, outbuf, - frameImpl->nb_samples * (int)_ratio, + (int)outSamples, (const uint8_t **)frameImpl->data, frameImpl->nb_samples); if (numFrames <= 0) { diff --git a/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift b/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift index b91f1d7a65..ccc3ea7841 100644 --- a/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift +++ b/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift @@ -863,6 +863,8 @@ public final class FeaturedStickersScreen: ViewController { fileprivate var searchNavigationNode: SearchNavigationContentNode? + private var eventsDisposable: Disposable? + public init(context: AccountContext, highlightedPackId: ItemCollectionId?, forceTheme: PresentationTheme? = nil, stickerActionTitle: String? = nil, sendSticker: ((FileMediaReference, UIView?, CGRect?) -> Bool)? = nil) { self.context = context self.highlightedPackId = highlightedPackId @@ -906,6 +908,18 @@ public final class FeaturedStickersScreen: ViewController { } } }) + + self.eventsDisposable = (context.account.stateManager.installedStickerPacksArchivedEvents + |> deliverOnMainQueue).startStrict(next: { [weak self] count in + guard let self else { + return + } + if count == 0 { + return + } + let presentationData = self.presentationData + self.push(textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: presentationData.strings.ArchivedPacksAlert_Title, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) + }) } required init(coder aDecoder: NSCoder) { @@ -914,6 +928,7 @@ public final class FeaturedStickersScreen: ViewController { deinit { self.presentationDataDisposable?.dispose() + self.eventsDisposable?.dispose() } private func updatePresentationData() { @@ -1167,7 +1182,8 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { private var enqueuedTransitions: [FeaturedSearchGridTransition] = [] private let searchDisposable = MetaDisposable() - + private var stickerSearchContext: StickerSearchContext? + private let queue = Queue() private let currentEntries = Atomic<[FeaturedSearchEntry]?>(value: nil) private let currentRemotePacks = Atomic(value: nil) @@ -1222,6 +1238,14 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { self.gridNode.scrollingInitiated = { [weak self] in self?.deactivateSearchBar?() } + self.gridNode.visibleItemsUpdated = { [weak self] visibleItems in + guard let self, let (bottomIndex, _) = visibleItems.bottomVisible else { + return + } + if bottomIndex >= self.gridNode.items.count - 15 { + self.stickerSearchContext?.loadMore() + } + } self.interaction = StickerPaneSearchInteraction(open: { [weak self] info in if let strongSelf = self { @@ -1284,56 +1308,52 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { } func updateText(_ text: String, languageCode: String?) { - let signal: Signal<([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)?, NoError> + let signal: Signal<([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?, Bool, StickerSearchContext?)?, NoError> if !text.isEmpty { let context = self.context - let stickers: Signal<[(String?, FoundStickerItem)], NoError> = Signal { subscriber in - var signals: Signal<[Signal<(String?, [FoundStickerItem]), NoError>], NoError> = .single([]) - - let query = text.trimmingCharacters(in: .whitespacesAndNewlines) - if query.isSingleEmoji { - signals = .single([context.engine.stickers.searchStickers(query: nil, emoticon: [text.basicEmoji.0], inputLanguageCode: "") - |> map { (nil, $0.items) }]) - } else if query.count > 1, let languageCode = languageCode, !languageCode.isEmpty && languageCode != "emoji" { - var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3) - if !languageCode.lowercased().hasPrefix("en") { - signal = signal - |> mapToSignal { keywords in - return .single(keywords) - |> then( - context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query.lowercased(), completeMatch: query.count < 3) - |> map { englishKeywords in - return keywords + englishKeywords - } - ) - } - } - signals = signal - |> map { keywords -> [Signal<(String?, [FoundStickerItem]), NoError>] in - let emoticon = keywords.flatMap { $0.emoticons }.map { $0.basicEmoji.0 } - return [context.engine.stickers.searchStickers(query: query, emoticon: emoticon, inputLanguageCode: languageCode) - |> map { (nil, $0.items) }] + let query = text.trimmingCharacters(in: .whitespacesAndNewlines) + let _ = self.currentRemotePacks.swap(nil) + self.stickerSearchContext = nil + + let stickers: Signal<(items: [(String?, FoundStickerItem)], isSearching: Bool, searchContext: StickerSearchContext?), NoError> + if query.isSingleEmoji { + let searchContext = context.engine.stickers.stickerSearchContext(query: nil, emoticon: [query.basicEmoji.0], inputLanguageCode: "") + stickers = searchContext.state + |> map { state -> (items: [(String?, FoundStickerItem)], isSearching: Bool, searchContext: StickerSearchContext?) in + return (state.items.map { (nil, $0) }, state.items.isEmpty && state.isLoadingMore, searchContext) + } + } else if query.count > 1, let languageCode = languageCode, !languageCode.isEmpty && languageCode != "emoji" { + var keywordsSignal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3) + if !languageCode.lowercased().hasPrefix("en") { + keywordsSignal = keywordsSignal + |> mapToSignal { keywords in + return .single(keywords) + |> then( + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query.lowercased(), completeMatch: query.count < 3) + |> map { englishKeywords in + return keywords + englishKeywords + } + ) } } - - return (signals - |> mapToSignal { signals in - return combineLatest(signals) - }).start(next: { results in - var result: [(String?, FoundStickerItem)] = [] - for (emoji, stickers) in results { - for sticker in stickers { - result.append((emoji, sticker)) - } + stickers = keywordsSignal + |> mapToSignal { keywords -> Signal<(items: [(String?, FoundStickerItem)], isSearching: Bool, searchContext: StickerSearchContext?), NoError> in + let emoticon = Array(Set(keywords.flatMap { $0.emoticons }.map { $0.basicEmoji.0 })) + guard !emoticon.isEmpty else { + return .single(([], false, nil)) } - subscriber.putNext(result) - }, completed: { - subscriber.putCompletion() - }) + let searchContext = context.engine.stickers.stickerSearchContext(query: query, emoticon: emoticon, inputLanguageCode: languageCode) + return searchContext.state + |> map { state -> (items: [(String?, FoundStickerItem)], isSearching: Bool, searchContext: StickerSearchContext?) in + return (state.items.map { (nil, $0) }, state.items.isEmpty && state.isLoadingMore, searchContext) + } + } + } else { + stickers = .single(([], false, nil)) } - let local = context.engine.stickers.searchStickerSets(query: text) - let remote = context.engine.stickers.searchStickerSetsRemotely(query: text) + let local = context.engine.stickers.searchStickerSets(query: query) + let remote = context.engine.stickers.searchStickerSetsRemotely(query: query) |> delay(0.2, queue: Queue.mainQueue()) let rawPacks = local |> mapToSignal { result -> Signal<(FoundStickerSets, Bool, FoundStickerSets?), NoError> in @@ -1387,11 +1407,12 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { } signal = combineLatest(stickers, packs) - |> map { stickers, packs -> ([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)? in - return (stickers, packs.0, packs.1, packs.2) + |> map { stickers, packs -> ([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?, Bool, StickerSearchContext?)? in + return (stickers.items, packs.0, packs.1, packs.2, stickers.isSearching, stickers.searchContext) } - self.updateActivity?(true) } else { + self.stickerSearchContext = nil + let _ = self.currentRemotePacks.swap(nil) signal = .single(nil) self.updateActivity?(false) } @@ -1406,14 +1427,13 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { var displayResults: Bool = false var entries: [FeaturedSearchEntry] = [] - if let (stickers, packs, final, remote) = result { + if let (stickers, packs, final, remote, isSearching, searchContext) = result { + strongSelf.stickerSearchContext = searchContext if let remote = remote { let _ = strongSelf.currentRemotePacks.swap(remote) } - if final { - strongSelf.updateActivity?(false) - } + strongSelf.updateActivity?(isSearching) var index = 0 var existingStickerIds = Set() @@ -1444,12 +1464,15 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { } } - if final || !entries.isEmpty { + if !isSearching && (final || !entries.isEmpty) { strongSelf.notFoundNode.isHidden = !entries.isEmpty + } else { + strongSelf.notFoundNode.isHidden = true } displayResults = true } else { + strongSelf.stickerSearchContext = nil let _ = strongSelf.currentRemotePacks.swap(nil) strongSelf.updateActivity?(false) } diff --git a/submodules/FeaturedStickersScreen/Sources/MediaInputPaneTrendingItem.swift b/submodules/FeaturedStickersScreen/Sources/MediaInputPaneTrendingItem.swift index 59228b5fff..c466760248 100644 --- a/submodules/FeaturedStickersScreen/Sources/MediaInputPaneTrendingItem.swift +++ b/submodules/FeaturedStickersScreen/Sources/MediaInputPaneTrendingItem.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import StickerResources diff --git a/submodules/FeaturedStickersScreen/Sources/StickerPaneSearchGlobaltem.swift b/submodules/FeaturedStickersScreen/Sources/StickerPaneSearchGlobaltem.swift index 7cf0cfffe2..e36198db62 100644 --- a/submodules/FeaturedStickersScreen/Sources/StickerPaneSearchGlobaltem.swift +++ b/submodules/FeaturedStickersScreen/Sources/StickerPaneSearchGlobaltem.swift @@ -4,7 +4,6 @@ import Display import AsyncDisplayKit import TelegramCore import SwiftSignalKit -import Postbox import TelegramPresentationData import ListSectionHeaderNode import AccountContext diff --git a/submodules/FeaturedStickersScreen/Sources/StickerPaneSearchStickerItem.swift b/submodules/FeaturedStickersScreen/Sources/StickerPaneSearchStickerItem.swift index d1521567eb..2e12bc2331 100644 --- a/submodules/FeaturedStickersScreen/Sources/StickerPaneSearchStickerItem.swift +++ b/submodules/FeaturedStickersScreen/Sources/StickerPaneSearchStickerItem.swift @@ -4,7 +4,6 @@ import Display import TelegramCore import SwiftSignalKit import AsyncDisplayKit -import Postbox import TelegramPresentationData import StickerResources import AccountContext diff --git a/submodules/FileMediaResourceStatus/BUILD b/submodules/FileMediaResourceStatus/BUILD index 44d70774c6..12da9fa5c8 100644 --- a/submodules/FileMediaResourceStatus/BUILD +++ b/submodules/FileMediaResourceStatus/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/MediaPlayer:UniversalMediaPlayer", 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/ChatItemGalleryFooterContentNode.swift b/submodules/GalleryUI/Sources/ChatItemGalleryFooterContentNode.swift index cc8dd4f502..1b5551a902 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 @@ -311,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 @@ -489,8 +500,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) @@ -1297,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)) @@ -1888,41 +1905,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 +1923,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 +1948,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 +1995,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 +2013,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 +2038,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 +2150,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 +2163,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 +2188,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/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 ab140949c9..0162ae83fb 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))) + } } } } @@ -681,7 +690,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, accentDisabledButtonColor: .white, accentForegroundColor: .black, style: .glass) private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode @@ -918,11 +927,12 @@ 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), + self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) |> take(1), translateToLanguage |> take(1) ).start(next: { [weak self] view, preferencesView, translateToLanguage in let f: () -> Void = { @@ -930,7 +940,7 @@ public class GalleryController: ViewController, StandalonePresentableController, if let view = view { strongSelf.peerIsCopyProtected = view.peerIsCopyProtected - let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue + let appConfiguration: AppConfiguration = preferencesView?.get(AppConfiguration.self) ?? .defaultValue let configuration = GalleryConfiguration.with(appConfiguration: appConfiguration) strongSelf.configuration = configuration @@ -1006,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/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index 094fadb5fc..618021a6c9 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import SwipeToDismissGesture import AccountContext import UndoUI diff --git a/submodules/GalleryUI/Sources/GalleryItemNode.swift b/submodules/GalleryUI/Sources/GalleryItemNode.swift index 8bbb4e686b..4c94ab8aad 100644 --- a/submodules/GalleryUI/Sources/GalleryItemNode.swift +++ b/submodules/GalleryUI/Sources/GalleryItemNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import SwiftSignalKit -import Postbox public enum GalleryItemNodeNavigationStyle { case light diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index af98d0480d..ac62f7640f 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import SwiftSignalKit -import Postbox private func edgeWidth(width: CGFloat) -> CGFloat { return min(44.0, floor(width / 6.0)) diff --git a/submodules/GalleryUI/Sources/GalleryTitleView.swift b/submodules/GalleryUI/Sources/GalleryTitleView.swift index e75948f5fc..01714785b3 100644 --- a/submodules/GalleryUI/Sources/GalleryTitleView.swift +++ b/submodules/GalleryUI/Sources/GalleryTitleView.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import AccountContext import TelegramPresentationData diff --git a/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift index f7e42b59d4..e4a06bc893 100644 --- a/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatAnimationGalleryItem.swift @@ -93,7 +93,7 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode { private var disposable = MetaDisposable() private var fetchDisposable = MetaDisposable() private let statusDisposable = MetaDisposable() - private var status: MediaResourceStatus? + private var status: EngineMediaResource.FetchStatus? init(context: AccountContext, presentationData: PresentationData) { self.context = context @@ -195,7 +195,7 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode { } private func setupStatus(resource: MediaResource) { - self.statusDisposable.set((self.context.account.postbox.mediaBox.resourceStatus(resource) + self.statusDisposable.set((self.context.engine.resources.status(resource: EngineMediaResource(resource)) |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self { let previousStatus = strongSelf.status @@ -341,7 +341,7 @@ final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode { if let resource = resource { switch status { case .Fetching: - self.context.account.postbox.mediaBox.cancelInteractiveResourceFetch(resource.resource) + self.context.engine.resources.cancelInteractiveResourceFetch(id: EngineMediaResource.Id(resource.resource.id)) case .Remote: self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: resource, statsCategory: statsCategory ?? .generic).start()) default: diff --git a/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift index 19eadbaba7..7d1b52c291 100644 --- a/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/ChatDocumentGalleryItem.swift @@ -110,7 +110,7 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD private var fetchDisposable = MetaDisposable() private let statusDisposable = MetaDisposable() - private var status: MediaResourceStatus? + private var status: EngineMediaResource.FetchStatus? init(context: AccountContext, presentationData: PresentationData) { if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { @@ -183,12 +183,12 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD self.maybeLoadContent() self.setupStatus(context: context, resource: fileReference.media.resource) - self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start()) + self.fetchDisposable.set(context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file).start()) } } private func setupStatus(context: AccountContext, resource: MediaResource) { - self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(resource) + self.statusDisposable.set((context.engine.resources.status(resource: EngineMediaResource(resource)) |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self { let previousStatus = strongSelf.status @@ -238,11 +238,11 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD if let fileName = fileReference.media.fileName { pathExtension = (fileName as NSString).pathExtension } - let data = context.account.postbox.mediaBox.resourceData(fileReference.media.resource, pathExtension: pathExtension, option: .complete(waitUntilFetchStatus: false)) + let data = context.engine.resources.data(resource: EngineMediaResource(fileReference.media.resource), pathExtension: pathExtension) |> deliverOnMainQueue self.dataDisposable.set(data.start(next: { [weak self] data in if let strongSelf = self { - if data.complete { + if data.isComplete { if let webView = strongSelf.webView as? WKWebView { if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { let blockRules = """ @@ -386,9 +386,9 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD if let (context, fileReference) = self.contextAndFile, let status = self.status { switch status { case .Fetching: - context.account.postbox.mediaBox.cancelInteractiveResourceFetch(fileReference.media.resource) + context.engine.resources.cancelInteractiveResourceFetch(id: EngineMediaResource.Id(fileReference.media.resource.id)) case .Remote: - self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start()) + self.fetchDisposable.set(context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file).start()) default: break } diff --git a/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift b/submodules/GalleryUI/Sources/Items/ChatExternalFileGalleryItem.swift index 54cb1eeeed..67f671959d 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 { @@ -85,7 +84,7 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode { private var fetchDisposable = MetaDisposable() private let statusDisposable = MetaDisposable() - private var status: MediaResourceStatus? + private var status: EngineMediaResource.FetchStatus? init(context: AccountContext, presentationData: PresentationData) { self.containerNode = ASDisplayNode() @@ -184,7 +183,7 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode { } private func setupStatus(context: AccountContext, resource: MediaResource) { - self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(resource) + self.statusDisposable.set((context.engine.resources.status(resource: EngineMediaResource(resource)) |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self { let previousStatus = strongSelf.status @@ -321,9 +320,9 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode { if let (context, fileReference) = self.contextAndFile, let status = self.status { switch status { case .Fetching: - context.account.postbox.mediaBox.cancelInteractiveResourceFetch(fileReference.media.resource) + context.engine.resources.cancelInteractiveResourceFetch(id: EngineMediaResource.Id(fileReference.media.resource.id)) case .Remote: - self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start()) + self.fetchDisposable.set(context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file).start()) default: break } @@ -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..6e0913d2c5 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 @@ -250,7 +249,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private let statusDisposable = MetaDisposable() private let dataDisposable = MetaDisposable() private let recognitionDisposable = MetaDisposable() - private var status: MediaResourceStatus? + private var status: EngineMediaResource.FetchStatus? private var fetchedDimensions: PixelDimensions? private let pagingEnabledPromise = ValuePromise(true) @@ -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: @@ -508,7 +507,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { self.zoomableContent = (largestSize.dimensions.cgSize, self.imageNode) - self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: imageReference.resourceReference(largestSize.resource)).start()) + self.fetchDisposable.set(self.context.engine.resources.fetch(reference: imageReference.resourceReference(largestSize.resource), userLocation: userLocation, userContentType: .image).start()) self.setupStatus(resource: largestSize.resource) } else { self._ready.set(.single(Void())) @@ -730,9 +729,9 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { guard let self else { return } - let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media) + let _ = (fetchMediaData(context: context, userLocation: .other, mediaReference: media) |> deliverOnMainQueue).start(next: { [weak self] (value, isImage) in - guard let self, case let .data(data) = value, data.complete, isImage, let image = UIImage(contentsOfFile: data.path) else { + guard let self, case let .data(data) = value, data.isComplete, isImage, let image = UIImage(contentsOfFile: data.path) else { return } let sendSticker = self.sendSticker @@ -755,8 +754,8 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Gallery_SaveImage, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) - - let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: media) + + let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, userLocation: .peer(message.id.peerId), mediaReference: media) |> deliverOnMainQueue).start(completed: { [weak self] in guard let strongSelf = self else { return @@ -770,7 +769,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } } - if let peer, let message = self.message, canSendMessagesToPeer(peer._asPeer()) { + if let peer, let message = self.message, canSendMessagesToPeer(peer) { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in if let self, let navigationController = self.baseNavigationController() { self.beginCustomDismiss(.simpleAnimation) @@ -933,7 +932,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } self._rightBarButtonItems.set(.single(barButtonItems)) - self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: fileReference.resourceReference(fileReference.media.resource)).start()) + self.fetchDisposable.set(self.context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: userLocation, userContentType: .image).start()) } else { let _ = (chatMessageFileDatas(account: context.account, userLocation: userLocation, fileReference: fileReference, progressive: false, fetched: true) |> mapToSignal { value -> Signal in @@ -958,7 +957,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { } private func setupStatus(resource: MediaResource) { - self.statusDisposable.set((self.context.account.postbox.mediaBox.resourceStatus(resource) + self.statusDisposable.set((self.context.engine.resources.status(resource: EngineMediaResource(resource)) |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self { let previousStatus = strongSelf.status @@ -1197,7 +1196,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { if let resource = resource { switch status { case .Fetching: - self.context.account.postbox.mediaBox.cancelInteractiveResourceFetch(resource.resource) + self.context.engine.resources.cancelInteractiveResourceFetch(id: EngineMediaResource.Id(resource.resource.id)) case .Remote: self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .image, reference: resource, statsCategory: statsCategory ?? .generic).start()) default: diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 007a2f5b50..32153dae56 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 } @@ -3702,7 +3704,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { allFiles.append(contentsOf: qualitySet.qualityFiles.values) let qualitySignals = allFiles.map { file -> Signal<(fileId: MediaId, isCached: Bool), NoError> in - return self.context.account.postbox.mediaBox.resourceStatus(file.media.resource) + return self.context.engine.resources.status(resource: EngineMediaResource(file.media.resource)) |> take(1) |> map { status -> (fileId: MediaId, isCached: Bool) in return (file.media.fileId, status == .Local) @@ -3759,7 +3761,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let stringSaved = self.presentationData.strings.Story_TooltipSaved let saveFileReference: AnyMediaReference = qualityFile.abstract - let saveSignal = SaveToCameraRoll.saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: saveFileReference) + let saveSignal = SaveToCameraRoll.saveToCameraRoll(context: self.context, userLocation: .peer(message.id.peerId), mediaReference: saveFileReference) let disposable = (saveSignal |> deliverOnMainQueue).start(next: { [weak saveScreen] progress in @@ -3805,7 +3807,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { switch self.fetchStatus { case .Local: - let _ = (SaveToCameraRoll.saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file)) + let _ = (SaveToCameraRoll.saveToCameraRoll(context: self.context, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: file)) |> deliverOnMainQueue).start(completed: { [weak self] in guard let self else { return @@ -3855,10 +3857,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if let (message, _, _) = strongSelf.contentInfo(), let image = message.media.first(where: { $0 is TelegramMediaImage }) as? TelegramMediaImage, !message.isCopyProtected() && !item.peerIsCopyProtected && message.paidContent == nil { let context = strongSelf.context + var videoReference: AnyMediaReference? + if let video = image.video { + videoReference = .message(message: MessageReference(message), media: video) + } items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Gallery_SaveImage, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.default) - let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: image)) + let _ = (SaveToCameraRoll.saveToCameraRoll(context: context, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: image), video: videoReference) |> deliverOnMainQueue).start(completed: { [weak self] in guard let strongSelf = self else { return @@ -3866,7 +3872,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { guard let controller = strongSelf.galleryController() else { return } - controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_ImageSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) + controller.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .mediaSaved(text: strongSelf.presentationData.strings.Gallery_ImageSaved), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) }) }))) } @@ -3899,7 +3905,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer._asPeer()) { + if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer) { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in if let self, let navigationController = self.baseNavigationController() { self.beginCustomDismiss(.simpleAnimation) diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift index c9812c4e2c..baf3a36d31 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewController.swift @@ -516,7 +516,7 @@ public final class SecretMediaPreviewController: ViewController { var duration: Double = 0.0 for media in message.media { if let file = media as? TelegramMediaFile { - if let path = self.context.account.postbox.mediaBox.completedResourcePath(file.resource) { + if let path = self.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.resource.id)) { let tempFile = TempBox.shared.file(path: path, fileName: file.fileName ?? "file") self.tempFile = tempFile tempFilePath = tempFile.path diff --git a/submodules/GalleryUI/Sources/SecretMediaPreviewFooterContentNode.swift b/submodules/GalleryUI/Sources/SecretMediaPreviewFooterContentNode.swift index f82b066de1..2797b42d66 100644 --- a/submodules/GalleryUI/Sources/SecretMediaPreviewFooterContentNode.swift +++ b/submodules/GalleryUI/Sources/SecretMediaPreviewFooterContentNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import SwiftSignalKit 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/GraphCore/Sources/Charts/Controllers/Lines/GeneralLinesChartController.swift b/submodules/GraphCore/Sources/Charts/Controllers/Lines/GeneralLinesChartController.swift index 4da43fc1a0..cf017eb9a0 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/Lines/GeneralLinesChartController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/Lines/GeneralLinesChartController.swift @@ -34,6 +34,8 @@ public class GeneralLinesChartController: BaseLinesChartController { private var prevoiusHorizontalStrideInterval: Int = 1 private(set) var chartLines: [LinesChartRenderer.LineData] = [] + + public var min5 = false override public init(chartsCollection: ChartsCollection) { self.initialChartCollection = chartsCollection @@ -217,9 +219,18 @@ public class GeneralLinesChartController: BaseLinesChartController { } func updateHorizontalLimits(horizontalRange: ClosedRange, animated: Bool) { - if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange, - scaleType: isZoomed ? .hour : .day, - prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) { + var scaleType: ChartScaleType = .day + if self.min5 { + scaleType = .minutes5 + } else if isZoomed { + scaleType = .hour + } + + if let (stride, labels) = horizontalLimitsLabels( + horizontalRange: horizontalRange, + scaleType: scaleType, + prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval + ) { self.horizontalScalesRenderer.setup(labels: labels, animated: animated) self.prevoiusHorizontalStrideInterval = stride } diff --git a/submodules/GraphUI/Sources/ChartNode.swift b/submodules/GraphUI/Sources/ChartNode.swift index 6391759d9f..bd8d521f40 100644 --- a/submodules/GraphUI/Sources/ChartNode.swift +++ b/submodules/GraphUI/Sources/ChartNode.swift @@ -20,6 +20,7 @@ public enum ChartType { case twoAxis5MinStep case currency case stars + case lines5Min } public extension ChartTheme { @@ -142,6 +143,11 @@ public func createChartController(_ data: String, type: ChartType, rate: Double stepController.min5 = true controller = stepController controller.isZoomable = false + case .lines5Min: + let linesController = GeneralLinesChartController(chartsCollection: collection) + linesController.min5 = true + controller = linesController + controller.isZoomable = false } controller.getDetailsData = { date, completion in getDetailsData(date, { detailsData in diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift index 3eabc0345c..7bb94a7882 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, groupId: nil, 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, groupId: nil, tags: nil, minDate: nil, maxDate: nil, folderId: nil), query: self.query, state: currentSearchState) } self.historyViewDisposable?.dispose() diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift index 6d5dfd858b..4e74271f81 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import SearchBarNode diff --git a/submodules/HorizontalPeerItem/BUILD b/submodules/HorizontalPeerItem/BUILD index 36c2ddebc8..b6ad5e8864 100644 --- a/submodules/HorizontalPeerItem/BUILD +++ b/submodules/HorizontalPeerItem/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/TelegramCore:TelegramCore", - "//submodules/Postbox:Postbox", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift b/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift index d7c20dfd8e..2e095f765e 100644 --- a/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift +++ b/submodules/HorizontalPeerItem/Sources/HorizontalPeerItem.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import TelegramCore -import Postbox import SwiftSignalKit import TelegramPresentationData import TelegramStringFormatting @@ -27,8 +26,7 @@ public final class HorizontalPeerItem: ListViewItem { let strings: PresentationStrings let mode: HorizontalPeerItemMode let accountPeerId: EnginePeer.Id - let postbox: Postbox - let network: Network + let stateManager: AccountStateManager let energyUsageSettings: EnergyUsageSettings let contentSettings: ContentSettings let animationCache: AnimationCache @@ -48,8 +46,7 @@ public final class HorizontalPeerItem: ListViewItem { strings: PresentationStrings, mode: HorizontalPeerItemMode, accountPeerId: EnginePeer.Id, - postbox: Postbox, - network: Network, + stateManager: AccountStateManager, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, @@ -67,8 +64,7 @@ public final class HorizontalPeerItem: ListViewItem { self.strings = strings self.mode = mode self.accountPeerId = accountPeerId - self.postbox = postbox - self.network = network + self.stateManager = stateManager self.energyUsageSettings = energyUsageSettings self.contentSettings = contentSettings self.animationCache = animationCache @@ -228,7 +224,7 @@ public final class HorizontalPeerItemNode: ListViewItemNode { } else { strongSelf.peerNode.compact = false } - strongSelf.peerNode.setup(accountPeerId: item.accountPeerId, postbox: item.postbox, network: item.network, energyUsageSettings: item.energyUsageSettings, contentSettings: item.contentSettings, animationCache: item.animationCache, animationRenderer: item.animationRenderer, resolveInlineStickers: item.resolveInlineStickers, theme: item.theme, strings: item.strings, peer: EngineRenderedPeer(peer: item.peer), requiresPremiumForMessaging: false, numberOfLines: 1, synchronousLoad: synchronousLoads) + strongSelf.peerNode.setup(accountPeerId: item.accountPeerId, stateManager: item.stateManager, energyUsageSettings: item.energyUsageSettings, contentSettings: item.contentSettings, animationCache: item.animationCache, animationRenderer: item.animationRenderer, resolveInlineStickers: item.resolveInlineStickers, theme: item.theme, strings: item.strings, peer: EngineRenderedPeer(peer: item.peer), requiresPremiumForMessaging: false, numberOfLines: 1, synchronousLoad: synchronousLoads) strongSelf.peerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.size) strongSelf.peerNode.updateSelection(selected: item.isPeerSelected(item.peer.id), animated: false) diff --git a/submodules/ICloudResources/Sources/ICloudResources.swift b/submodules/ICloudResources/Sources/ICloudResources.swift index 2ef67e5e9d..9658b88460 100644 --- a/submodules/ICloudResources/Sources/ICloudResources.swift +++ b/submodules/ICloudResources/Sources/ICloudResources.swift @@ -68,6 +68,7 @@ public struct ICloudFileDescription { public let title: String? public let performer: String? public let duration: Int + public let hasAudioArtwork: Bool } public let urlData: String @@ -76,6 +77,51 @@ public struct ICloudFileDescription { public let audioMetadata: AudioMetadata? } +private let audioFileExtensions: Set = ["mp3", "m4a", "aac", "flac"] + +private func validatedAudioArtworkData(_ data: Data?) -> Data? { + guard let data, UIImage(data: data) != nil else { + return nil + } + return data +} + +private func audioArtworkData(from metadataItem: AVMetadataItem) -> Data? { + if let data = validatedAudioArtworkData(metadataItem.value(forKey: "dataValue") as? Data) { + return data + } + if let data = metadataItem.value(forKey: "value") as? Data { + return validatedAudioArtworkData(data) + } + if let data = metadataItem.value(forKey: "value") as? NSData { + return validatedAudioArtworkData(data as Data) + } + return nil +} + +private func audioArtworkData(from asset: AVURLAsset) -> Data? { + func firstArtworkData(in metadataItems: [AVMetadataItem]) -> Data? { + for item in metadataItems { + if item.commonKey == AVMetadataKey.commonKeyArtwork, let data = audioArtworkData(from: item) { + return data + } + } + return nil + } + + if let data = firstArtworkData(in: asset.commonMetadata) { + return data + } + + for format in asset.availableMetadataFormats { + if let data = firstArtworkData(in: asset.metadata(forFormat: format)) { + return data + } + } + + return nil +} + private func descriptionWithUrl(_ url: URL) -> ICloudFileDescription? { if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { guard url.startAccessingSecurityScopedResource() else { @@ -95,17 +141,50 @@ private func descriptionWithUrl(_ url: URL) -> ICloudFileDescription? { } var audioMetadata: ICloudFileDescription.AudioMetadata? - if ["mp3", "m4a"].contains(url.pathExtension.lowercased()) { + let fileExtension = url.pathExtension.lowercased() + var hasAudioArtwork = false + let audioAsset: AVURLAsset? + if audioFileExtensions.contains(fileExtension) { let asset = AVURLAsset(url: url) + audioAsset = asset + hasAudioArtwork = audioArtworkData(from: asset) != nil + } else { + audioAsset = nil + } + if ["mp3", "m4a"].contains(fileExtension), let asset = audioAsset { let title = AVMetadataItem.metadataItems(from: asset.commonMetadata, withKey: AVMetadataKey.commonKeyTitle, keySpace: AVMetadataKeySpace.common).first?.stringValue let performer = AVMetadataItem.metadataItems(from: asset.commonMetadata, withKey: AVMetadataKey.commonKeyArtist, keySpace: AVMetadataKeySpace.common).first?.stringValue let duration = CMTimeGetSeconds(asset.duration) if duration > 0 { - audioMetadata = ICloudFileDescription.AudioMetadata(title: title, performer: performer, duration: Int(duration)) + audioMetadata = ICloudFileDescription.AudioMetadata(title: title, performer: performer, duration: Int(duration), hasAudioArtwork: hasAudioArtwork) + } + } else if fileExtension == "flac", let asset = audioAsset { + var title: String? + var performer: String? + let vorbisComment = AVMetadataFormat(rawValue: "org.xiph.vorbis-comment") + if asset.availableMetadataFormats.contains(vorbisComment) { + let items = asset.metadata(forFormat: vorbisComment) + for item in items { + if item.commonKey == AVMetadataKey.commonKeyTitle { + title = item.stringValue + } + if item.commonKey == AVMetadataKey.commonKeyArtist { + performer = item.stringValue + } + } + } + let duration = CMTimeGetSeconds(asset.duration) + if duration > 0 { + audioMetadata = ICloudFileDescription.AudioMetadata(title: title, performer: performer, duration: Int(duration), hasAudioArtwork: hasAudioArtwork) } } - let result = ICloudFileDescription(urlData: urlData.base64EncodedString(), fileName: fileName, fileSize: fileSize, audioMetadata: audioMetadata) + let result = ICloudFileDescription( + urlData: urlData.base64EncodedString(), + fileName: fileName, + fileSize: fileSize, + audioMetadata: audioMetadata + ) url.stopAccessingSecurityScopedResource() @@ -229,11 +308,17 @@ public func fetchICloudFileResource(resource: ICloudFileResource) -> Signal mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { [weak self] peer in guard let strongSelf = self else { return } - + var signals: [Signal<(UUID, StickerVerificationStatus, EngineMediaResource?), NoError>] = [] for sticker in strongSelf.stickerPack.stickers { if let resource = strongSelf.controllerNode.stickerResources[sticker.uuid] { - signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource._asResource(), thumbnail: nil, alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), duration: nil, mimeType: sticker.mimeType) + signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource, thumbnail: nil, alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), duration: nil, mimeType: sticker.mimeType) |> map { result -> (UUID, StickerVerificationStatus, EngineMediaResource?) in switch result { case .progress: return (sticker.uuid, .loading, nil) case let .complete(resource, mimeType): if ["application/x-tgsticker", "video/webm"].contains(mimeType) { - return (sticker.uuid, .verified, EngineMediaResource(resource)) + return (sticker.uuid, .verified, resource) } else { return (sticker.uuid, .declined, nil) } diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift index fe689873f6..a00b0f3073 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift @@ -623,7 +623,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, ASScroll } if let resource = self.uploadedStickerResources[item.stickerItem.uuid] { if let localResource = item.stickerItem.resource { - self.context.account.postbox.mediaBox.copyResourceData(from: localResource._asResource().id, to: resource._asResource().id) + self.context.engine.resources.copyResourceData(from: localResource.id, to: resource.id) } stickers.append(ImportSticker(resource: .standalone(resource: resource._asResource()), emojis: item.stickerItem.emojis, dimensions: dimensions, duration: nil, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords)) } else if let resource = item.stickerItem.resource { @@ -637,7 +637,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, ASScroll dimensions = PixelDimensions(image.size) } let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnail.data) + self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: thumbnail.data) thumbnailSticker = ImportSticker(resource: .standalone(resource: resource), emojis: [], dimensions: dimensions, duration: nil, mimeType: thumbnail.mimeType, keywords: thumbnail.keywords) } @@ -796,7 +796,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, ASScroll item.resource = resource } else { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: item.data) + self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: item.data) item.resource = EngineMediaResource(resource) self.stickerResources[item.uuid] = EngineMediaResource(resource) } diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index be9aee83dc..74f3824243 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -3,7 +3,6 @@ import CoreLocation import SwiftSignalKit import StoreKit import TelegramCore -import Postbox import TelegramStringFormatting import TelegramUIPreferences import PersistentStringHash @@ -623,9 +622,9 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver { } let id = Int64.random(in: Int64.min ... Int64.max) let fileResource = LocalFileMediaResource(fileId: id, size: Int64(receiptData.count), isSecretRelated: false) - engine.account.postbox.mediaBox.storeResourceData(fileResource.id, data: receiptData) + engine.resources.storeResourceData(id: EngineMediaResource.Id(fileResource.id), data: receiptData) - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(receiptData.count), attributes: [.FileName(fileName: "Receipt.dat")], alternativeRepresentations: []) + let file = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(receiptData.count), attributes: [.FileName(fileName: "Receipt.dat")], alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) let _ = enqueueMessages(account: engine.account, peerId: engine.account.peerId, messages: [message]).start() @@ -663,6 +662,7 @@ private final class PendingInAppPurchaseState: Codable { case restore case phoneNumber case phoneCodeHash + case premiumDays } enum PurposeType: Int32 { @@ -687,7 +687,7 @@ private final class PendingInAppPurchaseState: Codable { case stars(count: Int64, peerId: EnginePeer.Id?) case starsGift(peerId: EnginePeer.Id, count: Int64) case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, users: Int32) - case authCode(restore: Bool, phoneNumber: String, phoneCodeHash: String) + case authCode(restore: Bool, phoneNumber: String, phoneCodeHash: String, premiumDays: Int32) public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -749,7 +749,8 @@ private final class PendingInAppPurchaseState: Codable { self = .authCode( restore: try container.decode(Bool.self, forKey: .restore), phoneNumber: try container.decode(String.self, forKey: .phoneNumber), - phoneCodeHash: try container.decode(String.self, forKey: .phoneCodeHash) + phoneCodeHash: try container.decode(String.self, forKey: .phoneCodeHash), + premiumDays: try container.decode(Int32.self, forKey: .premiumDays), ) default: throw DecodingError.generic @@ -805,11 +806,12 @@ private final class PendingInAppPurchaseState: Codable { try container.encode(randomId, forKey: .randomId) try container.encode(untilDate, forKey: .untilDate) try container.encode(users, forKey: .users) - case let .authCode(restore, phoneNumber, phoneCodeHash): + case let .authCode(restore, phoneNumber, phoneCodeHash, premiumDays): try container.encode(PurposeType.authCode.rawValue, forKey: .type) try container.encode(restore, forKey: .restore) try container.encode(phoneNumber, forKey: .phoneNumber) try container.encode(phoneCodeHash, forKey: .phoneCodeHash) + try container.encode(premiumDays, forKey: .premiumDays) } } @@ -833,8 +835,8 @@ private final class PendingInAppPurchaseState: Codable { self = .starsGift(peerId: peerId, count: count) case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _, users): self = .starsGiveaway(stars: stars, boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, users: users) - case let .authCode(restore, phoneNumber, phoneCodeHash, _, _): - self = .authCode(restore: restore, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash) + case let .authCode(restore, phoneNumber, phoneCodeHash, premiumDays, _, _): + self = .authCode(restore: restore, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, premiumDays: premiumDays) } } @@ -859,8 +861,8 @@ private final class PendingInAppPurchaseState: Codable { return .starsGift(peerId: peerId, count: count, currency: currency, amount: amount) case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, users): return .starsGiveaway(stars: stars, boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount, users: users) - case let .authCode(restore, phoneNumber, phoneCodeHash): - return .authCode(restore: restore, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, currency: currency, amount: amount) + case let .authCode(restore, phoneNumber, phoneCodeHash, premiumDays): + return .authCode(restore: restore, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, premiumDays: premiumDays, currency: currency, amount: amount) } } } diff --git a/submodules/InAppPurchaseManager/Sources/Receipt.swift b/submodules/InAppPurchaseManager/Sources/Receipt.swift index fbb5d1b9d4..f16667bcbd 100644 --- a/submodules/InAppPurchaseManager/Sources/Receipt.swift +++ b/submodules/InAppPurchaseManager/Sources/Receipt.swift @@ -16,97 +16,157 @@ private struct Asn1Entry { let length: Int } -private func parse(_ data: Data, startIndex: Int = 0) -> Asn1Entry { +private func dataByte(_ data: Data, at index: Int) -> UInt8? { + guard index >= 0 && index < data.count else { + return nil + } + return data[index] +} + +private func parse(_ data: Data, startIndex: Int = 0) -> Asn1Entry? { + guard startIndex >= 0 && startIndex < data.count else { + return nil + } + var index = startIndex - var value = data[index] + guard var value = dataByte(data, at: index) else { + return nil + } index += 1 var tagValue = Int32(value & 0x1f) if tagValue == 31 { - value = data[index] - index += 1 - while (value & 0x80) != 0 { + repeat { + guard let nextValue = dataByte(data, at: index) else { + return nil + } + value = nextValue + index += 1 + guard tagValue <= Int32.max >> 8 else { + return nil + } tagValue <<= 8 tagValue |= Int32(value & 0x7f) - value = data[index] - index += 1 - } - tagValue <<= 8 - tagValue |= Int32(value & 0x7f) + } while (value & 0x80) != 0 } var length = 0 - var nextTag = 0 - value = data[index] + guard let lengthValue = dataByte(data, at: index) else { + return nil + } + value = lengthValue + let isIndefiniteLength = value == 0x80 index += 1 - if value & 0x80 == 0 { + if !isIndefiniteLength && value & 0x80 == 0 { length = Int(value) - nextTag = index + length - } else if value != 0x80 { + } else if !isIndefiniteLength { let octetsCount = Int(value & 0x7f) + guard octetsCount > 0 else { + return nil + } for _ in 0 ..< octetsCount { + guard length <= Int.max >> 8 else { + return nil + } length <<= 8 - value = data[index] + guard let nextValue = dataByte(data, at: index) else { + return nil + } + value = nextValue index += 1 length |= Int(value) & 0xff } - nextTag = index + length } else { var scanIndex = index - while data[scanIndex] != 0 && data[scanIndex + 1] != 0 { + while true { + guard scanIndex + 1 < data.count else { + return nil + } + if data[scanIndex] == 0 && data[scanIndex + 1] == 0 { + break + } scanIndex += 1 } length = scanIndex - index - nextTag = scanIndex + 2 } - return Asn1Entry(tag: tagValue, data: data.subdata(in: index ..< (index + length)), length: nextTag - startIndex) + + guard length >= 0, length <= data.count - index else { + return nil + } + + let payloadEndIndex = index + length + let entryEndIndex: Int + if isIndefiniteLength { + entryEndIndex = payloadEndIndex + 2 + guard entryEndIndex <= data.count else { + return nil + } + } else { + entryEndIndex = payloadEndIndex + } + + return Asn1Entry(tag: tagValue, data: data.subdata(in: index ..< payloadEndIndex), length: entryEndIndex - startIndex) } -private func parseSequence(_ data: Data) -> [Asn1Entry] { +private func parseSequence(_ data: Data) -> [Asn1Entry]? { var result : [Asn1Entry] = [] var index = 0 while index < data.count { - let entry = parse(data, startIndex: index) + guard let entry = parse(data, startIndex: index), entry.length > 0 else { + return nil + } result.append(entry) index += entry.length } return result } -private func parseInteger(_ data: Data) -> Int32 { - let length = data.count - var value: Int32 = 0 - for i in 0 ..< length { - if i == 0 { - value = Int32(data[i] & 0x7f) - } else { - value <<= 8 - value |= Int32(data[i]) - } +private func parseInteger(_ data: Data) -> Int32? { + guard !data.isEmpty, data.count <= MemoryLayout.size else { + return nil } - if length > 0 && data[0] & 0x80 != 0 { - let complement: Int32 = 1 << (length * 8) - value -= complement + + var value: UInt32 = 0 + for byte in data { + value = (value << 8) | UInt32(byte) } - return value + + if let firstByte = data.first, firstByte & 0x80 != 0 && data.count < MemoryLayout.size { + let shift = UInt32(data.count * 8) + value |= ~UInt32(0) << shift + } + + return Int32(bitPattern: value) } -private func parseObjectIdentifier(_ data: Data, startIndex: Int = 0, length: Int? = nil) -> [Int32] { +private func parseObjectIdentifier(_ data: Data, startIndex: Int = 0, length: Int? = nil) -> [Int32]? { let dataLen = length ?? data.count + guard startIndex >= 0, dataLen > 0, startIndex <= data.count, dataLen <= data.count - startIndex else { + return nil + } + + let endIndex = startIndex + dataLen var index = startIndex var identifier: [Int32] = [] - while index < startIndex + dataLen { - var subidentifier: Int32 = 0 - var value = data[index] - index += 1 - while (value & 0x80) != 0 { - subidentifier <<= 7 - subidentifier |= Int32(value & 0x7f) - value = data[index] + while index < endIndex { + var subidentifier: Int64 = 0 + while true { + guard let value = dataByte(data, at: index) else { + return nil + } index += 1 + guard subidentifier <= Int64(Int32.max >> 7) else { + return nil + } + subidentifier <<= 7 + subidentifier |= Int64(value & 0x7f) + if (value & 0x80) == 0 { + break + } + guard index < endIndex else { + return nil + } } - subidentifier <<= 7 - subidentifier |= Int32(value & 0x7f) - identifier.append(subidentifier) + identifier.append(Int32(subidentifier)) } return identifier } @@ -137,51 +197,71 @@ struct Receipt { } func parseReceipt(_ data: Data) -> Receipt? { - let root = parseSequence(data) + guard let root = parseSequence(data) else { + return nil + } guard root.count == 1 && root[0].tag == Asn1Tag.sequence else { return nil } - let rootSeq = parseSequence(root[0].data) + guard let rootSeq = parseSequence(root[0].data) else { + return nil + } guard rootSeq.count == 2 && rootSeq[0].tag == Asn1Tag.objectIdentifier && parseObjectIdentifier(rootSeq[0].data) == ObjectIdentifier.pkcs7SignedData else { return nil } - let signedData = parseSequence(rootSeq[1].data) + guard let signedData = parseSequence(rootSeq[1].data) else { + return nil + } guard signedData.count == 1 && signedData[0].tag == Asn1Tag.sequence else { return nil } - let signedDataSeq = parseSequence(signedData[0].data) + guard let signedDataSeq = parseSequence(signedData[0].data) else { + return nil + } guard signedDataSeq.count > 3 && signedDataSeq[2].tag == Asn1Tag.sequence else { return nil } - let contentData = parseSequence(signedDataSeq[2].data) + guard let contentData = parseSequence(signedDataSeq[2].data) else { + return nil + } guard contentData.count == 2 && contentData[0].tag == Asn1Tag.objectIdentifier && parseObjectIdentifier(contentData[0].data) == ObjectIdentifier.pkcs7Data else { return nil } - let payload = parse(contentData[1].data) + guard let payload = parse(contentData[1].data) else { + return nil + } guard payload.tag == Asn1Tag.octetString else { return nil } - let payloadRoot = parse(payload.data) + guard let payloadRoot = parse(payload.data) else { + return nil + } guard payloadRoot.tag == Asn1Tag.set else { return nil } var purchases: [Receipt.Purchase] = [] - let receiptAttributes = parseSequence(payloadRoot.data) + guard let receiptAttributes = parseSequence(payloadRoot.data) else { + return nil + } for attribute in receiptAttributes { if attribute.tag != Asn1Tag.sequence { continue } - let attributeEntries = parseSequence(attribute.data) + guard let attributeEntries = parseSequence(attribute.data) else { + return nil + } guard attributeEntries.count == 3 && attributeEntries[0].tag == Asn1Tag.integer && attributeEntries[1].tag == Asn1Tag.integer && attributeEntries[2].tag == Asn1Tag.octetString else { return nil } - let type = parseInteger(attributeEntries[0].data) + guard let type = parseInteger(attributeEntries[0].data) else { + return nil + } let value = attributeEntries[2].data switch (type) { case Receipt.Tag.purchases: @@ -202,22 +282,41 @@ private func parseRfc3339Date(_ str: String) -> Date? { formatter1.locale = posixLocale formatter1.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssX5" formatter1.timeZone = TimeZone(secondsFromGMT: 0) - - let result = formatter1.date(from: str) + + var result = formatter1.date(from: str) if result != nil { return result } - + let formatter2 = DateFormatter() formatter2.locale = posixLocale formatter2.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSSSSSX5" formatter2.timeZone = TimeZone(secondsFromGMT: 0) + + result = formatter2.date(from: str) + if result != nil { + return result + } - return formatter2.date(from: str) + let formatterWithFractionalSeconds = ISO8601DateFormatter() + formatterWithFractionalSeconds.timeZone = TimeZone(secondsFromGMT: 0) + formatterWithFractionalSeconds.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + + if let result = formatterWithFractionalSeconds.date(from: str) { + return result + } + + let formatterWithoutFractionalSeconds = ISO8601DateFormatter() + formatterWithoutFractionalSeconds.timeZone = TimeZone(secondsFromGMT: 0) + formatterWithoutFractionalSeconds.formatOptions = [.withInternetDateTime] + + return formatterWithoutFractionalSeconds.date(from: str) } private func parsePurchaseAttributes(_ data: Data) -> Receipt.Purchase? { - let root = parse(data) + guard let root = parse(data) else { + return nil + } guard root.tag == Asn1Tag.set else { return nil } @@ -226,26 +325,38 @@ private func parsePurchaseAttributes(_ data: Data) -> Receipt.Purchase? { var transactionId: String? var expirationDate: Date? - let receiptAttributes = parseSequence(root.data) + guard let receiptAttributes = parseSequence(root.data) else { + return nil + } for attribute in receiptAttributes { if attribute.tag != Asn1Tag.sequence { continue } - let attributeEntries = parseSequence(attribute.data) + guard let attributeEntries = parseSequence(attribute.data) else { + return nil + } guard attributeEntries.count == 3 && attributeEntries[0].tag == Asn1Tag.integer && attributeEntries[1].tag == Asn1Tag.integer && attributeEntries[2].tag == Asn1Tag.octetString else { return nil } - let type = parseInteger(attributeEntries[0].data) + guard let type = parseInteger(attributeEntries[0].data) else { + return nil + } let value = attributeEntries[2].data switch (type) { case Receipt.Purchase.Tag.productIdentifier: - let valEntry = parse(value) + guard let valEntry = parse(value) else { + return nil + } guard valEntry.tag == Asn1Tag.utf8String else { return nil } productId = String(bytes: valEntry.data, encoding: .utf8) case Receipt.Purchase.Tag.transactionIdentifier: - let valEntry = parse(value) + guard let valEntry = parse(value) else { + return nil + } guard valEntry.tag == Asn1Tag.utf8String else { return nil } transactionId = String(bytes: valEntry.data, encoding: .utf8) case Receipt.Purchase.Tag.expirationDate: - let valEntry = parse(value) + guard let valEntry = parse(value) else { + return nil + } guard valEntry.tag == Asn1Tag.date else { return nil } expirationDate = parseRfc3339Date(String(bytes: valEntry.data, encoding: .utf8) ?? "") default: diff --git a/submodules/InstantPageUI/BUILD b/submodules/InstantPageUI/BUILD index 52f8e2ec5f..91e9d5a2bf 100644 --- a/submodules/InstantPageUI/BUILD +++ b/submodules/InstantPageUI/BUILD @@ -12,11 +12,13 @@ swift_library( deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/CheckNode:CheckNode", "//submodules/Display:Display", "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/TextFormat:TextFormat", "//submodules/GalleryUI:GalleryUI", "//submodules/MusicAlbumArtResources:MusicAlbumArtResources", "//submodules/LiveLocationPositionNode:LiveLocationPositionNode", @@ -27,6 +29,7 @@ swift_library( "//submodules/UndoUI:UndoUI", "//submodules/TranslateUI:TranslateUI", "//submodules/Tuples:Tuples", + "//third-party/SwiftMath:SwiftMath", ], visibility = [ "//visibility:public", diff --git a/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift b/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift index 1ed0820c7b..cd1fc49ef7 100644 --- a/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift +++ b/submodules/InstantPageUI/Sources/InstantImageGalleryItem.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import AccountContext @@ -183,7 +182,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { }) } else { self.imageNode.setSignal(chatMessagePhoto(postbox: self.context.account.postbox, userLocation: userLocation, photoReference: imageReference), dispatchOnDisplayLink: false) - self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: imageReference.resourceReference(largestSize.resource)).start()) + self.fetchDisposable.set(self.context.engine.resources.fetch(reference: imageReference.resourceReference(largestSize.resource), userLocation: userLocation, userContentType: .image).start()) } } else { self._ready.set(.single(Void())) @@ -339,7 +338,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode { if let (context, media) = self.contextAndMedia, let fileReference = media.concrete(TelegramMediaFile.self) { if isVisible { - self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: self.userLocation ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start()) + self.fetchDisposable.set(context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: self.userLocation ?? .other, userContentType: .file).start()) } else { self.fetchDisposable.set(nil) } diff --git a/submodules/InstantPageUI/Sources/InstantPageChecklistMarkerItem.swift b/submodules/InstantPageUI/Sources/InstantPageChecklistMarkerItem.swift new file mode 100644 index 0000000000..ea145d81a2 --- /dev/null +++ b/submodules/InstantPageUI/Sources/InstantPageChecklistMarkerItem.swift @@ -0,0 +1,101 @@ +import Foundation +import UIKit +import TelegramCore +import AsyncDisplayKit +import Display +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import ContextUI +import CheckNode + +final class InstantPageChecklistMarkerItem: InstantPageItem { + var frame: CGRect + let checked: Bool + + let wantsNode: Bool = true + let separatesTiles: Bool = false + let medias: [InstantPageMedia] = [] + + init(frame: CGRect, checked: Bool) { + self.frame = frame + self.checked = checked + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func drawInTile(context: CGContext) { + } + + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? { + return InstantPageChecklistMarkerNode(theme: theme, checked: self.checked) + } + + func matchesNode(_ node: InstantPageNode) -> Bool { + if let node = node as? InstantPageChecklistMarkerNode { + return node.checked == self.checked + } else { + return false + } + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + return [] + } + + func distanceThresholdGroup() -> Int? { + return nil + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + return 0.0 + } +} + +private func instantPageChecklistMarkerTheme(theme: InstantPageTheme) -> CheckNodeTheme { + return CheckNodeTheme( + backgroundColor: theme.panelAccentColor, + strokeColor: theme.pageBackgroundColor, + borderColor: theme.controlColor, + overlayBorder: false, + hasInset: false, + hasShadow: false + ) +} + +final class InstantPageChecklistMarkerNode: ASDisplayNode, InstantPageNode { + let checked: Bool + private let checkNode: CheckNode + + init(theme: InstantPageTheme, checked: Bool) { + self.checked = checked + self.checkNode = CheckNode(theme: instantPageChecklistMarkerTheme(theme: theme), content: .check(isRectangle: true)) + + super.init() + + self.isUserInteractionEnabled = false + self.checkNode.isUserInteractionEnabled = false + self.addSubnode(self.checkNode) + self.checkNode.setSelected(checked, animated: false) + } + + func updateIsVisible(_ isVisible: Bool) { + } + + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + return nil + } + + func updateHiddenMedia(media: InstantPageMedia?) { + } + + func update(strings: PresentationStrings, theme: InstantPageTheme) { + self.checkNode.theme = instantPageChecklistMarkerTheme(theme: theme) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(node: self.checkNode, frame: CGRect(origin: .zero, size: size)) + } +} diff --git a/submodules/InstantPageUI/Sources/InstantPageContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageContentNode.swift index 71505c13ee..debdbc8044 100644 --- a/submodules/InstantPageUI/Sources/InstantPageContentNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageContentNode.swift @@ -9,7 +9,7 @@ import TelegramUIPreferences import AccountContext import ContextUI -public final class InstantPageContentNode : ASDisplayNode { +public final class InstantPageContentNode : ASDisplayNode, InstantPageExternalMediaDimensionsNode { private let context: AccountContext private let strings: PresentationStrings private let nameDisplayOrder: PresentationPersonNameOrder @@ -29,13 +29,20 @@ public final class InstantPageContentNode : ASDisplayNode { var distanceThresholdGroupCount: [Int: Int] = [:] var visibleTiles: [Int: InstantPageTileNode] = [:] - var visibleItemsWithNodes: [Int: InstantPageNode] = [:] + public var visibleItemsWithNodes: [Int: InstantPageNode] = [:] var currentWebEmbedHeights: [Int : CGFloat] = [:] - var currentExpandedDetails: [Int : Bool]? + public var currentExpandedDetails: [Int : Bool]? var currentDetailsItems: [InstantPageDetailsItem] = [] var requestLayoutUpdate: ((Bool) -> Void)? + public var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? { + didSet { + for (_, itemNode) in self.visibleItemsWithNodes { + self.applyExternalMediaDimensionsUpdater(to: itemNode) + } + } + } var currentLayout: InstantPageLayout let contentSize: CGSize @@ -223,6 +230,7 @@ public final class InstantPageContentNode : ASDisplayNode { topNode = newNode self.visibleItemsWithNodes[itemIndex] = newNode itemNode = newNode + self.applyExternalMediaDimensionsUpdater(to: newNode) if let itemNode = itemNode as? InstantPageDetailsNode { itemNode.requestLayoutUpdate = { [weak self] animated in @@ -303,6 +311,16 @@ public final class InstantPageContentNode : ASDisplayNode { } } + private func applyExternalMediaDimensionsUpdater(to itemNode: InstantPageNode) { + if let itemNode = itemNode as? InstantPageImageNode { + itemNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions + } else if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.contentNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions + } else if let itemNode = itemNode as? InstantPageSlideshowNode { + itemNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions + } + } + private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) { // let currentHeight = self.currentWebEmbedHeights[index] // if height != currentHeight { diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index 528b6efceb..8df30b2e86 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 @@ -66,6 +65,8 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { var currentWebEmbedHeights: [Int : CGFloat] = [:] var currentExpandedDetails: [Int : Bool]? var currentDetailsItems: [InstantPageDetailsItem] = [] + private var resolvedExternalMediaDimensions: [MediaId: PixelDimensions] = [:] + private var pendingResolvedExternalMediaDimensions = Set() var currentAccessibilityAreas: [AccessibilityAreaNode] = [] @@ -80,6 +81,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { private let loadProgressDisposable = MetaDisposable() private let updateLayoutDisposable = MetaDisposable() + private let updateExternalMediaDimensionsDisposable = MetaDisposable() private var themeReferenceDate: Date? @@ -142,14 +144,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 +159,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 +201,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { }), nil) } }) - } + })) strongSelf.present(shareController, nil) } } @@ -227,6 +227,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { self.resolveUrlDisposable.dispose() self.loadWebpageDisposable.dispose() self.loadProgressDisposable.dispose() + self.updateExternalMediaDimensionsDisposable.dispose() } func update(settings: InstantPagePresentationSettings, themeSettings: PresentationThemeSettings?, strings: PresentationStrings) { @@ -365,6 +366,8 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { } else { self.webPage = nil } + self.resolvedExternalMediaDimensions.removeAll() + self.pendingResolvedExternalMediaDimensions.removeAll() if let anchor = anchor { self.initialAnchor = anchor.removingPercentEncoding } else if let state = state { @@ -468,17 +471,12 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { } private func updateLayout() { - guard let containerLayout = self.containerLayout, let (webPage, instantPage) = self.webPage, let theme = self.theme else { + guard let containerLayout = self.containerLayout, let (webPage, instantPage) = self.resolvedWebPage(), let theme = self.theme else { return } let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme, dateTimeFormat: self.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights) - for (_, tileNode) in self.visibleTiles { - tileNode.removeFromSupernode() - } - self.visibleTiles.removeAll() - let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: containerLayout.size.width) var currentDetailsItems: [InstantPageDetailsItem] = [] @@ -668,6 +666,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { topNode = newNode self.visibleItemsWithNodes[itemIndex] = newNode itemNode = newNode + self.configureExternalMediaDimensionsUpdates(for: newNode) if let itemNode = itemNode as? InstantPageDetailsNode { itemNode.requestLayoutUpdate = { [weak self] animated in @@ -691,6 +690,10 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { } } + if let itemNode = itemNode { + self.configureExternalMediaDimensionsUpdates(for: itemNode) + } + if let itemNode = itemNode as? InstantPageDetailsNode { itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated) } @@ -721,8 +724,11 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { topNode = tileNode self.visibleTiles[tileIndex] = tileNode } else { - if visibleTiles[tileIndex]!.frame != tileFrame { - transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame) + if let tileNode = self.visibleTiles[tileIndex] { + tileNode.update(tile: tile, backgroundColor: theme.pageBackgroundColor) + if tileNode.frame != tileFrame { + transition.updateFrame(node: tileNode, frame: tileFrame) + } } } } @@ -1023,20 +1029,402 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { return nil } + private func configureExternalMediaDimensionsUpdates(for itemNode: InstantPageNode) { + let update: (MediaId, PixelDimensions) -> Void = { [weak self] mediaId, dimensions in + self?.updateExternalMediaDimensions(mediaId, dimensions) + } + if let itemNode = itemNode as? InstantPageExternalMediaDimensionsNode { + itemNode.updateExternalMediaDimensions = update + } + if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.contentNode.updateExternalMediaDimensions = update + } + } + + private func updateExternalMediaDimensions(_ mediaId: MediaId, _ dimensions: PixelDimensions) { + if self.resolvedExternalMediaDimensions[mediaId] == dimensions { + return + } + self.resolvedExternalMediaDimensions[mediaId] = dimensions + self.pendingResolvedExternalMediaDimensions.insert(mediaId) + + let signal: Signal = (.complete() |> delay(0.08, queue: Queue.mainQueue())) + self.updateExternalMediaDimensionsDisposable.set(signal.start(completed: { [weak self] in + self?.relayoutForResolvedExternalMediaDimensions() + })) + } + + private func relayoutForResolvedExternalMediaDimensions() { + guard !self.pendingResolvedExternalMediaDimensions.isEmpty else { + return + } + + let mediaIds = Array(self.pendingResolvedExternalMediaDimensions) + self.pendingResolvedExternalMediaDimensions.removeAll() + + let detailsStateMaps = self.captureExpandedDetailsStateMaps() + let viewportTop = self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentInset.top + var oldFrames: [MediaId: CGRect] = [:] + for mediaId in mediaIds { + if let frame = self.effectiveFrameForMedia(mediaId, detailsStateMaps: detailsStateMaps) { + oldFrames[mediaId] = frame + } + } + + self.updateLayout() + + var newFrames: [MediaId: CGRect] = [:] + for mediaId in mediaIds { + if let frame = self.effectiveFrameForMedia(mediaId, detailsStateMaps: detailsStateMaps) { + newFrames[mediaId] = frame + } + } + + if let compensatedViewportTop = self.compensatedViewportTop(oldFrames: oldFrames, newFrames: newFrames, viewportTop: viewportTop) { + self.setViewportTop(compensatedViewportTop) + } + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) + } + + private func setViewportTop(_ viewportTop: CGFloat) { + let scrollView = self.scrollNode.view + let minOffsetY = -scrollView.contentInset.top + let maxOffsetY = max(minOffsetY, scrollView.contentSize.height - scrollView.bounds.height + scrollView.contentInset.bottom) + let contentOffsetY = min(max(viewportTop - scrollView.contentInset.top, minOffsetY), maxOffsetY) + if contentOffsetY.isFinite { + let contentOffset = CGPoint(x: scrollView.contentOffset.x, y: contentOffsetY) + scrollView.contentOffset = contentOffset + self.previousContentOffset = contentOffset + self.updateNavigationBar() + } + } + + private func compensatedViewportTop(oldFrames: [MediaId: CGRect], newFrames: [MediaId: CGRect], viewportTop: CGFloat) -> CGFloat? { + var pairedFrames: [(old: CGRect, new: CGRect)] = [] + for (mediaId, oldFrame) in oldFrames { + if let newFrame = newFrames[mediaId] { + pairedFrames.append((oldFrame, newFrame)) + } + } + if pairedFrames.isEmpty { + return nil + } + + if let intersecting = pairedFrames + .filter({ $0.old.height > 0.0 && $0.new.height > 0.0 && viewportTop > $0.old.minY && viewportTop < $0.old.maxY }) + .max(by: { $0.old.minY < $1.old.minY }) { + let ratio = min(max((viewportTop - intersecting.old.minY) / intersecting.old.height, 0.0), 1.0) + return intersecting.new.minY + ratio * intersecting.new.height + } + + if let above = pairedFrames + .filter({ viewportTop >= $0.old.maxY }) + .max(by: { $0.old.maxY < $1.old.maxY }) { + return viewportTop + (above.new.maxY - above.old.maxY) + } + + return nil + } + + private func captureExpandedDetailsStateMaps() -> [String: [Int: Bool]] { + guard let currentLayout = self.currentLayout else { + return [:] + } + var result: [String: [Int: Bool]] = [:] + self.captureExpandedDetailsStateMaps(items: currentLayout.items, visibleItemsWithNodes: self.visibleItemsWithNodes, path: [], result: &result) + return result + } + + private func captureExpandedDetailsStateMaps(items: [InstantPageItem], visibleItemsWithNodes: [Int: InstantPageNode], path: [Int], result: inout [String: [Int: Bool]]) { + let detailsNodes = visibleItemsWithNodes.compactMap { $0.value as? InstantPageDetailsNode } + + var detailsIndex = -1 + for item in items { + guard let detailsItem = item as? InstantPageDetailsItem else { + continue + } + detailsIndex += 1 + + guard let detailsNode = detailsNodes.first(where: { $0.item === detailsItem }) else { + continue + } + let nextPath = path + [detailsIndex] + result[self.detailsStateKey(nextPath)] = detailsNode.contentNode.currentExpandedDetails ?? [:] + self.captureExpandedDetailsStateMaps(items: detailsItem.items, visibleItemsWithNodes: detailsNode.contentNode.visibleItemsWithNodes, path: nextPath, result: &result) + } + } + + private func detailsStateKey(_ path: [Int]) -> String { + if path.isEmpty { + return "" + } + return path.map(String.init).joined(separator: ".") + } + + private func effectiveFrameForMedia(_ mediaId: MediaId, detailsStateMaps: [String: [Int: Bool]]) -> CGRect? { + guard let currentLayout = self.currentLayout else { + return nil + } + return self.effectiveFrameForMedia(mediaId, items: currentLayout.items, origin: .zero, expandedDetails: self.currentExpandedDetails, path: [], detailsStateMaps: detailsStateMaps) + } + + private func effectiveFrameForMedia(_ mediaId: MediaId, items: [InstantPageItem], origin: CGPoint, expandedDetails: [Int: Bool]?, path: [Int], detailsStateMaps: [String: [Int: Bool]]) -> CGRect? { + var collapseOffset: CGFloat = 0.0 + var detailsIndex = -1 + + for item in items { + if item is InstantPageDetailsItem { + detailsIndex += 1 + } + + var itemFrame = item.frame.offsetBy(dx: origin.x, dy: origin.y - collapseOffset) + if let detailsItem = item as? InstantPageDetailsItem { + let nextPath = path + [detailsIndex] + let nestedExpandedDetails = detailsStateMaps[self.detailsStateKey(nextPath)] + let expanded = expandedDetails?[detailsIndex] ?? detailsItem.initiallyExpanded + let height = expanded ? detailsItem.titleHeight + self.effectiveContentHeight(items: detailsItem.items, baseHeight: detailsItem.frame.height - detailsItem.titleHeight, expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) : detailsItem.titleHeight + collapseOffset += item.frame.height - height + itemFrame.size.height = height + + if expanded, let nestedFrame = self.effectiveFrameForMedia(mediaId, items: detailsItem.items, origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + detailsItem.titleHeight), expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) { + return nestedFrame + } + continue + } + + if self.itemContainsMedia(item, mediaId: mediaId) { + return itemFrame + } + } + + return nil + } + + private func effectiveContentHeight(items: [InstantPageItem], baseHeight: CGFloat, expandedDetails: [Int: Bool]?, path: [Int], detailsStateMaps: [String: [Int: Bool]]) -> CGFloat { + var contentHeight = baseHeight + var detailsIndex = -1 + + for item in items { + guard let detailsItem = item as? InstantPageDetailsItem else { + continue + } + detailsIndex += 1 + + let nextPath = path + [detailsIndex] + let nestedExpandedDetails = detailsStateMaps[self.detailsStateKey(nextPath)] + let expanded = expandedDetails?[detailsIndex] ?? detailsItem.initiallyExpanded + let height = expanded ? detailsItem.titleHeight + self.effectiveContentHeight(items: detailsItem.items, baseHeight: detailsItem.frame.height - detailsItem.titleHeight, expandedDetails: nestedExpandedDetails, path: nextPath, detailsStateMaps: detailsStateMaps) : detailsItem.titleHeight + contentHeight += -detailsItem.frame.height + height + } + + return contentHeight + } + + private func itemContainsMedia(_ item: InstantPageItem, mediaId: MediaId) -> Bool { + for media in item.medias { + if media.media.id == mediaId { + return true + } + } + return false + } + + private func resolvedWebPage() -> (webPage: TelegramMediaWebpage, instantPage: InstantPage?)? { + guard let (webPage, instantPage) = self.webPage else { + return nil + } + guard !self.resolvedExternalMediaDimensions.isEmpty, case let .Loaded(content) = webPage.content else { + return (webPage, instantPage) + } + + var instantPageUpdated = false + var effectiveInstantPage = instantPage + if let instantPage { + var media = instantPage.media + for (mediaId, currentMedia) in instantPage.media { + if let updatedMedia = self.updatedMediaIfNeeded(currentMedia) { + media[mediaId] = updatedMedia + instantPageUpdated = true + } + } + if instantPageUpdated { + effectiveInstantPage = InstantPage(blocks: instantPage.blocks, media: media, isComplete: instantPage.isComplete, rtl: instantPage.rtl, url: instantPage.url, views: instantPage.views) + } + } + + var imageUpdated = false + let effectiveImage = content.image.map { image -> TelegramMediaImage in + if let updated = self.updatedImageIfNeeded(image) { + imageUpdated = true + return updated + } else { + return image + } + } + + var fileUpdated = false + let effectiveFile = content.file.map { file -> TelegramMediaFile in + if let updated = self.updatedFileIfNeeded(file) { + fileUpdated = true + return updated + } else { + return file + } + } + + if !instantPageUpdated && !imageUpdated && !fileUpdated { + return (webPage, instantPage) + } + + let effectiveContent = TelegramMediaWebpageLoadedContent( + url: content.url, + displayUrl: content.displayUrl, + hash: content.hash, + type: content.type, + websiteName: content.websiteName, + title: content.title, + text: content.text, + embedUrl: content.embedUrl, + embedType: content.embedType, + embedSize: content.embedSize, + duration: content.duration, + author: content.author, + isMediaLargeByDefault: content.isMediaLargeByDefault, + imageIsVideoCover: content.imageIsVideoCover, + image: effectiveImage, + file: effectiveFile, + story: content.story, + attributes: content.attributes, + instantPage: effectiveInstantPage + ) + return (TelegramMediaWebpage(webpageId: webPage.webpageId, content: .Loaded(effectiveContent)), effectiveInstantPage) + } + + private func updatedMediaIfNeeded(_ media: Media) -> Media? { + if let image = media as? TelegramMediaImage { + return self.updatedImageIfNeeded(image) + } else if let file = media as? TelegramMediaFile { + return self.updatedFileIfNeeded(file) + } else { + return nil + } + } + + private func updatedImageIfNeeded(_ image: TelegramMediaImage) -> TelegramMediaImage? { + guard let dimensions = self.resolvedExternalMediaDimensions[image.imageId] else { + return nil + } + + var updatedRepresentations = image.representations + var didUpdate = false + for i in 0 ..< updatedRepresentations.count { + let representation = updatedRepresentations[i] + guard representation.resource is InstantPageExternalMediaResource, representation.dimensions != dimensions else { + continue + } + updatedRepresentations[i] = TelegramMediaImageRepresentation( + dimensions: dimensions, + resource: representation.resource, + progressiveSizes: representation.progressiveSizes, + immediateThumbnailData: representation.immediateThumbnailData, + hasVideo: representation.hasVideo, + isPersonal: representation.isPersonal, + typeHint: representation.typeHint + ) + didUpdate = true + } + + guard didUpdate else { + return nil + } + return TelegramMediaImage( + imageId: image.imageId, + representations: updatedRepresentations, + videoRepresentations: image.videoRepresentations, + immediateThumbnailData: image.immediateThumbnailData, + emojiMarkup: image.emojiMarkup, + reference: image.reference, + partialReference: image.partialReference, + flags: image.flags, + video: image.video + ) + } + + private func updatedFileIfNeeded(_ file: TelegramMediaFile) -> TelegramMediaFile? { + guard let dimensions = self.resolvedExternalMediaDimensions[file.fileId], file.resource is InstantPageExternalMediaResource else { + return nil + } + + let (attributes, didUpdate) = self.fileAttributesWithResolvedDimensions(file.attributes, dimensions: dimensions) + guard didUpdate else { + return nil + } + + return TelegramMediaFile( + fileId: file.fileId, + partialReference: file.partialReference, + resource: file.resource, + previewRepresentations: file.previewRepresentations, + videoThumbnails: file.videoThumbnails, + videoCover: file.videoCover, + immediateThumbnailData: file.immediateThumbnailData, + mimeType: file.mimeType, + size: file.size, + attributes: attributes, + alternativeRepresentations: file.alternativeRepresentations + ) + } + + private func fileAttributesWithResolvedDimensions(_ attributes: [TelegramMediaFileAttribute], dimensions: PixelDimensions) -> ([TelegramMediaFileAttribute], Bool) { + var updatedAttributes: [TelegramMediaFileAttribute] = [] + var didUpdate = false + var hasSizeAttribute = false + + for attribute in attributes { + switch attribute { + case let .ImageSize(size): + hasSizeAttribute = true + if size != dimensions { + updatedAttributes.append(.ImageSize(size: dimensions)) + didUpdate = true + } else { + updatedAttributes.append(attribute) + } + case let .Video(duration, size, flags, preloadSize, coverTime, videoCodec): + hasSizeAttribute = true + if size != dimensions { + updatedAttributes.append(.Video(duration: duration, size: dimensions, flags: flags, preloadSize: preloadSize, coverTime: coverTime, videoCodec: videoCodec)) + didUpdate = true + } else { + updatedAttributes.append(attribute) + } + default: + updatedAttributes.append(attribute) + } + } + + if !hasSizeAttribute { + updatedAttributes.append(.ImageSize(size: dimensions)) + didUpdate = true + } + + return (updatedAttributes, didUpdate) + } + private func longPressMedia(_ media: InstantPageMedia) { let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in if let strongSelf = self, case let .image(image) = media.media { let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() + let _ = copyToPasteboard(context: strongSelf.context, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() } }), ContextMenuAction(content: .text(title: self.strings.Conversation_LinkDialogSave, accessibilityLabel: self.strings.Conversation_LinkDialogSave), action: { [weak self] in if let strongSelf = self, case let .image(image) = media.media { let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() + let _ = saveToCameraRoll(context: strongSelf.context, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() } }), 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 +1538,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) } })] @@ -1375,7 +1763,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate { let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)) |> deliverOnMainQueue).start(next: { peer in if let strongSelf = self, let peer = peer { - if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { strongSelf.getNavigationController()?.pushViewController(controller) } } diff --git a/submodules/InstantPageUI/Sources/InstantPageFormulaItem.swift b/submodules/InstantPageUI/Sources/InstantPageFormulaItem.swift new file mode 100644 index 0000000000..3c6d8d1cbb --- /dev/null +++ b/submodules/InstantPageUI/Sources/InstantPageFormulaItem.swift @@ -0,0 +1,185 @@ +import Foundation +import UIKit +import TelegramCore +import AsyncDisplayKit +import Display +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import ContextUI + +final class InstantPageFormulaNode: ASImageNode, InstantPageNode { + let attachment: InstantPageMathAttachment + + init(attachment: InstantPageMathAttachment) { + self.attachment = attachment + super.init() + + self.isUserInteractionEnabled = false + self.displaysAsynchronously = false + self.image = attachment.rendered.image + } + + func updateIsVisible(_ isVisible: Bool) { + } + + func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + return nil + } + + func updateHiddenMedia(media: InstantPageMedia?) { + } + + func update(strings: PresentationStrings, theme: InstantPageTheme) { + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + } +} + +final class InstantPageTextFormulaItem: InstantPageItem { + var frame: CGRect + let attachment: InstantPageMathAttachment + + let wantsNode: Bool = true + let separatesTiles: Bool = false + let medias: [InstantPageMedia] = [] + + init(frame: CGRect, attachment: InstantPageMathAttachment) { + self.frame = frame + self.attachment = attachment + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func drawInTile(context: CGContext) { + } + + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? { + return InstantPageFormulaNode(attachment: self.attachment) + } + + func matchesNode(_ node: InstantPageNode) -> Bool { + guard let node = node as? InstantPageFormulaNode else { + return false + } + return self.attachment.isEqual(to: node.attachment) + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + return [] + } + + func distanceThresholdGroup() -> Int? { + return nil + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + return 0.0 + } +} + +final class InstantPageFormulaItem: InstantPageItem { + var frame: CGRect + let attachment: InstantPageMathAttachment + + let wantsNode: Bool = true + let separatesTiles: Bool = false + let medias: [InstantPageMedia] = [] + + init(frame: CGRect, attachment: InstantPageMathAttachment) { + self.frame = frame + self.attachment = attachment + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func drawInTile(context: CGContext) { + } + + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? { + return InstantPageFormulaNode(attachment: self.attachment) + } + + func matchesNode(_ node: InstantPageNode) -> Bool { + guard let node = node as? InstantPageFormulaNode else { + return false + } + return self.attachment.isEqual(to: node.attachment) + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + return [] + } + + func distanceThresholdGroup() -> Int? { + return nil + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + return 0.0 + } +} + +final class InstantPageScrollableFormulaItem: InstantPageScrollableItem { + var frame: CGRect + let attachment: InstantPageMathAttachment + let totalWidth: CGFloat + let horizontalInset: CGFloat + + let medias: [InstantPageMedia] = [] + let wantsNode: Bool = true + let separatesTiles: Bool = false + let isRTL: Bool = false + + init(frame: CGRect, attachment: InstantPageMathAttachment, totalWidth: CGFloat, horizontalInset: CGFloat) { + self.frame = frame + self.attachment = attachment + self.totalWidth = totalWidth + self.horizontalInset = horizontalInset + } + + var contentSize: CGSize { + return CGSize(width: self.totalWidth, height: self.frame.height) + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func drawInTile(context: CGContext) { + } + + func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? { + let node = InstantPageFormulaNode(attachment: self.attachment) + node.frame = CGRect(origin: .zero, size: self.attachment.rendered.size) + return InstantPageScrollableNode(item: self, additionalNodes: [node]) + } + + func matchesNode(_ node: InstantPageNode) -> Bool { + if let node = node as? InstantPageScrollableNode { + return node.item === self + } + return false + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + return [] + } + + func distanceThresholdGroup() -> Int? { + return nil + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + return 0.0 + } + + func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? { + return nil + } +} diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift index 3f4da96123..49ac9776c2 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryFooterContentNode.swift @@ -2,14 +2,12 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import SwiftSignalKit import Photos import TelegramPresentationData import TextFormat import AccountContext -import ShareController import GalleryUI import AppBundle @@ -147,7 +145,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/InstantPageImageItem.swift b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift index e29fcab2d7..c2e49314be 100644 --- a/submodules/InstantPageUI/Sources/InstantPageImageItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift @@ -54,7 +54,7 @@ public final class InstantPageImageItem: InstantPageItem { public func matchesNode(_ node: InstantPageNode) -> Bool { if let node = node as? InstantPageImageNode { - return node.media == self.media + return instantPageMediaMatchesNodeIdentity(node.media, self.media) } else { return false } diff --git a/submodules/InstantPageUI/Sources/InstantPageImageNode.swift b/submodules/InstantPageUI/Sources/InstantPageImageNode.swift index 76fc85e0fc..a7a413751b 100644 --- a/submodules/InstantPageUI/Sources/InstantPageImageNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageImageNode.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import ImageIO import AsyncDisplayKit import Display import TelegramCore @@ -21,7 +22,35 @@ private struct FetchControls { let cancel: () -> Void } -final class InstantPageImageNode: ASDisplayNode, InstantPageNode { +private enum ExternalImageLoadState { + case loading + case ready + case failed +} + +private func externalImagePixelDimensions(data: Data) -> PixelDimensions? { + guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { + return nil + } + guard let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else { + return nil + } + guard let pixelWidth = (properties[kCGImagePropertyPixelWidth] as? NSNumber)?.int32Value, + let pixelHeight = (properties[kCGImagePropertyPixelHeight] as? NSNumber)?.int32Value, + pixelWidth > 0, pixelHeight > 0 else { + return nil + } + + let orientation = imageOrientationFromSource(source) + switch orientation { + case .left, .right, .leftMirrored, .rightMirrored: + return PixelDimensions(width: pixelHeight, height: pixelWidth) + default: + return PixelDimensions(width: pixelWidth, height: pixelHeight) + } +} + +final class InstantPageImageNode: ASDisplayNode, InstantPageNode, InstantPageExternalMediaDimensionsNode { private let context: AccountContext private let webPage: TelegramMediaWebpage private var theme: InstantPageTheme @@ -32,8 +61,13 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { private let fit: Bool private let openMedia: (InstantPageMedia) -> Void private let longPressMedia: (InstantPageMedia) -> Void + private let getPreloadedResource: (String) -> Data? private var fetchControls: FetchControls? + private var externalImageLoadState: ExternalImageLoadState? + var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? + private var externalMediaDimensions: PixelDimensions? + private var didReportExternalMediaDimensions = false private let pinchContainerNode: PinchSourceContainerNode private let imageNode: TransformImageNode @@ -48,6 +82,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { private var statusDisposable = MetaDisposable() private var themeUpdated: Bool = false + private var externalMediaDimensionsUpdated: Bool = false init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, getPreloadedResource: @escaping (String) -> Data?) { self.context = context @@ -60,6 +95,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { self.fit = fit self.openMedia = openMedia self.longPressMedia = longPressMedia + self.getPreloadedResource = getPreloadedResource self.pinchContainerNode = PinchSourceContainerNode() self.imageNode = TransformImageNode() @@ -72,33 +108,14 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { self.pinchContainerNode.contentNode.addSubnode(self.imageNode) self.addSubnode(self.pinchContainerNode) + if interactive, media.url != nil { + self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink") + self.pinchContainerNode.contentNode.addSubnode(self.linkIconNode) + } + if case let .image(image) = media.media, let largest = largestImageRepresentation(image.representations) { if let externalResource = largest.resource as? InstantPageExternalMediaResource { - var url = externalResource.url - if !url.hasPrefix("http") && !url.hasPrefix("https") && url.hasPrefix("//") { - url = "https:\(url)" - } - let photoData: Signal, NoError> - if let preloadedData = getPreloadedResource(externalResource.url) { - photoData = .single(Tuple4(nil, preloadedData, .full, true)) - } else { - photoData = context.engine.resources.httpData(url: url, preserveExactUrl: true) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> map { data in - if let data { - return Tuple4(nil, data, .full, true) - } else { - return Tuple4(nil, nil, .full, false) - } - } - } - self.imageNode.setSignal(chatMessagePhotoInternal(photoData: photoData) - |> map { _, _, generate in - return generate - }) + self.loadExternalImage(resourceUrl: externalResource.url) } else { let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference)) @@ -116,46 +133,21 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { }) if interactive { - self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(largest.resource) |> deliverOnMainQueue).start(next: { [weak self] status in + self.statusDisposable.set((context.engine.resources.status(resource: EngineMediaResource(largest.resource)) |> deliverOnMainQueue).start(next: { [weak self] status in displayLinkDispatcher.dispatch { if let strongSelf = self { - strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status) + strongSelf.fetchStatus = status strongSelf.updateFetchStatus() } } })) - - if media.url != nil { - self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink") - self.pinchContainerNode.contentNode.addSubnode(self.linkIconNode) - } - + self.pinchContainerNode.contentNode.addSubnode(self.statusNode) } } } else if case let .file(file) = media.media { if let externalResource = file.resource as? InstantPageExternalMediaResource { - let photoData: Signal, NoError> - if let preloadedData = getPreloadedResource(externalResource.url) { - photoData = .single(Tuple4(nil, preloadedData, .full, true)) - } else { - photoData = context.engine.resources.httpData(url: externalResource.url, preserveExactUrl: true) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> map { data in - if let data { - return Tuple4(nil, data, .full, true) - } else { - return Tuple4(nil, nil, .full, false) - } - } - } - self.imageNode.setSignal(chatMessagePhotoInternal(photoData: photoData) - |> map { _, _, generate in - return generate - }) + self.loadExternalImage(resourceUrl: externalResource.url) } else { let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file) if file.mimeType.hasPrefix("image/") { @@ -235,6 +227,114 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { } } + private func loadExternalImage(resourceUrl: String) { + self.externalImageLoadState = .loading + self.updateExternalImageLoadState() + + var requestUrl = resourceUrl + if !requestUrl.hasPrefix("http") && !requestUrl.hasPrefix("https") && requestUrl.hasPrefix("//") { + requestUrl = "https:\(requestUrl)" + } + + let photoData: Signal, NoError> + if let preloadedData = self.getPreloadedResource(resourceUrl) { + photoData = .single(Tuple4(nil, preloadedData, .full, true)) + } else { + photoData = self.context.engine.resources.httpData(url: requestUrl, preserveExactUrl: true) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { data in + if let data { + return Tuple4(nil, data, .full, true) + } else { + return Tuple4(nil, nil, .full, false) + } + } + } + + let stateAwarePhotoData = photoData + |> deliverOnMainQueue + |> afterNext { [weak self] value in + guard let strongSelf = self else { + return + } + if let data = value._1 ?? value._0, UIImage(data: data) != nil { + if let dimensions = externalImagePixelDimensions(data: data) { + strongSelf.externalMediaDimensions = dimensions + strongSelf.externalMediaDimensionsUpdated = true + strongSelf.maybeUpdateExternalMediaDimensions(dimensions) + } + strongSelf.externalImageLoadState = .ready + strongSelf.setNeedsLayout() + } else { + strongSelf.externalImageLoadState = .failed + } + strongSelf.updateExternalImageLoadState() + } + + self.imageNode.setSignal(chatMessagePhotoInternal(photoData: stateAwarePhotoData) + |> map { _, _, generate in + return generate + }) + + self.fetchControls = FetchControls(fetch: { [weak self] _ in + self?.loadExternalImage(resourceUrl: resourceUrl) + }, cancel: {}) + } + + private func currentMediaDimensions() -> PixelDimensions? { + if case let .image(image) = self.media.media, let largest = largestImageRepresentation(image.representations) { + return largest.dimensions + } else if case let .file(file) = self.media.media { + return file.dimensions + } else { + return nil + } + } + + private func effectiveMediaDimensions() -> PixelDimensions? { + return self.externalMediaDimensions ?? self.currentMediaDimensions() + } + + private func maybeUpdateExternalMediaDimensions(_ dimensions: PixelDimensions) { + guard !self.didReportExternalMediaDimensions, let mediaId = self.media.media.id else { + return + } + if let currentDimensions = self.currentMediaDimensions(), currentDimensions == dimensions { + return + } + self.didReportExternalMediaDimensions = true + self.updateExternalMediaDimensions?(mediaId, dimensions) + } + + private func updateExternalImageLoadState() { + guard let externalImageLoadState = self.externalImageLoadState else { + return + } + + if self.statusNode.supernode == nil { + self.pinchContainerNode.contentNode.addSubnode(self.statusNode) + } + + let state: RadialStatusNodeState + switch externalImageLoadState { + case .loading: + state = .progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true) + case .ready: + state = .none + case .failed: + state = .none + } + + self.statusNode.transitionToState(state, completion: { [weak statusNode] in + if state == .none { + statusNode?.removeFromSupernode() + } + }) + } + private func updateFetchStatus() { var state: RadialStatusNodeState = .none if let fetchStatus = self.fetchStatus { @@ -260,19 +360,20 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { let size = self.bounds.size - if self.currentSize != size || self.themeUpdated { + if self.currentSize != size || self.themeUpdated || self.externalMediaDimensionsUpdated { self.currentSize = size self.themeUpdated = false + self.externalMediaDimensionsUpdated = false self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size) self.pinchContainerNode.update(size: size, transition: .immediate) self.imageNode.frame = CGRect(origin: CGPoint(), size: size) - let radialStatusSize: CGFloat = 50.0 + let radialStatusSize: CGFloat = max(18.0, min(50.0, floor(min(size.width, size.height) * 0.7))) self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - radialStatusSize) / 2.0), y: floorToScreenPixels((size.height - radialStatusSize) / 2.0), width: radialStatusSize, height: radialStatusSize) - if case let .image(image) = self.media.media, let largest = largestImageRepresentation(image.representations) { - let imageSize = largest.dimensions.cgSize.aspectFilled(size) + if case .image = self.media.media, let dimensions = self.effectiveMediaDimensions() { + let imageSize = dimensions.cgSize.aspectFilled(size) let boundingSize = size let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0 let makeLayout = self.imageNode.asyncLayout() @@ -280,7 +381,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { apply() self.linkIconNode.frame = CGRect(x: size.width - 38.0, y: 14.0, width: 24.0, height: 24.0) - } else if case let .file(file) = self.media.media, let dimensions = file.dimensions { + } else if case let .file(file) = self.media.media, let dimensions = self.effectiveMediaDimensions() { let emptyColor = file.mimeType.hasPrefix("image/") ? self.theme.imageTintColor : nil let imageSize = dimensions.cgSize.aspectFilled(size) @@ -318,7 +419,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { } func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { - if media == self.media { + if instantPageMediaMatchesNodeIdentity(media, self.media) { let imageNode = self.imageNode return (self.imageNode, self.imageNode.bounds, { [weak imageNode] in return (imageNode?.view.snapshotContentTree(unhide: true), nil) @@ -329,14 +430,32 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode { } func updateHiddenMedia(media: InstantPageMedia?) { - self.imageNode.isHidden = self.media == media + if let media { + self.imageNode.isHidden = instantPageMediaMatchesNodeIdentity(self.media, media) + } else { + self.imageNode.isHidden = false + } self.statusNode.isHidden = self.imageNode.isHidden + self.linkIconNode.isHidden = self.imageNode.isHidden } @objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + if let externalImageLoadState = self.externalImageLoadState { + switch externalImageLoadState { + case .loading: + return + case .failed: + if case .tap = gesture { + self.fetchControls?.fetch(true) + } + return + case .ready: + break + } + } if let fetchStatus = self.fetchStatus { switch fetchStatus { case .Local: diff --git a/submodules/InstantPageUI/Sources/InstantPageLayout.swift b/submodules/InstantPageUI/Sources/InstantPageLayout.swift index a19a069f7f..39baa17a74 100644 --- a/submodules/InstantPageUI/Sources/InstantPageLayout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageLayout.swift @@ -6,6 +6,7 @@ import TelegramPresentationData import TelegramUIPreferences import TelegramStringFormatting import MosaicLayout +import TextFormat public final class InstantPageLayout { public let origin: CGPoint @@ -28,8 +29,7 @@ public final class InstantPageLayout { } } -private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, category: InstantPageTextCategoryType, link: Bool) { - let attributes = theme.textCategories.attributes(type: category, link: link) +private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, attributes: InstantPageTextAttributes) { stack.push(.textColor(attributes.color)) stack.push(.markerColor(theme.markerColor)) stack.push(.linkColor(theme.linkColor)) @@ -47,7 +47,125 @@ private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantP } } -public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], excludeCaptions: Bool) -> InstantPageLayout { +private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, category: InstantPageTextCategoryType, link: Bool) { + setupStyleStack(stack, theme: theme, attributes: theme.textCategories.attributes(type: category, link: link)) +} + +private func instantPageFont(style: InstantPageTextAttributes, bold: Bool = false, italic: Bool = false, fixed: Bool = false) -> UIFont { + let size = style.font.size + if fixed { + if bold && italic { + return UIFont(name: "Menlo-BoldItalic", size: size) ?? Font.semiboldItalic(size) + } else if bold { + return UIFont(name: "Menlo-Bold", size: size) ?? Font.bold(size) + } else if italic { + return UIFont(name: "Menlo-Italic", size: size) ?? Font.italic(size) + } else { + return UIFont(name: "Menlo", size: size) ?? Font.regular(size) + } + } + switch style.font.style { + case .serif: + if bold && italic { + return UIFont(name: "Georgia-BoldItalic", size: size) ?? Font.semiboldItalic(size) + } else if bold { + return UIFont(name: "Georgia-Bold", size: size) ?? Font.bold(size) + } else if italic { + return UIFont(name: "Georgia-Italic", size: size) ?? Font.italic(size) + } else { + return UIFont(name: "Georgia", size: size) ?? Font.regular(size) + } + case .sans: + if bold && italic { + return Font.semiboldItalic(size) + } else if bold { + return Font.bold(size) + } else if italic { + return Font.italic(size) + } else { + return Font.regular(size) + } + } +} + +private func attributedStringForPreformattedText(_ text: RichText, language: String?, theme: InstantPageTheme, cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight?) -> NSAttributedString { + let paragraphAttributes = theme.textCategories.attributes(type: .paragraph, link: false) + let textValue = text.plainText + guard !textValue.isEmpty else { + return NSAttributedString( + string: "", + attributes: [ + .font: instantPageFont(style: paragraphAttributes, fixed: true), + .foregroundColor: paragraphAttributes.color, + NSAttributedString.Key(rawValue: InstantPageLineSpacingFactorAttribute): paragraphAttributes.font.lineSpacingFactor as NSNumber + ] + ) + } + + let attributedString = stringWithAppliedEntities( + textValue, + entities: [ + MessageTextEntity(range: 0 ..< (textValue as NSString).length, type: .Pre(language: language)) + ], + baseColor: paragraphAttributes.color, + linkColor: theme.linkColor, + codeBlockTitleColor: paragraphAttributes.color, + codeBlockAccentColor: paragraphAttributes.color, + codeBlockBackgroundColor: theme.codeBlockBackgroundColor, + baseFont: instantPageFont(style: paragraphAttributes), + linkFont: instantPageFont(style: paragraphAttributes), + boldFont: instantPageFont(style: paragraphAttributes, bold: true), + italicFont: instantPageFont(style: paragraphAttributes, italic: true), + boldItalicFont: instantPageFont(style: paragraphAttributes, bold: true, italic: true), + fixedFont: instantPageFont(style: paragraphAttributes, fixed: true), + blockQuoteFont: instantPageFont(style: paragraphAttributes), + underlineLinks: false, + message: nil, + cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight + ).mutableCopy() as! NSMutableAttributedString + attributedString.addAttribute( + NSAttributedString.Key(rawValue: InstantPageLineSpacingFactorAttribute), + value: paragraphAttributes.font.lineSpacingFactor as NSNumber, + range: NSRange(location: 0, length: attributedString.length) + ) + return attributedString +} + +private let instantPageTaskListUncheckedNumber = "\u{001f}tg-md-task:unchecked" +private let instantPageTaskListCheckedNumber = "\u{001f}tg-md-task:checked" +private let instantPageChecklistMarkerSize = CGSize(width: 18.0, height: 18.0) + +private func instantPageTaskListMarkerState(_ number: String?) -> Bool? { + switch number { + case instantPageTaskListUncheckedNumber: + return false + case instantPageTaskListCheckedNumber: + return true + default: + return nil + } +} + +private func instantPageFirstTextLineMidY(in items: [InstantPageItem]) -> CGFloat? { + for item in items { + if let textItem = item as? InstantPageTextItem { + if let line = textItem.lines.first { + return textItem.frame.minY + line.frame.midY + } else { + return textItem.frame.midY + } + } else if let scrollableTextItem = item as? InstantPageScrollableTextItem { + if let line = scrollableTextItem.item.lines.first { + return scrollableTextItem.frame.minY + scrollableTextItem.item.frame.minY + line.frame.midY + } else { + return scrollableTextItem.frame.midY + } + } + } + return nil +} + +public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, excludeCaptions: Bool) -> InstantPageLayout { let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in var items: [InstantPageItem] = [] @@ -100,7 +218,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: switch block { case let .cover(block): - return layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToSize: fillToSize, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + return layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToSize: fillToSize, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false) case let .title(text): let styleStack = InstantPageTextStyleStack() setupStyleStack(styleStack, theme: theme, category: .header, link: false) @@ -168,16 +286,65 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: setupStyleStack(styleStack, theme: theme, category: .subheader, link: false) let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .heading(text, level): + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, attributes: theme.headingTextAttributes(level: level, link: false)) + let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .formula(latex): + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) + let attributes = styleStack.textAttributes() + let textColor = (attributes[NSAttributedString.Key.foregroundColor] as? UIColor) ?? theme.textCategories.paragraph.color + let fontSize = (attributes[NSAttributedString.Key.font] as? UIFont)?.pointSize ?? theme.textCategories.paragraph.font.size + + guard let attachment = instantPageMathAttachment(latex: latex, fontSize: fontSize, textColor: textColor, mode: .block) else { + let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(.plain(latex), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + } + + let availableWidth = boundingWidth - horizontalInset * 2.0 + if attachment.rendered.size.width > availableWidth { + let scrollableItem = InstantPageScrollableFormulaItem( + frame: CGRect(origin: .zero, size: CGSize(width: boundingWidth, height: attachment.rendered.size.height)), + attachment: attachment, + totalWidth: attachment.rendered.size.width, + horizontalInset: horizontalInset + ) + return InstantPageLayout(origin: CGPoint(), contentSize: scrollableItem.frame.size, items: [scrollableItem]) + } else { + let item = InstantPageFormulaItem( + frame: CGRect( + origin: CGPoint(x: horizontalInset + floor((availableWidth - attachment.rendered.size.width) / 2.0), y: 0.0), + size: attachment.rendered.size + ), + attachment: attachment + ) + return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(width: boundingWidth, height: attachment.rendered.size.height), items: [item]) + } case let .paragraph(text): let styleStack = InstantPageTextStyleStack() setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, horizontalInset: horizontalInset, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) - case let .preformatted(text): - let styleStack = InstantPageTextStyleStack() - setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) + case let .preformatted(text, language): let backgroundInset: CGFloat = 14.0 - let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0, offset: CGPoint(x: 17.0, y: backgroundInset), media: media, webpage: webpage, opaqueBackground: true) + let attributedString: NSAttributedString + if let language, !language.isEmpty { + attributedString = attributedStringForPreformattedText(text, language: language, theme: theme, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight) + } else { + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) + attributedString = attributedStringForRichText(text, styleStack: styleStack) + } + let (_, items, contentSize) = layoutTextItemWithString( + attributedString, + boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0, + offset: CGPoint(x: 17.0, y: backgroundInset), + media: media, + webpage: webpage, + opaqueBackground: true + ) let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height + backgroundInset * 2.0)), shape: .rect, color: theme.codeBlockBackgroundColor) var allItems: [InstantPageItem] = [backgroundItem] allItems.append(contentsOf: items) @@ -196,12 +363,21 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: var maxIndexWidth: CGFloat = 0.0 var listItems: [InstantPageItem] = [] var indexItems: [InstantPageItem] = [] + var hasTaskMarkers = false var hasNums = false if ordered { for item in contentItems { - if let num = item.num, !num.isEmpty { + if instantPageTaskListMarkerState(item.num) != nil { + hasTaskMarkers = true + } else if let num = item.num, !num.isEmpty { hasNums = true + } + } + } else { + for item in contentItems { + if instantPageTaskListMarkerState(item.num) != nil { + hasTaskMarkers = true break } } @@ -209,7 +385,13 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: for i in 0 ..< contentItems.count { let item = contentItems[i] - if ordered { + if let checked = instantPageTaskListMarkerState(item.num) { + let checklistItem = InstantPageChecklistMarkerItem(frame: CGRect(origin: .zero, size: instantPageChecklistMarkerSize), checked: checked) + if ordered { + maxIndexWidth = max(maxIndexWidth, instantPageChecklistMarkerSize.width) + } + indexItems.append(checklistItem) + } else if ordered { let styleStack = InstantPageTextStyleStack() setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) let value: String @@ -233,7 +415,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: indexItems.append(shapeItem) } } - let indexSpacing: CGFloat = ordered ? 12.0 : 20.0 + let indexSpacing: CGFloat = ordered ? (hasTaskMarkers ? 16.0 : 12.0) : (hasTaskMarkers ? 24.0 : 20.0) for (i, item) in contentItems.enumerated() { if (i != 0) { contentSize.height += 18.0 @@ -264,6 +446,12 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: if let textIndexItem = indexItem as? InstantPageTextItem, let line = textIndexItem.lines.first { itemFrame = itemFrame.offsetBy(dx: horizontalInset + maxIndexWidth - line.frame.width, dy: floorToScreenPixels(lineMidY - (itemFrame.height / 2.0))) + } else if indexItem is InstantPageChecklistMarkerItem { + if ordered { + itemFrame = itemFrame.offsetBy(dx: horizontalInset + maxIndexWidth - itemFrame.width, dy: floorToScreenPixels(lineMidY - (itemFrame.height / 2.0))) + } else { + itemFrame = itemFrame.offsetBy(dx: horizontalInset, dy: floorToScreenPixels(lineMidY - (itemFrame.height / 2.0))) + } } else { itemFrame = itemFrame.offsetBy(dx: horizontalInset, dy: floorToScreenPixels(lineMidY - itemFrame.height / 2.0)) } @@ -273,14 +461,18 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: case let .blocks(blocks, _): var previousBlock: InstantPageBlock? var originY: CGFloat = contentSize.height + var firstBlockLineMidY: CGFloat? for subBlock in blocks { - let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false) let spacing: CGFloat = previousBlock != nil && subLayout.contentSize.height > 0.0 ? spacingBetweenBlocks(upper: previousBlock, lower: subBlock) : 0.0 let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + indexSpacing + maxIndexWidth, y: contentSize.height + spacing)) if previousBlock == nil { originY += spacing } + if firstBlockLineMidY == nil { + firstBlockLineMidY = instantPageFirstTextLineMidY(in: blockItems) + } listItems.append(contentsOf: blockItems) contentSize.height += subLayout.contentSize.height + spacing previousBlock = subBlock @@ -289,6 +481,18 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: var indexItemFrame = indexItem.frame if let textIndexItem = indexItem as? InstantPageTextItem, let line = textIndexItem.lines.first { indexItemFrame = indexItemFrame.offsetBy(dx: horizontalInset + maxIndexWidth - line.frame.width, dy: originY) + } else if indexItem is InstantPageChecklistMarkerItem { + let markerOriginY: CGFloat + if let firstBlockLineMidY { + markerOriginY = floorToScreenPixels(firstBlockLineMidY - indexItemFrame.height / 2.0) + } else { + markerOriginY = originY + } + if ordered { + indexItemFrame = indexItemFrame.offsetBy(dx: horizontalInset + maxIndexWidth - indexItemFrame.width, dy: markerOriginY) + } else { + indexItemFrame = indexItemFrame.offsetBy(dx: horizontalInset, dy: markerOriginY) + } } else { indexItemFrame = indexItemFrame.offsetBy(dx: horizontalInset, dy: originY) } @@ -476,7 +680,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: var i = 0 for subItem in innerItems { let frame = mosaicLayout[i].0 - let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subItem, boundingWidth: frame.width, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: frame.size, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: true) + let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subItem, boundingWidth: frame.width, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: frame.size, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: true) items.append(contentsOf: subLayout.flattenedItemsWithOrigin(frame.origin)) i += 1 } @@ -550,7 +754,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: var previousBlock: InstantPageBlock? for subBlock in blocks { - let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock) let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + lineInset, y: contentSize.height + spacing)) @@ -733,7 +937,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: var previousBlock: InstantPageBlock? for subBlock in blocks { - let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock) let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) @@ -842,7 +1046,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: } } -public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instantPage: InstantPage?, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:]) -> InstantPageLayout { +public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instantPage: InstantPage?, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, addFeedback: Bool = true) -> InstantPageLayout { var maybeLoadedContent: TelegramMediaWebpageLoadedContent? if case let .Loaded(content) = webPage.content { maybeLoadedContent = content @@ -871,7 +1075,7 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instant var previousBlock: InstantPageBlock? for block in pageBlocks { - let blockLayout = layoutInstantPageBlock(webpage: webPage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0 + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + let blockLayout = layoutInstantPageBlock(webpage: webPage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0 + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: block) let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) items.append(contentsOf: blockItems) @@ -884,7 +1088,7 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instant let closingSpacing = spacingBetweenBlocks(upper: previousBlock, lower: nil) contentSize.height += closingSpacing - if webPage.webpageId.id != 0 { + if webPage.webpageId.id != 0 && addFeedback { let feedbackItem = InstantPageFeedbackItem(frame: CGRect(x: 0.0, y: contentSize.height, width: boundingWidth, height: 40.0), webPage: webPage) contentSize.height += feedbackItem.frame.height items.append(feedbackItem) diff --git a/submodules/InstantPageUI/Sources/InstantPageLayoutSpacings.swift b/submodules/InstantPageUI/Sources/InstantPageLayoutSpacings.swift index 6ad2c61e7c..6536f2a200 100644 --- a/submodules/InstantPageUI/Sources/InstantPageLayoutSpacings.swift +++ b/submodules/InstantPageUI/Sources/InstantPageLayoutSpacings.swift @@ -21,29 +21,47 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> return 20.0 case (.title, .paragraph), (.authorDate, .paragraph): return 34.0 - case (.header, .paragraph), (.subheader, .paragraph): + case (.header, .paragraph), (.subheader, .paragraph), (.heading, .paragraph): return 25.0 case (.list, .paragraph): return 31.0 case (.preformatted, .paragraph): return 19.0 + case (.formula, .paragraph): + return 19.0 case (.paragraph, .paragraph): return 25.0 case (_, .paragraph): return 20.0 + case (.title, .formula), (.authorDate, .formula): + return 34.0 + case (.header, .formula), (.subheader, .formula), (.heading, .formula): + return 25.0 + case (.list, .formula): + return 31.0 + case (.preformatted, .formula): + return 19.0 + case (.paragraph, .formula): + return 19.0 + case (_, .formula): + return 20.0 case (.title, .list), (.authorDate, .list): return 34.0 - case (.header, .list), (.subheader, .list): + case (.header, .list), (.subheader, .list), (.heading, .list): return 31.0 case (.preformatted, .list): return 19.0 + case (.formula, .list): + return 25.0 case (_, .list): return 25.0 case (.paragraph, .preformatted): return 19.0 + case (.formula, .preformatted): + return 19.0 case (_, .preformatted): return 20.0 - case (_, .header), (_, .subheader): + case (_, .header), (_, .subheader), (_, .heading): return 32.0 default: return 20.0 diff --git a/submodules/InstantPageUI/Sources/InstantPageMath.swift b/submodules/InstantPageUI/Sources/InstantPageMath.swift new file mode 100644 index 0000000000..1c77532019 --- /dev/null +++ b/submodules/InstantPageUI/Sources/InstantPageMath.swift @@ -0,0 +1,79 @@ +import Foundation +import UIKit +import SwiftMath + +enum InstantPageMathMode { + case inline + case block +} + +struct InstantPageMathRenderResult { + let image: UIImage + let size: CGSize + let width: CGFloat + let ascent: CGFloat + let descent: CGFloat +} + +final class InstantPageMathAttachment: NSObject { + let latex: String + let fontSize: CGFloat + let textColor: UIColor + let mode: InstantPageMathMode + let rendered: InstantPageMathRenderResult + + init(latex: String, fontSize: CGFloat, textColor: UIColor, mode: InstantPageMathMode, rendered: InstantPageMathRenderResult) { + self.latex = latex + self.fontSize = fontSize + self.textColor = textColor + self.mode = mode + self.rendered = rendered + } + + func isEqual(to other: InstantPageMathAttachment) -> Bool { + return self.latex == other.latex + && self.fontSize == other.fontSize + && self.mode == other.mode + && self.textColor.isEqual(other.textColor) + && self.rendered.size == other.rendered.size + && self.rendered.ascent == other.rendered.ascent + && self.rendered.descent == other.rendered.descent + } +} + +func instantPageMathAttachment(latex: String, fontSize: CGFloat, textColor: UIColor, mode: InstantPageMathMode) -> InstantPageMathAttachment? { + let effectiveFontSize: CGFloat + let multiplier: CGFloat = 1.12 + switch mode { + case .inline: + effectiveFontSize = fontSize * multiplier + case .block: + effectiveFontSize = fontSize * multiplier + } + + guard let rendered = instantPageRenderMath(latex: latex, fontSize: effectiveFontSize, textColor: textColor, mode: mode) else { + return nil + } + return InstantPageMathAttachment(latex: latex, fontSize: effectiveFontSize, textColor: textColor, mode: mode, rendered: rendered) +} + +private func instantPageRenderMath(latex: String, fontSize: CGFloat, textColor: UIColor, mode: InstantPageMathMode) -> InstantPageMathRenderResult? { + let renderMode: MTMathUILabelMode + switch mode { + case .inline: + renderMode = .text + case .block: + renderMode = .display + } + + guard let rendered = MTMathRenderer.render(latex: latex, fontSize: fontSize, textColor: textColor, mode: renderMode) else { + return nil + } + return InstantPageMathRenderResult( + image: rendered.image, + size: rendered.size, + width: rendered.width, + ascent: rendered.ascent, + descent: rendered.descent + ) +} diff --git a/submodules/InstantPageUI/Sources/InstantPageMedia.swift b/submodules/InstantPageUI/Sources/InstantPageMedia.swift index ff9b2bc3f5..a7813f63bd 100644 --- a/submodules/InstantPageUI/Sources/InstantPageMedia.swift +++ b/submodules/InstantPageUI/Sources/InstantPageMedia.swift @@ -20,3 +20,28 @@ public struct InstantPageMedia: Equatable { return lhs.index == rhs.index && lhs.media == rhs.media && lhs.url == rhs.url && lhs.caption == rhs.caption && lhs.credit == rhs.credit } } + +func instantPageMediaMatchesNodeIdentity(_ lhs: InstantPageMedia, _ rhs: InstantPageMedia) -> Bool { + if lhs.index != rhs.index { + return false + } + if lhs.url != rhs.url || lhs.caption != rhs.caption || lhs.credit != rhs.credit { + return false + } + if let lhsId = lhs.media.id, let rhsId = rhs.media.id { + return lhsId == rhsId + } + return lhs == rhs +} + +func instantPageMediaArraysMatchNodeIdentity(_ lhs: [InstantPageMedia], _ rhs: [InstantPageMedia]) -> Bool { + if lhs.count != rhs.count { + return false + } + for i in 0 ..< lhs.count { + if !instantPageMediaMatchesNodeIdentity(lhs[i], rhs[i]) { + return false + } + } + return true +} diff --git a/submodules/InstantPageUI/Sources/InstantPageNode.swift b/submodules/InstantPageUI/Sources/InstantPageNode.swift index c235614961..fb011c454f 100644 --- a/submodules/InstantPageUI/Sources/InstantPageNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageNode.swift @@ -2,8 +2,13 @@ import Foundation import UIKit import AsyncDisplayKit import Display +import TelegramCore import TelegramPresentationData +public protocol InstantPageExternalMediaDimensionsNode: AnyObject { + var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? { get set } +} + public protocol InstantPageNode: ASDisplayNode { func updateIsVisible(_ isVisible: Bool) diff --git a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift index 97a545133d..fc3d3b47f9 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoNode.swift @@ -68,12 +68,12 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler self.addSubnode(self.videoNode) if case let .file(file) = media.media { - self.fetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .video, reference: AnyMediaReference.webPage(webPage: WebpageReference(webPage), media: file).resourceReference(file.resource)).start()) + self.fetchedDisposable.set(context.engine.resources.fetch(reference: AnyMediaReference.webPage(webPage: WebpageReference(webPage), media: file).resourceReference(file.resource), userLocation: userLocation, userContentType: .video).start()) - self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in + self.statusDisposable.set((context.engine.resources.status(resource: EngineMediaResource(file.resource)) |> deliverOnMainQueue).start(next: { [weak self] status in displayLinkDispatcher.dispatch { if let strongSelf = self { - strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status) + strongSelf.fetchStatus = status strongSelf.updateFetchStatus() } } 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/InstantPageUI/Sources/InstantPageSlideshowItem.swift b/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift index d9a39cabfc..560185cd86 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSlideshowItem.swift @@ -30,7 +30,7 @@ final class InstantPageSlideshowItem: InstantPageItem { func matchesNode(_ node: InstantPageNode) -> Bool { if let node = node as? InstantPageSlideshowNode { - return self.medias == node.medias + return instantPageMediaArraysMatchNodeIdentity(self.medias, node.medias) } else { return false } @@ -55,4 +55,3 @@ final class InstantPageSlideshowItem: InstantPageItem { func drawInTile(context: CGContext) { } } - diff --git a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift index 6baa616027..6fcc36030f 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSlideshowItemNode.swift @@ -61,6 +61,12 @@ private final class InstantPageSlideshowItemNode: ASDisplayNode { } return nil } + + func updateExternalMediaDimensions(_ update: ((EngineMedia.Id, PixelDimensions) -> Void)?) { + if let node = self.contentNode as? InstantPageImageNode { + node.updateExternalMediaDimensions = update + } + } } private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDelegate { @@ -88,6 +94,13 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDe } private var containerLayout: ContainerViewLayout? + var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? { + didSet { + for node in self.itemNodes { + node.updateExternalMediaDimensions(self.updateExternalMediaDimensions) + } + } + } var centralItemIndexUpdated: (Int?) -> Void = { _ in } @@ -195,6 +208,7 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDe } let node = InstantPageSlideshowItemNode(contentNode: contentNode) + node.updateExternalMediaDimensions(self.updateExternalMediaDimensions) node.index = index return node @@ -380,8 +394,13 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDe } } -final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode { +final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode, InstantPageExternalMediaDimensionsNode { var medias: [InstantPageMedia] = [] + var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? { + didSet { + self.pagerNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions + } + } private let pagerNode: InstantPageSlideshowPagerNode private let pageControlNode: PageControlNode diff --git a/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift b/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift index 30bdf1c6d9..d3633c7f9b 100644 --- a/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageSubContentNode.swift @@ -8,7 +8,7 @@ import TelegramPresentationData import TelegramUIPreferences import AccountContext -final class InstantPageSubContentNode : ASDisplayNode { +final class InstantPageSubContentNode : ASDisplayNode, InstantPageExternalMediaDimensionsNode { private let context: AccountContext private let strings: PresentationStrings private let nameDisplayOrder: PresentationPersonNameOrder @@ -32,6 +32,13 @@ final class InstantPageSubContentNode : ASDisplayNode { var currentDetailsItems: [InstantPageDetailsItem] = [] var requestLayoutUpdate: ((Bool) -> Void)? + var updateExternalMediaDimensions: ((EngineMedia.Id, PixelDimensions) -> Void)? { + didSet { + for (_, itemNode) in self.visibleItemsWithNodes { + self.applyExternalMediaDimensionsUpdater(to: itemNode) + } + } + } var currentLayout: InstantPageLayout let contentSize: CGSize @@ -209,6 +216,7 @@ final class InstantPageSubContentNode : ASDisplayNode { topNode = newNode self.visibleItemsWithNodes[itemIndex] = newNode itemNode = newNode + self.applyExternalMediaDimensionsUpdater(to: newNode) if let itemNode = itemNode as? InstantPageDetailsNode { itemNode.requestLayoutUpdate = { [weak self] animated in @@ -289,6 +297,16 @@ final class InstantPageSubContentNode : ASDisplayNode { } } + private func applyExternalMediaDimensionsUpdater(to itemNode: InstantPageNode) { + if let itemNode = itemNode as? InstantPageImageNode { + itemNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions + } else if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.contentNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions + } else if let itemNode = itemNode as? InstantPageSlideshowNode { + itemNode.updateExternalMediaDimensions = self.updateExternalMediaDimensions + } + } + private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) { // let currentHeight = self.currentWebEmbedHeights[index] // if height != currentHeight { diff --git a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift index 1b4c08197c..a66ff44839 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTextItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTextItem.swift @@ -26,6 +26,7 @@ public final class InstantPageUrlItem: Equatable { struct InstantPageTextMarkedItem { let frame: CGRect let color: UIColor + let range: NSRange } struct InstantPageTextStrikethroughItem { @@ -38,6 +39,12 @@ struct InstantPageTextImageItem { let id: EngineMedia.Id } +struct InstantPageTextFormulaRun { + let frame: CGRect + let range: NSRange + let attachment: InstantPageMathAttachment +} + public struct InstantPageTextAnchorItem { public let name: String public let anchorText: NSAttributedString? @@ -63,16 +70,18 @@ public final class InstantPageTextLine { let strikethroughItems: [InstantPageTextStrikethroughItem] let markedItems: [InstantPageTextMarkedItem] let imageItems: [InstantPageTextImageItem] + let formulaItems: [InstantPageTextFormulaRun] public let anchorItems: [InstantPageTextAnchorItem] let isRTL: Bool - init(line: CTLine, range: NSRange, frame: CGRect, strikethroughItems: [InstantPageTextStrikethroughItem], markedItems: [InstantPageTextMarkedItem], imageItems: [InstantPageTextImageItem], anchorItems: [InstantPageTextAnchorItem], isRTL: Bool) { + init(line: CTLine, range: NSRange, frame: CGRect, strikethroughItems: [InstantPageTextStrikethroughItem], markedItems: [InstantPageTextMarkedItem], imageItems: [InstantPageTextImageItem], formulaItems: [InstantPageTextFormulaRun], anchorItems: [InstantPageTextAnchorItem], isRTL: Bool) { self.line = line self.range = range self.frame = frame self.strikethroughItems = strikethroughItems self.markedItems = markedItems self.imageItems = imageItems + self.formulaItems = formulaItems self.anchorItems = anchorItems self.isRTL = isRTL } @@ -88,6 +97,75 @@ private func frameForLine(_ line: InstantPageTextLine, boundingWidth: CGFloat, a return lineFrame } +private func expandedFrameForLine(_ line: InstantPageTextLine, boundingWidth: CGFloat, alignment: NSTextAlignment) -> CGRect { + var lineFrame = line.frame + for imageItem in line.imageItems { + if imageItem.frame.minY < lineFrame.minY { + let delta = lineFrame.minY - imageItem.frame.minY - 2.0 + lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY - delta, width: lineFrame.width, height: lineFrame.height + delta) + } + if imageItem.frame.maxY > lineFrame.maxY { + let delta = imageItem.frame.maxY - lineFrame.maxY - 2.0 + lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY, width: lineFrame.width, height: lineFrame.height + delta) + } + } + for formulaItem in line.formulaItems { + if formulaItem.frame.minY < lineFrame.minY { + let delta = lineFrame.minY - formulaItem.frame.minY - 2.0 + lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY - delta, width: lineFrame.width, height: lineFrame.height + delta) + } + if formulaItem.frame.maxY > lineFrame.maxY { + let delta = formulaItem.frame.maxY - lineFrame.maxY - 2.0 + lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY, width: lineFrame.width, height: lineFrame.height + delta) + } + } + lineFrame = lineFrame.insetBy(dx: 0.0, dy: -4.0) + if alignment == .center { + lineFrame.origin.x = floor((boundingWidth - lineFrame.size.width) / 2.0) + } else if alignment == .right || (alignment == .natural && line.isRTL) { + lineFrame.origin.x = boundingWidth - lineFrame.size.width + } + return lineFrame +} + +private func alignedAttachmentFrame(_ frame: CGRect, line: InstantPageTextLine, boundingWidth: CGFloat, alignment: NSTextAlignment) -> CGRect { + let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment) + return frame.offsetBy(dx: lineFrame.minX - line.frame.minX, dy: 0.0) +} + +private func localAttachmentBoundsForRange(_ range: NSRange, imageItems: [InstantPageTextImageItem], formulaItems: [InstantPageTextFormulaRun]) -> CGRect? { + var result: CGRect? + + for imageItem in imageItems { + if NSIntersectionRange(range, imageItem.range).length != 0 { + if let current = result { + result = current.union(imageItem.frame) + } else { + result = imageItem.frame + } + } + } + + for formulaItem in formulaItems { + if NSIntersectionRange(range, formulaItem.range).length != 0 { + if let current = result { + result = current.union(formulaItem.frame) + } else { + result = formulaItem.frame + } + } + } + + return result +} + +private func attachmentBoundsForRange(_ range: NSRange, line: InstantPageTextLine, boundingWidth: CGFloat, alignment: NSTextAlignment) -> CGRect? { + guard let localBounds = localAttachmentBoundsForRange(range, imageItems: line.imageItems, formulaItems: line.formulaItems) else { + return nil + } + return alignedAttachmentFrame(localBounds, line: line, boundingWidth: boundingWidth, alignment: alignment) +} + public final class InstantPageTextItem: InstantPageItem { let attributedString: NSAttributedString public let lines: [InstantPageTextLine] @@ -197,7 +275,7 @@ public final class InstantPageTextItem: InstantPageItem { for i in 0 ..< self.lines.count { let line = self.lines[i] - let lineFrame = frameForLine(line, boundingWidth: boundsWidth, alignment: self.alignment) + let lineFrame = expandedFrameForLine(line, boundingWidth: boundsWidth, alignment: self.alignment) if lineFrame.insetBy(dx: -5.0, dy: -5.0).contains(transformedPoint) { var index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: transformedPoint.x - lineFrame.minX, y: transformedPoint.y - lineFrame.minY)) if index == self.attributedString.length { @@ -238,8 +316,20 @@ public final class InstantPageTextItem: InstantPageItem { } let lineFrame = frameForLine(line, boundingWidth: boundsWidth, alignment: self.alignment) let width = abs(rightOffset - leftOffset) + + var rect: CGRect? if width > 1.0 { - rects.append(CGRect(origin: CGPoint(x: lineFrame.minX + (leftOffset < rightOffset ? leftOffset : rightOffset), y: lineFrame.minY), size: CGSize(width: width, height: lineFrame.size.height))) + rect = CGRect(origin: CGPoint(x: lineFrame.minX + (leftOffset < rightOffset ? leftOffset : rightOffset), y: lineFrame.minY), size: CGSize(width: width, height: lineFrame.size.height)) + } + if let attachmentBounds = attachmentBoundsForRange(lineRange, line: line, boundingWidth: boundsWidth, alignment: self.alignment) { + if rect != nil { + rect = rect!.union(attachmentBounds) + } else { + rect = attachmentBounds + } + } + if let rect, rect.width > 1.0 { + rects.append(rect) } } } @@ -304,25 +394,7 @@ public final class InstantPageTextItem: InstantPageItem { } } - var lineFrame = line.frame - for imageItem in line.imageItems { - if imageItem.frame.minY < lineFrame.minY { - let delta = lineFrame.minY - imageItem.frame.minY - 2.0 - lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY - delta, width: lineFrame.width, height: lineFrame.height + delta) - } - if imageItem.frame.maxY > lineFrame.maxY { - let delta = imageItem.frame.maxY - lineFrame.maxY - 2.0 - lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY, width: lineFrame.width, height: lineFrame.height + delta) - } - } - lineFrame = lineFrame.insetBy(dx: 0.0, dy: -4.0) - if self.alignment == .center { - lineFrame.origin.x = floor((boundsWidth - lineFrame.size.width) / 2.0) - } else if self.alignment == .right { - lineFrame.origin.x = boundsWidth - lineFrame.size.width - } else if self.alignment == .natural && self.rtlLineIndices.contains(i) { - lineFrame.origin.x = boundsWidth - lineFrame.size.width - } + let lineFrame = expandedFrameForLine(line, boundingWidth: boundsWidth, alignment: self.alignment) let width = max(0.0, abs(rightOffset - leftOffset)) @@ -368,25 +440,7 @@ public final class InstantPageTextItem: InstantPageItem { for i in 0 ..< self.lines.count { let line = self.lines[i] - var lineFrame = line.frame - for imageItem in line.imageItems { - if imageItem.frame.minY < lineFrame.minY { - let delta = lineFrame.minY - imageItem.frame.minY - 2.0 - lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY - delta, width: lineFrame.width, height: lineFrame.height + delta) - } - if imageItem.frame.maxY > lineFrame.maxY { - let delta = imageItem.frame.maxY - lineFrame.maxY - 2.0 - lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY, width: lineFrame.width, height: lineFrame.height + delta) - } - } - lineFrame = lineFrame.insetBy(dx: 0.0, dy: -4.0) - if self.alignment == .center { - lineFrame.origin.x = floor((boundsWidth - lineFrame.size.width) / 2.0) - } else if self.alignment == .right { - lineFrame.origin.x = boundsWidth - lineFrame.size.width - } else if self.alignment == .natural && self.rtlLineIndices.contains(i) { - lineFrame.origin.x = boundsWidth - lineFrame.size.width - } + let lineFrame = expandedFrameForLine(line, boundingWidth: boundsWidth, alignment: self.alignment) if lineFrame.minX < topLeft.x { topLeft = CGPoint(x: lineFrame.minX, y: topLeft.y) @@ -633,6 +687,43 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt let mutableAttributedString = attributedStringForRichText(.plain(" "), styleStack: styleStack, url: url).mutableCopy() as! NSMutableAttributedString mutableAttributedString.addAttributes(attrDictionaryDelegate, range: NSMakeRange(0, mutableAttributedString.length)) return mutableAttributedString + case let .formula(latex): + let attributes = styleStack.textAttributes() + let textColor = (attributes[NSAttributedString.Key.foregroundColor] as? UIColor) ?? UIColor.black + let fontSize = (attributes[NSAttributedString.Key.font] as? UIFont)?.pointSize ?? 16.0 + guard let attachment = instantPageMathAttachment(latex: latex, fontSize: fontSize, textColor: textColor, mode: .inline) else { + var fallbackAttributes = attributes + if let url = url { + fallbackAttributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] = url + } + return NSAttributedString(string: latex, attributes: fallbackAttributes) + } + + struct RunStruct { + let ascent: CGFloat + let descent: CGFloat + let width: CGFloat + } + let extentBuffer = UnsafeMutablePointer.allocate(capacity: 1) + extentBuffer.initialize(to: RunStruct(ascent: attachment.rendered.ascent, descent: attachment.rendered.descent, width: attachment.rendered.size.width)) + var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { _ in + }, getAscent: { pointer -> CGFloat in + let data = pointer.assumingMemoryBound(to: RunStruct.self) + return data.pointee.ascent + }, getDescent: { pointer -> CGFloat in + let data = pointer.assumingMemoryBound(to: RunStruct.self) + return data.pointee.descent + }, getWidth: { pointer -> CGFloat in + let data = pointer.assumingMemoryBound(to: RunStruct.self) + return data.pointee.width + }) + let delegate = CTRunDelegateCreate(&callbacks, extentBuffer) + let mutableAttributedString = attributedStringForRichText(.plain(" "), styleStack: styleStack, url: url).mutableCopy() as! NSMutableAttributedString + mutableAttributedString.addAttributes([ + kCTRunDelegateAttributeName as NSAttributedString.Key: delegate as Any, + NSAttributedString.Key(rawValue: InstantPageFormulaAttribute): attachment + ], range: NSMakeRange(0, mutableAttributedString.length)) + return mutableAttributedString case let .anchor(text, name): var empty = false var text = text @@ -655,6 +746,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo var lines: [InstantPageTextLine] = [] var imageItems: [InstantPageTextImageItem] = [] + var formulaItems: [InstantPageTextFormulaItem] = [] var font = string.attribute(NSAttributedString.Key.font, at: 0, effectiveRange: nil) as? UIFont if font == nil { let range = NSMakeRange(0, string.length) @@ -665,7 +757,8 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo } } let image = string.attribute(NSAttributedString.Key.init(rawValue: InstantPageMediaIdAttribute), at: 0, effectiveRange: nil) - guard font != nil || image != nil else { + let formula = string.attribute(NSAttributedString.Key(rawValue: InstantPageFormulaAttribute), at: 0, effectiveRange: nil) + guard font != nil || image != nil || formula != nil else { return (nil, [], CGSize()) } @@ -686,7 +779,6 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo var hasAnchors = false var maxLineWidth: CGFloat = 0.0 - var maxImageHeight: CGFloat = 0.0 var extraDescent: CGFloat = 0.0 let text = string.string var indexOffset: CFIndex? @@ -741,6 +833,8 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo let hadExtraDescent = extraDescent > 0.0 extraDescent = 0.0 var lineImageItems: [InstantPageTextImageItem] = [] + var lineFormulaItems: [InstantPageTextFormulaRun] = [] + var lineMaxAttachmentHeight: CGFloat = 0.0 var isRTL = false if let glyphRuns = CTLineGetGlyphRuns(line) as? [CTRun], !glyphRuns.isEmpty { if let run = glyphRuns.first, CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { @@ -769,20 +863,49 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo if !fontLineHeight.isZero { extraDescent = max(extraDescent, imageFrame.maxY - (workingLineOrigin.y + fontLineHeight + minSpacing)) } - maxImageHeight = max(maxImageHeight, imageFrame.height) + lineMaxAttachmentHeight = max(lineMaxAttachmentHeight, imageFrame.height) lineImageItems.append(InstantPageTextImageItem(frame: imageFrame, range: range, id: EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: id))) + } else if let attachment = attributes[NSAttributedString.Key(rawValue: InstantPageFormulaAttribute)] as? InstantPageMathAttachment { + let xOffset = CTLineGetOffsetForStringIndex(line, range.location, nil) + let baselineOffset = (attributes[NSAttributedString.Key.baselineOffset] as? CGFloat) ?? 0.0 + var formulaFrame = CGRect( + origin: CGPoint( + x: workingLineOrigin.x + xOffset, + y: workingLineOrigin.y + fontLineHeight + baselineOffset - attachment.rendered.ascent + ), + size: attachment.rendered.size + ) + + let minSpacing = fontLineSpacing - 4.0 + let delta = workingLineOrigin.y - minSpacing - formulaFrame.minY - appliedLineOffset + if !fontAscent.isZero && delta > 0.0 { + workingLineOrigin.y += delta + appliedLineOffset += delta + formulaFrame.origin = formulaFrame.origin.offsetBy(dx: 0.0, dy: delta) + } + if !fontLineHeight.isZero { + extraDescent = max(extraDescent, formulaFrame.maxY - (workingLineOrigin.y + fontLineHeight + minSpacing)) + } + lineMaxAttachmentHeight = max(lineMaxAttachmentHeight, formulaFrame.height) + lineFormulaItems.append(InstantPageTextFormulaRun(frame: formulaFrame, range: range, attachment: attachment)) } } } } - if substring.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && lineImageItems.count > 0 { + if substring.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && (!lineImageItems.isEmpty || !lineFormulaItems.isEmpty) { extraDescent += max(6.0, fontLineSpacing / 2.0) } - if !minimizeWidth && !hadIndexOffset && lineCharacterCount > 1 && lineWidth > currentMaxWidth + 5.0, let imageItem = lineImageItems.last { - indexOffset = -(lastIndex + lineCharacterCount - imageItem.range.lowerBound) - continue + if !minimizeWidth && !hadIndexOffset && lineCharacterCount > 1 && lineWidth > currentMaxWidth + 5.0 { + if let imageItem = lineImageItems.last { + indexOffset = -(lastIndex + lineCharacterCount - imageItem.range.lowerBound) + continue + } + if let formulaItem = lineFormulaItems.last { + indexOffset = -(lastIndex + lineCharacterCount - formulaItem.range.lowerBound) + continue + } } var strikethroughItems: [InstantPageTextStrikethroughItem] = [] @@ -807,7 +930,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo let lowerX = floor(CTLineGetOffsetForStringIndex(line, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(line, range.location + range.length, nil)) let x = lowerX < upperX ? lowerX : upperX - markedItems.append(InstantPageTextMarkedItem(frame: CGRect(x: workingLineOrigin.x + x, y: workingLineOrigin.y + delta, width: abs(upperX - lowerX), height: lineHeight), color: color)) + markedItems.append(InstantPageTextMarkedItem(frame: CGRect(x: workingLineOrigin.x + x, y: workingLineOrigin.y + delta, width: abs(upperX - lowerX), height: lineHeight), color: color, range: range)) } if let item = attributes[NSAttributedString.Key.init(rawValue: InstantPageAnchorAttribute)] as? Dictionary, let name = item["name"] as? String, let empty = item["empty"] as? Bool { anchorItems.append(InstantPageTextAnchorItem(name: name, anchorText: item["text"] as? NSAttributedString, empty: empty)) @@ -822,11 +945,35 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo workingLineOrigin.y += fontLineSpacing } - let height = !fontLineHeight.isZero ? fontLineHeight : maxImageHeight - let textLine = InstantPageTextLine(line: line, range: lineRange, frame: CGRect(x: workingLineOrigin.x, y: workingLineOrigin.y, width: lineWidth, height: height), strikethroughItems: strikethroughItems, markedItems: markedItems, imageItems: lineImageItems, anchorItems: anchorItems, isRTL: isRTL) + let height = !fontLineHeight.isZero ? max(fontLineHeight, lineMaxAttachmentHeight) : lineMaxAttachmentHeight + if !lineFormulaItems.isEmpty { + let baselineAdjustment = height - fontLineHeight + if !baselineAdjustment.isZero { + lineFormulaItems = lineFormulaItems.map { item in + InstantPageTextFormulaRun( + frame: item.frame.offsetBy(dx: 0.0, dy: baselineAdjustment), + range: item.range, + attachment: item.attachment + ) + } + } + } + if !markedItems.isEmpty { + markedItems = markedItems.map { item in + if let attachmentBounds = localAttachmentBoundsForRange(item.range, imageItems: lineImageItems, formulaItems: lineFormulaItems) { + return InstantPageTextMarkedItem(frame: attachmentBounds, color: item.color, range: item.range) + } else { + return item + } + } + } + let textLine = InstantPageTextLine(line: line, range: lineRange, frame: CGRect(x: workingLineOrigin.x, y: workingLineOrigin.y, width: lineWidth, height: height), strikethroughItems: strikethroughItems, markedItems: markedItems, imageItems: lineImageItems, formulaItems: lineFormulaItems, anchorItems: anchorItems, isRTL: isRTL) lines.append(textLine) imageItems.append(contentsOf: lineImageItems) + for formulaItem in lineFormulaItems { + formulaItems.append(InstantPageTextFormulaItem(frame: formulaItem.frame, attachment: formulaItem.attachment)) + } if lineWidth > maxLineWidth { maxLineWidth = lineWidth @@ -853,7 +1000,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo var textWidth = boundingWidth var requiresScroll = false - if !imageItems.isEmpty && maxLineWidth > boundingWidth + 10.0 { + if (!imageItems.isEmpty || !formulaItems.isEmpty) && maxLineWidth > boundingWidth + 10.0 { textWidth = maxLineWidth requiresScroll = true } @@ -870,13 +1017,13 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo var topInset: CGFloat = 0.0 var bottomInset: CGFloat = 0.0 var additionalItems: [InstantPageItem] = [] - if let webpage = webpage { - let offset = requiresScroll ? CGPoint() : offset - for line in textItem.lines { - let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment) + let effectiveOffset = requiresScroll ? CGPoint() : offset + for line in textItem.lines { + let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment) + if let webpage = webpage { for imageItem in line.imageItems { if let media = media[imageItem.id] { - let item = InstantPageImageItem(frame: imageItem.frame.offsetBy(dx: lineFrame.minX + offset.x, dy: offset.y), webPage: webpage, media: InstantPageMedia(index: -1, media: media, url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false) + let item = InstantPageImageItem(frame: imageItem.frame.offsetBy(dx: lineFrame.minX + effectiveOffset.x, dy: effectiveOffset.y), webPage: webpage, media: InstantPageMedia(index: -1, media: media, url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false) additionalItems.append(item) if item.frame.minY < topInset { @@ -888,6 +1035,17 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo } } } + for formulaItem in line.formulaItems { + let item = InstantPageTextFormulaItem(frame: formulaItem.frame.offsetBy(dx: lineFrame.minX + effectiveOffset.x, dy: effectiveOffset.y), attachment: formulaItem.attachment) + additionalItems.append(item) + + if item.frame.minY < topInset { + topInset = item.frame.minY + } + if item.frame.maxY > height { + bottomInset = max(bottomInset, item.frame.maxY - height) + } + } } if requiresScroll { diff --git a/submodules/InstantPageUI/Sources/InstantPageTextStyleStack.swift b/submodules/InstantPageUI/Sources/InstantPageTextStyleStack.swift index 413ff55840..2ab0a10df8 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTextStyleStack.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTextStyleStack.swift @@ -28,6 +28,7 @@ let InstantPageMarkerColorAttribute = "MarkerColorAttribute" let InstantPageMediaIdAttribute = "MediaIdAttribute" let InstantPageMediaDimensionsAttribute = "MediaDimensionsAttribute" let InstantPageAnchorAttribute = "AnchorAttribute" +let InstantPageFormulaAttribute = "FormulaAttribute" final class InstantPageTextStyleStack { private var items: [InstantPageTextStyle] = [] diff --git a/submodules/InstantPageUI/Sources/InstantPageTheme.swift b/submodules/InstantPageUI/Sources/InstantPageTheme.swift index d1305895e4..dccc23d9fd 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTheme.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTheme.swift @@ -1,6 +1,5 @@ import Foundation import UIKit -import Postbox import Display import TelegramPresentationData import TelegramUIPreferences @@ -136,6 +135,32 @@ public final class InstantPageTheme { public func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTheme { return InstantPageTheme(type: type, pageBackgroundColor: pageBackgroundColor, textCategories: self.textCategories.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), serif: forceSerif, codeBlockBackgroundColor: codeBlockBackgroundColor, linkColor: linkColor, textHighlightColor: textHighlightColor, linkHighlightColor: linkHighlightColor, markerColor: markerColor, panelBackgroundColor: panelBackgroundColor, panelHighlightedBackgroundColor: panelHighlightedBackgroundColor, panelPrimaryColor: panelPrimaryColor, panelSecondaryColor: panelSecondaryColor, panelAccentColor: panelAccentColor, tableBorderColor: tableBorderColor, tableHeaderColor: tableHeaderColor, controlColor: controlColor, imageTintColor: imageTintColor, overlayPanelColor: overlayPanelColor) } + + func headingTextAttributes(level: Int32, link: Bool) -> InstantPageTextAttributes { + let clampedLevel = max(Int32(3), min(level, Int32(6))) + let subheaderAttributes = self.textCategories.subheader + guard clampedLevel > 3 else { + return subheaderAttributes.withUnderline(link) + } + + let baseSize: CGFloat + switch clampedLevel { + case 4: + baseSize = 17.0 + case 5: + baseSize = 15.0 + default: + baseSize = 13.0 + } + + let sizeMultiplier = subheaderAttributes.font.size / 19.0 + let attributes = InstantPageTextAttributes( + font: InstantPageFont(style: .serif, size: floor(baseSize * sizeMultiplier), lineSpacingFactor: subheaderAttributes.font.lineSpacingFactor), + color: subheaderAttributes.color, + underline: subheaderAttributes.underline + ) + return attributes.withUnderline(link) + } } private let lightTheme = InstantPageTheme( diff --git a/submodules/InstantPageUI/Sources/InstantPageTileNode.swift b/submodules/InstantPageUI/Sources/InstantPageTileNode.swift index c16bcac1dd..2da701bd59 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTileNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTileNode.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import AsyncDisplayKit +import Display private final class InstantPageTileNodeParameters: NSObject { let tile: InstantPageTile @@ -15,10 +16,12 @@ private final class InstantPageTileNodeParameters: NSObject { } public final class InstantPageTileNode: ASDisplayNode { - private let tile: InstantPageTile + private var tile: InstantPageTile + private var tileBackgroundColor: UIColor public init(tile: InstantPageTile, backgroundColor: UIColor) { self.tile = tile + self.tileBackgroundColor = backgroundColor super.init() @@ -27,8 +30,15 @@ public final class InstantPageTileNode: ASDisplayNode { self.backgroundColor = backgroundColor } + public func update(tile: InstantPageTile, backgroundColor: UIColor) { + self.tile = tile + self.tileBackgroundColor = backgroundColor + self.backgroundColor = backgroundColor + self.setNeedsDisplay() + } + public override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return InstantPageTileNodeParameters(tile: self.tile, backgroundColor: self.backgroundColor ?? UIColor.white) + return InstantPageTileNodeParameters(tile: self.tile, backgroundColor: self.tileBackgroundColor) } @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { @@ -36,9 +46,11 @@ public final class InstantPageTileNode: ASDisplayNode { if let parameters = parameters as? InstantPageTileNodeParameters { if !isRasterizing { - context.setBlendMode(.copy) - context.setFillColor(parameters.backgroundColor.cgColor) - context.fill(bounds) + if !parameters.backgroundColor.alpha.isZero { + context.setBlendMode(.copy) + context.setFillColor(parameters.backgroundColor.cgColor) + context.fill(bounds) + } } parameters.tile.draw(context: context) 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/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index 79564f02cb..8cbb4e2389 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -355,7 +355,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { case let .timeHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .timePicker(_, value, enabled): - return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, strings: presentationData.strings, value: value, enabled: enabled, sectionId: self.section, updated: { value in + return ItemListInviteLinkTimeLimitItem(theme: presentationData.theme, systemStyle: .glass, strings: presentationData.strings, value: value, enabled: enabled, sectionId: self.section, updated: { value in arguments.updateState({ state in var updatedState = state if value != updatedState.time { @@ -418,7 +418,7 @@ private enum InviteLinksEditEntry: ItemListNodeEntry { case let .usageHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .usagePicker(_, dateTimeFormat, value, enabled): - return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, value: value, enabled: enabled, sectionId: self.section, updated: { value in + return ItemListInviteLinkUsageLimitItem(theme: presentationData.theme, systemStyle: .glass, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, value: value, enabled: enabled, sectionId: self.section, updated: { value in arguments.dismissInput() arguments.updateState({ state in var updatedState = state @@ -624,10 +624,17 @@ public func inviteLinkEditController(context: AccountContext, updatedPresentatio guard let inviteLink = invite?.link else { return } - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { peer in let isGroup: Bool - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } else { isGroup = true diff --git a/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift b/submodules/InviteLinksUI/Sources/InviteLinkInviteController.swift index f1776d198d..580562f972 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 @@ -411,13 +411,20 @@ public final class InviteLinkInviteController: ViewController { if let invite { if case let .groupOrChannel(peerId) = self.mode { - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { [weak self] peer in guard let strongSelf = self else { return } let isGroup: Bool - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } else { isGroup = true @@ -443,10 +450,17 @@ public final class InviteLinkInviteController: ViewController { return } - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { [weak self] peer in let isGroup: Bool - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } else { isGroup = true @@ -540,8 +554,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 +570,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 +591,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 +607,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..24b8c0660d 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) @@ -501,109 +498,144 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio guard let node = node as? ContextReferenceContentNode, let controller = getControllerImpl?(), let invite = invite else { return } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - var items: [ContextMenuItem] = [] - - items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.dismissWithoutContent) - - dismissTooltipsImpl?() - - UIPasteboard.general.string = invite.link - - 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) - }))) - items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.dismissWithoutContent) - - let _ = (context.account.postbox.loadedPeerWithId(peerId) - |> deliverOnMainQueue).start(next: { peer in - let isGroup: Bool - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - isGroup = false + let creatorIsBot: Signal + if let adminPeer = admin?.peer.peer as? TelegramUser { + creatorIsBot = .single(adminPeer.botInfo != nil) + } else if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _, _) = invite, adminId.toInt64() != 0 { + creatorIsBot = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: adminId)) + |> map { peer -> Bool in + if let peer, case let .user(user) = peer, user.botInfo != nil { + return true } else { - isGroup = true + return false } - presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil) - }) - }))) + } + } else { + creatorIsBot = .single(false) + } - if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _, _) = invite, adminId.toInt64() != 0 { - items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) + let _ = (creatorIsBot + |> take(1) + |> deliverOnMainQueue).start(next: { creatorIsBot in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var items: [ContextMenuItem] = [] + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.dismissWithoutContent) + + dismissTooltipsImpl?() + + UIPasteboard.general.string = invite.link + + 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) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Settings/QrIcon"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.dismissWithoutContent) - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { peer in let isGroup: Bool - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } else { isGroup = true } - - let controller = ActionSheetController(presentationData: presentationData) - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - controller.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: isGroup ? presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text : presentationData.strings.ChannelInfo_InviteLink_RevokeAlert_Text), - ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: { - dismissAction() - - var revoke = false - updateState { state in - if !state.revokingPrivateLink { - revoke = true - var updatedState = state - updatedState.revokingPrivateLink = true - return updatedState - } else { - return state - } - } - if revoke, let inviteLink = invite.link { - revokeLinkDisposable.set((context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: inviteLink) |> deliverOnMainQueue).start(next: { result in - updateState { state in - var updatedState = state - updatedState.revokingPrivateLink = false - return updatedState - } - if let result = result { - switch result { - case let .update(newInvite): - invitesContext.remove(newInvite) - revokedInvitesContext.add(newInvite) - case let .replace(previousInvite, newInvite): - revokedInvitesContext.add(previousInvite) - invitesContext.remove(previousInvite) - invitesContext.add(newInvite) - } - } - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkRevoked(text: presentationData.strings.InviteLink_InviteLinkRevoked), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) - })) - } - }) - ]), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) - ]) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + presentControllerImpl?(QrCodeScreen(context: context, updatedPresentationData: updatedPresentationData, subject: .invite(invite: invite, type: isGroup ? .group : .channel)), nil) }) }))) - } - let contextController = makeContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) - presentInGlobalOverlayImpl?(contextController) + if case let .link(_, _, _, _, _, adminId, _, _, _, _, _, _, _) = invite, adminId.toInt64() != 0, !creatorIsBot { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + |> deliverOnMainQueue).start(next: { peer in + let isGroup: Bool + if case let .channel(channel) = peer, case .broadcast = channel.info { + isGroup = false + } else { + isGroup = true + } + + let controller = ActionSheetController(presentationData: presentationData) + let dismissAction: () -> Void = { [weak controller] in + controller?.dismissAnimated() + } + controller.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: isGroup ? presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text : presentationData.strings.ChannelInfo_InviteLink_RevokeAlert_Text), + ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: { + dismissAction() + + var revoke = false + updateState { state in + if !state.revokingPrivateLink { + revoke = true + var updatedState = state + updatedState.revokingPrivateLink = true + return updatedState + } else { + return state + } + } + if revoke, let inviteLink = invite.link { + revokeLinkDisposable.set((context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: inviteLink) |> deliverOnMainQueue).start(next: { result in + updateState { state in + var updatedState = state + updatedState.revokingPrivateLink = false + return updatedState + } + if let result = result { + switch result { + case let .update(newInvite): + invitesContext.remove(newInvite) + revokedInvitesContext.add(newInvite) + case let .replace(previousInvite, newInvite): + revokedInvitesContext.add(previousInvite) + invitesContext.remove(previousInvite) + invitesContext.add(newInvite) + } + } + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .linkRevoked(text: presentationData.strings.InviteLink_InviteLinkRevoked), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + })) + } + }) + ]), + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ]) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }) + }))) + } + + let contextController = makeContextController(presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + presentInGlobalOverlayImpl?(contextController) + }) }, createLink: { let controller = inviteLinkEditController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, invite: nil, completion: { invite in if let invite = invite { @@ -649,8 +681,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 +693,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 +714,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 +731,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) }))) @@ -710,10 +740,17 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio }, action: { _, f in f(.default) - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { peer in let isGroup: Bool - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } else { isGroup = true @@ -780,10 +817,17 @@ public func inviteLinkListController(context: AccountContext, updatedPresentatio }, action: { _, f in f(.default) - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { peer in let isGroup: Bool - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } else { isGroup = true diff --git a/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift b/submodules/InviteLinksUI/Sources/InviteLinkViewController.swift index 41a6025c11..25df8e383c 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() @@ -743,13 +741,20 @@ public final class InviteLinkViewController: ViewController { }, action: { [weak self] _, f in f(.dismissWithoutContent) - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { [weak self] peer in guard let strongSelf = self, let parentController = strongSelf.controller else { return } let isGroup: Bool - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } else { isGroup = true @@ -765,10 +770,17 @@ public final class InviteLinkViewController: ViewController { }, action: { [weak self] _, f in f(.dismissWithoutContent) - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { peer in let isGroup: Bool - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } else { isGroup = true @@ -825,11 +837,19 @@ public final class InviteLinkViewController: ViewController { } if case let .link(_, _, _, _, _, adminId, date, _, _, usageLimit, _, _, _) = invite { + let creatorPeerSignal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: adminId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } self.disposable = (combineLatest( self.presentationDataPromise.get(), self.importersContext.state, requestsState, - context.account.postbox.loadedPeerWithId(adminId) + creatorPeerSignal ) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, requestsState, creatorPeer in if let strongSelf = self { let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0 @@ -851,7 +871,7 @@ public final class InviteLinkViewController: ViewController { } entries.append(.creatorHeader(presentationData.theme, presentationData.strings.InviteLink_CreatedBy.uppercased())) - entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, EnginePeer(creatorPeer), date)) + entries.append(.creator(presentationData.theme, presentationData.dateTimeFormat, creatorPeer, date)) if !requestsState.importers.isEmpty || (state.isLoadingMore && requestsState.count > 0) { entries.append(.requestHeader(presentationData.theme, presentationData.strings.MemberRequests_PeopleRequested(Int32(requestsState.count)).uppercased(), "", false)) diff --git a/submodules/InviteLinksUI/Sources/InviteRequestsController.swift b/submodules/InviteLinksUI/Sources/InviteRequestsController.swift index 8ab569cab3..6e38456b86 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 { @@ -377,7 +376,7 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio } } navigateToProfileImpl = { [weak controller] peer in - if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { + if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { navigationController.pushViewController(controller) } } diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkDateLimitItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkDateLimitItem.swift index 3555521e4e..fe0dd5935e 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkDateLimitItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkDateLimitItem.swift @@ -73,14 +73,16 @@ enum InviteLinkTimeLimit: Equatable { final class ItemListInviteLinkTimeLimitItem: ListViewItem, ItemListItem { let theme: PresentationTheme + let systemStyle: ItemListSystemStyle let strings: PresentationStrings let value: InviteLinkTimeLimit let enabled: Bool let sectionId: ItemListSectionId let updated: (InviteLinkTimeLimit) -> Void - init(theme: PresentationTheme, strings: PresentationStrings, value: InviteLinkTimeLimit, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (InviteLinkTimeLimit) -> Void) { + init(theme: PresentationTheme, systemStyle: ItemListSystemStyle, strings: PresentationStrings, value: InviteLinkTimeLimit, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (InviteLinkTimeLimit) -> Void) { self.theme = theme + self.systemStyle = systemStyle self.strings = strings self.value = value self.enabled = enabled @@ -326,7 +328,7 @@ private final class ItemListInviteLinkTimeLimitItemNode: ListViewItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteLinkUsageLimitItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteLinkUsageLimitItem.swift index 3bc5994529..8b297264d9 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteLinkUsageLimitItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteLinkUsageLimitItem.swift @@ -87,6 +87,7 @@ enum InviteLinkUsageLimit: Equatable { final class ItemListInviteLinkUsageLimitItem: ListViewItem, ItemListItem { let theme: PresentationTheme + let systemStyle: ItemListSystemStyle let strings: PresentationStrings let dateTimeFormat: PresentationDateTimeFormat let value: InviteLinkUsageLimit @@ -94,8 +95,9 @@ final class ItemListInviteLinkUsageLimitItem: ListViewItem, ItemListItem { let sectionId: ItemListSectionId let updated: (InviteLinkUsageLimit) -> Void - init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, value: InviteLinkUsageLimit, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (InviteLinkUsageLimit) -> Void) { + init(theme: PresentationTheme, systemStyle: ItemListSystemStyle, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, value: InviteLinkUsageLimit, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (InviteLinkUsageLimit) -> Void) { self.theme = theme + self.systemStyle = systemStyle self.strings = strings self.dateTimeFormat = dateTimeFormat self.value = value @@ -336,7 +338,7 @@ private final class ItemListInviteLinkUsageLimitItemNode: ListViewItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) diff --git a/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift b/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift index 8af202a948..6e389b97ed 100644 --- a/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift +++ b/submodules/InviteLinksUI/Sources/ItemListInviteRequestItem.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/ItemListAvatarAndNameInfoItem/BUILD b/submodules/ItemListAvatarAndNameInfoItem/BUILD index 35a105dd3c..c43af7de09 100644 --- a/submodules/ItemListAvatarAndNameInfoItem/BUILD +++ b/submodules/ItemListAvatarAndNameInfoItem/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/PeerPresenceStatusManager:PeerPresenceStatusManager", diff --git a/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift b/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift index 3c92955fe6..170c5af092 100644 --- a/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift +++ b/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData @@ -135,7 +134,7 @@ public enum ItemListAvatarAndNameInfoItemMode { public class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { public enum ItemContext { case accountContext(AccountContext) - case other(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network) + case other(accountPeerId: EnginePeer.Id, stateManager: AccountStateManager) var accountContext: AccountContext? { if case let .accountContext(accountContext) = self { @@ -758,8 +757,8 @@ public class ItemListAvatarAndNameInfoItemNode: ListViewItemNode, ItemListItemNo switch item.itemContext { case let .accountContext(context): strongSelf.avatarNode.setPeer(context: context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: ignoreEmpty ? nil : item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) - case let .other(accountPeerId, postbox, network): - strongSelf.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: postbox, network: network, contentSettings: .default, theme: item.presentationData.theme, peer: peer, authorOfMessage: nil, overrideImage: overrideImage, emptyColor: ignoreEmpty ? nil : item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) + case let .other(accountPeerId, stateManager): + strongSelf.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: stateManager.postbox, network: stateManager.network, contentSettings: .default, theme: item.presentationData.theme, peer: peer, authorOfMessage: nil, overrideImage: overrideImage, emptyColor: ignoreEmpty ? nil : item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoads) } } diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index e7612f066c..2d999a8023 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -326,25 +326,22 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { public enum Context { public final class Custom { public let accountPeerId: EnginePeer.Id - public let postbox: Postbox - public let network: Network + public let engine: TelegramEngine public let animationCache: AnimationCache public let animationRenderer: MultiAnimationRenderer public let isPremiumDisabled: Bool public let resolveInlineStickers: ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> - + public init( accountPeerId: EnginePeer.Id, - postbox: Postbox, - network: Network, + engine: TelegramEngine, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isPremiumDisabled: Bool, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> ) { self.accountPeerId = accountPeerId - self.postbox = postbox - self.network = network + self.engine = engine self.animationCache = animationCache self.animationRenderer = animationRenderer self.isPremiumDisabled = isPremiumDisabled @@ -364,21 +361,12 @@ public final class ItemListPeerItem: ListViewItem, ItemListItem { } } - public var postbox: Postbox { + public var engine: TelegramEngine { switch self { case let .account(context): - return context.account.postbox + return context.engine case let .custom(custom): - return custom.postbox - } - } - - public var network: Network { - switch self { - case let .account(context): - return context.account.network - case let .custom(custom): - return custom.network + return custom.engine } } @@ -952,6 +940,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo if item.peer.isVerified { credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) + credibilityParticleColor = nil } if let verificationIconFileId = item.peer.verificationIconFileId { verifiedIcon = .animation(content: .customEmoji(fileId: verificationIconFileId), size: CGSize(width: 32.0, height: 32.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(0)) @@ -1494,7 +1483,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } let verifiedIconComponent = EmojiStatusComponent( - postbox: item.context.postbox, + postbox: item.context.engine.account.postbox, energyUsageSettings: item.context.energyUsageSettings, resolveInlineStickers: item.context.resolveInlineStickers, animationCache: animationCache, @@ -1542,7 +1531,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } let credibilityIconComponent = EmojiStatusComponent( - postbox: item.context.postbox, + postbox: item.context.engine.account.postbox, energyUsageSettings: item.context.energyUsageSettings, resolveInlineStickers: item.context.resolveInlineStickers, animationCache: animationCache, @@ -1701,7 +1690,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } let avatarIconComponent = EmojiStatusComponent( - postbox: item.context.postbox, + postbox: item.context.engine.account.postbox, energyUsageSettings: item.context.energyUsageSettings, resolveInlineStickers: item.context.resolveInlineStickers, animationCache: item.context.animationCache, @@ -1729,8 +1718,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo if item.peer.id == item.context.accountPeerId, case .threatSelfAsSaved = item.aliasHandling { strongSelf.avatarNode.setPeer( accountPeerId: item.context.accountPeerId, - postbox: item.context.postbox, - network: item.context.network, + postbox: item.context.engine.account.postbox, + network: item.context.engine.account.network, contentSettings: item.context.contentSettings, theme: item.presentationData.theme, peer: item.peer, @@ -1741,8 +1730,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } else if item.peer.id.isReplies { strongSelf.avatarNode.setPeer( accountPeerId: item.context.accountPeerId, - postbox: item.context.postbox, - network: item.context.network, + postbox: item.context.engine.account.postbox, + network: item.context.engine.account.network, contentSettings: item.context.contentSettings, theme: item.presentationData.theme, peer: item.peer, @@ -1764,8 +1753,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo strongSelf.avatarNode.setPeer( accountPeerId: item.context.accountPeerId, - postbox: item.context.postbox, - network: item.context.network, + postbox: item.context.engine.account.postbox, + network: item.context.engine.account.network, contentSettings: item.context.contentSettings, theme: item.presentationData.theme, peer: item.peer, diff --git a/submodules/ItemListStickerPackItem/BUILD b/submodules/ItemListStickerPackItem/BUILD index 5752bd50bc..f82bb003b7 100644 --- a/submodules/ItemListStickerPackItem/BUILD +++ b/submodules/ItemListStickerPackItem/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/ItemListUI:ItemListUI", diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index 620f6d9657..73a61b8757 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import ItemListUI @@ -52,12 +51,12 @@ public final class ItemListStickerPackItem: ListViewItem, ItemListItem { let style: ItemListStyle public let sectionId: ItemListSectionId let action: (() -> Void)? - let setPackIdWithRevealedOptions: (ItemCollectionId?, ItemCollectionId?) -> Void + let setPackIdWithRevealedOptions: (EngineItemCollectionId?, EngineItemCollectionId?) -> Void let addPack: () -> Void let removePack: () -> Void let toggleSelected: () -> Void - public init(presentationData: ItemListPresentationData, context: AccountContext, systemStyle: ItemListSystemStyle = .legacy, packInfo: StickerPackCollectionInfo.Accessor, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListStickerPackItemControl, editing: ItemListStickerPackItemEditing, enabled: Bool, playAnimatedStickers: Bool, style: ItemListStyle = .blocks, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void, toggleSelected: @escaping () -> Void) { + public init(presentationData: ItemListPresentationData, context: AccountContext, systemStyle: ItemListSystemStyle = .legacy, packInfo: StickerPackCollectionInfo.Accessor, itemCount: String, topItem: StickerPackItem?, unread: Bool, control: ItemListStickerPackItemControl, editing: ItemListStickerPackItemEditing, enabled: Bool, playAnimatedStickers: Bool, style: ItemListStyle = .blocks, sectionId: ItemListSectionId, action: (() -> Void)?, setPackIdWithRevealedOptions: @escaping (EngineItemCollectionId?, EngineItemCollectionId?) -> Void, addPack: @escaping () -> Void, removePack: @escaping () -> Void, toggleSelected: @escaping () -> Void) { self.presentationData = presentationData self.context = context self.systemStyle = systemStyle @@ -126,8 +125,8 @@ public final class ItemListStickerPackItem: ListViewItem, ItemListItem { public enum StickerPackThumbnailItem: Equatable { case still(TelegramMediaImageRepresentation) - case animated(MediaResource, PixelDimensions, Bool, Bool) - + case animated(EngineMediaResource, PixelDimensions, Bool, Bool) + public static func ==(lhs: StickerPackThumbnailItem, rhs: StickerPackThumbnailItem) -> Bool { switch lhs { case let .still(representation): @@ -137,7 +136,7 @@ public enum StickerPackThumbnailItem: Equatable { return false } case let .animated(lhsResource, lhsDimensions, lhsIsVideo, lhsTinted): - if case let .animated(rhsResource, rhsDimensions, rhsIsVideo, rhsTinted) = rhs, lhsResource.isEqual(to: rhsResource), lhsDimensions == rhsDimensions, lhsIsVideo == rhsIsVideo, lhsTinted == rhsTinted { + if case let .animated(rhsResource, rhsDimensions, rhsIsVideo, rhsTinted) = rhs, lhsResource == rhsResource, lhsDimensions == rhsDimensions, lhsIsVideo == rhsIsVideo, lhsTinted == rhsTinted { return true } else { return false @@ -500,7 +499,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { var resourceReference: MediaResourceReference? if item.packInfo.hasThumbnail, let thumbnail = item.packInfo._parse().thumbnail { if thumbnail.typeHint != .generic { - thumbnailItem = .animated(thumbnail.resource, thumbnail.dimensions, thumbnail.typeHint == .video, item.packInfo.flags.contains(.isCustomTemplateEmoji)) + thumbnailItem = .animated(EngineMediaResource(thumbnail.resource), thumbnail.dimensions, thumbnail.typeHint == .video, item.packInfo.flags.contains(.isCustomTemplateEmoji)) } else { thumbnailItem = .still(thumbnail) } @@ -508,7 +507,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } else if let item = item.topItem { let itemFile = item.file._parse() if itemFile.isAnimatedSticker || itemFile.isVideoSticker { - thumbnailItem = .animated(itemFile.resource, itemFile.dimensions ?? PixelDimensions(width: 100, height: 100), itemFile.isVideoSticker, itemFile.isCustomTemplateEmoji) + thumbnailItem = .animated(EngineMediaResource(itemFile.resource), itemFile.dimensions ?? PixelDimensions(width: 100, height: 100), itemFile.isVideoSticker, itemFile.isCustomTemplateEmoji) resourceReference = MediaResourceReference.media(media: .standalone(media: itemFile), resource: itemFile.resource) } else if let dimensions = itemFile.dimensions, let resource = chatMessageStickerResource(file: itemFile, small: true) as? TelegramMediaResource { thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) @@ -517,7 +516,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } var updatedImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - var updatedFetchSignal: Signal? + var updatedFetchSignal: Signal? let imageBoundingSize = CGSize(width: 34.0, height: 34.0) var imageApply: (() -> Void)? @@ -537,14 +536,14 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } case let .animated(resource, dimensions, _, _): imageSize = dimensions.cgSize.aspectFitted(imageBoundingSize) - + if fileUpdated { imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageBoundingSize, boundingSize: imageBoundingSize, intrinsicInsets: UIEdgeInsets())) - updatedImageSignal = chatMessageStickerPackThumbnail(postbox: item.context.account.postbox, resource: resource, animated: true, nilIfEmpty: true) + updatedImageSignal = chatMessageStickerPackThumbnail(postbox: item.context.account.postbox, resource: resource._asResource(), animated: true, nilIfEmpty: true) } } if fileUpdated, let resourceReference = resourceReference { - updatedFetchSignal = fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: resourceReference) + updatedFetchSignal = item.context.engine.resources.fetch(reference: resourceReference, userLocation: .other, userContentType: .sticker) } } else { updatedImageSignal = .single({ _ in return nil }) @@ -829,9 +828,9 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } strongSelf.animationNode = animationNode strongSelf.addSubnode(animationNode) - - let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id) - animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: resource, isVideo: isVideo), width: 80, height: 80, playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) + + let pathPrefix = item.context.engine.resources.shortLivedResourceCachePathPrefix(id: resource.id) + animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: resource._asResource(), isVideo: isVideo), width: 80, height: 80, playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) } animationNode.visibility = strongSelf.visibility != .none && item.playAnimatedStickers animationNode.isHidden = !item.playAnimatedStickers 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/LegacyComponents/PublicHeaders/LegacyComponents/NSInputStream+TL.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/NSInputStream+TL.h index aacf17f8e8..5c9c0105b4 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/NSInputStream+TL.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/NSInputStream+TL.h @@ -4,11 +4,8 @@ @interface NSInputStream (TL) -- (int32_t)readInt32; - (int32_t)readInt32:(bool *)failed __attribute__((nonnull(1))); -- (int64_t)readInt64; - (int64_t)readInt64:(bool *)failed __attribute__((nonnull(1))); -- (double)readDouble; - (double)readDouble:(bool *)failed __attribute__((nonnull(1))); - (NSData *)readData:(int)length; - (NSData *)readData:(int)length failed:(bool *)failed __attribute__((nonnull(2))); diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h index 38d1fdfa9f..3ce8d62e49 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h @@ -35,7 +35,7 @@ @property (nonatomic) bool reminder; @property (nonatomic) bool forum; @property (nonatomic) bool isSuggesting; -@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t)); +@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t, bool)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t)); @property (nonatomic, strong) NSArray *underlyingViews; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h index d1e0b2fcc0..7e1164ee05 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h @@ -63,7 +63,7 @@ typedef enum { @property (nonatomic, copy) void(^finishedTransitionOut)(void); @property (nonatomic, copy) void(^customPresentOverlayController)(TGOverlayController *(^)(id)); -@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t)); +@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t, bool)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t)); - (instancetype)initWithContext:(id)context saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h index 178f043270..1578e0f772 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h @@ -70,7 +70,7 @@ typedef enum @property (nonatomic, assign) bool forum; @property (nonatomic, assign) bool isSuggesting; -@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t)); +@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t, bool)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t)); @property (nonatomic, assign) bool liveVideoUploadEnabled; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerController.h index c256f0832c..42ade02f88 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerController.h @@ -35,7 +35,7 @@ @property (nonatomic, assign) bool forum; @property (nonatomic, assign) bool isSuggesting; -@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t)); +@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t, bool)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t)); @property (nonatomic, assign) CGFloat topInset; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerModernGalleryMixin.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerModernGalleryMixin.h index 24f4876030..46aacd32a1 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerModernGalleryMixin.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerModernGalleryMixin.h @@ -28,7 +28,7 @@ @property (nonatomic, copy) void (^editorOpened)(void); @property (nonatomic, copy) void (^editorClosed)(void); -@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t)); +@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t, bool)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t)); - (instancetype)initWithContext:(id)context item:(id)item fetchResult:(TGMediaAssetFetchResult *)fetchResult parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions inhibitMute:(bool)inhibitMute asFile:(bool)asFile itemsLimit:(NSUInteger)itemsLimit recipientName:(NSString *)recipientName hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder hasCoverButton:(bool)hasCoverButton stickersContext:(id)stickersContext; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h index 44d2e574b2..bd58b0f294 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoCaptionInputMixin.h @@ -8,7 +8,6 @@ @interface TGPhotoCaptionInputMixin : NSObject @property (nonatomic, strong) id stickersContext; -@property (nonatomic, readonly) UIView *backgroundView; @property (nonatomic, readonly) id inputPanel; @property (nonatomic, readonly) UIView *inputPanelView; @property (nonatomic, readonly) UIView *dismissView; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index 05095af3af..4afdca957e 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -37,6 +37,7 @@ @protocol TGCaptionPanelView @property (nonatomic, readonly) UIView * _Nonnull view; +@property (nonatomic, readonly) CGFloat additionalInputHeight; - (void)setTimeout:(int32_t)timeout isVideo:(bool)isVideo isCaptionAbove:(bool)isCaptionAbove; @@ -55,6 +56,10 @@ @property (nonatomic, copy) void(^ _Nullable timerUpdated)(NSNumber * _Nullable value); @property (nonatomic, copy) void(^ _Nullable captionIsAboveUpdated)(BOOL value); +@optional +- (CGFloat)updateContainerLayoutSize:(CGSize)size safeAreaInset:(UIEdgeInsets)safeAreaInset bottomInset:(CGFloat)bottomInset keyboardHeight:(CGFloat)keyboardHeight animated:(bool)animated; + +@required - (CGFloat)updateLayoutSize:(CGSize)size keyboardHeight:(CGFloat)keyboardHeight sideInset:(CGFloat)sideInset animated:(bool)animated; - (CGFloat)baseHeight; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h index ef21ecd0b6..2031d9610e 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h @@ -7,12 +7,20 @@ #import #import +@class TGModernGalleryController; + +typedef void (^ _Nonnull TGPhotoVideoEditorSchedulePickerCompletion)(int32_t time, bool silentPosting); +typedef void (^ _Nonnull TGPhotoVideoEditorSchedulePicker)(bool media, TGPhotoVideoEditorSchedulePickerCompletion _Nonnull done); +typedef void (^ _Nonnull TGPhotoVideoEditorCompletion)(id _Nonnull item, TGMediaEditingContext * _Nonnull editingContext, bool silentPosting, int32_t scheduleTime); + @interface TGPhotoVideoEditor : NSObject -+ (void)presentWithContext:(id)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id)stickersContext transitionView:(UIView *)transitionView senderName:(NSString *)senderName didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed; ++ (void)presentWithContext:(id _Nonnull)context parentController:(TGViewController * _Nonnull)parentController image:(UIImage * _Nullable)image video:(NSURL * _Nullable)video stickersContext:(id _Nullable)stickersContext transitionView:(UIView * _Nullable)transitionView senderName:(NSString * _Nullable)senderName didFinishWithImage:(void (^ _Nullable)(UIImage * _Nonnull image))didFinishWithImage didFinishWithVideo:(void (^ _Nullable)(UIImage * _Nonnull image, NSURL * _Nonnull url, TGVideoEditAdjustments * _Nullable adjustments))didFinishWithVideo dismissed:(void (^ _Nonnull)(void))dismissed; -+ (void)presentWithContext:(id)context controller:(TGViewController *)controller caption:(NSAttributedString *)caption withItem:(id)item paint:(bool)paint adjustments:(bool)adjustments recipientName:(NSString *)recipientName stickersContext:(id)stickersContext fromRect:(CGRect)fromRect mainSnapshot:(UIView *)mainSnapshot snapshots:(NSArray *)snapshots immediate:(bool)immediate activateInput:(bool)activateInput isGif:(bool)isGif appeared:(void (^)(void))appeared completion:(void (^)(id, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed; ++ (TGModernGalleryController * _Nonnull)controllerWithContext:(id _Nonnull)context caption:(NSAttributedString * _Nonnull)caption withItem:(id _Nonnull)item paint:(bool)paint adjustments:(bool)adjustments recipientName:(NSString * _Nonnull)recipientName stickersContext:(id _Nullable)stickersContext fromRect:(CGRect)fromRect mainSnapshot:(UIView * _Nullable)mainSnapshot snapshots:(NSArray * _Nonnull)snapshots immediate:(bool)immediate activateInput:(bool)activateInput isGif:(bool)isGif hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder presentSchedulePicker:(TGPhotoVideoEditorSchedulePicker _Nonnull)presentSchedulePicker appeared:(void (^ _Nonnull)(void))appeared completion:(TGPhotoVideoEditorCompletion _Nonnull)completion dismissed:(void (^ _Nonnull)(void))dismissed; -+ (void)presentEditorWithContext:(id)context controller:(TGViewController *)controller withItem:(id)item cropRect:(CGRect)cropRect adjustments:(id)adjustments referenceView:(UIView *)referenceView completion:(void (^)(UIImage *, id))completion fullSizeCompletion:(void (^)(UIImage *))fullSizeCompletion beginTransitionOut:(void (^)(bool))beginTransitionOut finishTransitionOut:(void (^)())finishTransitionOut; ++ (void)presentWithContext:(id _Nonnull)context controller:(TGViewController * _Nonnull)controller caption:(NSAttributedString * _Nonnull)caption withItem:(id _Nonnull)item paint:(bool)paint adjustments:(bool)adjustments recipientName:(NSString * _Nonnull)recipientName stickersContext:(id _Nullable)stickersContext fromRect:(CGRect)fromRect mainSnapshot:(UIView * _Nullable)mainSnapshot snapshots:(NSArray * _Nonnull)snapshots immediate:(bool)immediate activateInput:(bool)activateInput isGif:(bool)isGif hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder presentSchedulePicker:(TGPhotoVideoEditorSchedulePicker _Nonnull)presentSchedulePicker appeared:(void (^ _Nonnull)(void))appeared completion:(TGPhotoVideoEditorCompletion _Nonnull)completion dismissed:(void (^ _Nonnull)(void))dismissed; + ++ (void)presentEditorWithContext:(id _Nonnull)context controller:(TGViewController * _Nonnull)controller withItem:(id _Nonnull)item cropRect:(CGRect)cropRect adjustments:(id _Nullable)adjustments referenceView:(UIView * _Nonnull)referenceView completion:(void (^ _Nonnull)(UIImage * _Nonnull image, id _Nullable adjustments))completion fullSizeCompletion:(void (^ _Nonnull)(UIImage * _Nonnull image))fullSizeCompletion beginTransitionOut:(void (^ _Nullable)(bool saving))beginTransitionOut finishTransitionOut:(void (^ _Nullable)(void))finishTransitionOut; @end diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h index 1538db8304..99af708e39 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h @@ -31,7 +31,7 @@ @property (nonatomic, copy) void(^didDismiss)(void); @property (nonatomic, copy) void(^didStop)(void); @property (nonatomic, copy) void(^displaySlowmodeTooltip)(void); -@property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t)); +@property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t, bool)); - (instancetype)initWithContext:(id)context forStory:(bool)forStory assets:(TGVideoMessageCaptureControllerAssets *)assets transitionInView:(UIView *(^)(void))transitionInView parentController:(TGViewController *)parentController controlsFrame:(CGRect)controlsFrame isAlreadyLocked:(bool (^)(void))isAlreadyLocked liveUploadInterface:(id)liveUploadInterface pallete:(TGModernConversationInputMicPallete *)pallete slowmodeTimestamp:(int32_t)slowmodeTimestamp slowmodeView:(UIView *(^)(void))slowmodeView canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule reminder:(bool)reminder; diff --git a/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@2x.png b/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@2x.png deleted file mode 100644 index 7f9680c2be..0000000000 Binary files a/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@2x.png and /dev/null differ diff --git a/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@3x.png b/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@3x.png deleted file mode 100644 index de8d7887ac..0000000000 Binary files a/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/VideoMessageShadow@3x.png and /dev/null differ diff --git a/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/ocr_nn.bin b/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/ocr_nn.bin deleted file mode 100755 index ccab89e554..0000000000 Binary files a/submodules/LegacyComponents/Resources/LegacyComponentsResources.bundle/ocr_nn.bin and /dev/null differ 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/NSInputStream+TL.m b/submodules/LegacyComponents/Sources/NSInputStream+TL.m index 36fd423570..a27517b88b 100644 --- a/submodules/LegacyComponents/Sources/NSInputStream+TL.m +++ b/submodules/LegacyComponents/Sources/NSInputStream+TL.m @@ -22,27 +22,6 @@ static inline int roundUpInput(int numToRound, int multiple) @implementation NSInputStream (TL) -- (int32_t)readInt32 -{ - int32_t value = 0; - - if ([self read:(uint8_t *)&value maxLength:4] != 4) - { - TGLegacyLog(@"***** Couldn't read int32"); - - @throw [[NSException alloc] initWithName:@"NSInputStream+TLException" reason:@"readInt32 end of stream" userInfo:@{}]; - } - -#if __BYTE_ORDER == __LITTLE_ENDIAN -#elif __BYTE_ORDER == __BIG_ENDIAN -# error "Big endian is not implemented" -#else -# error "Unknown byte order" -#endif - - return value; -} - - (int32_t)readInt32:(bool *)failed { int32_t value = 0; 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/TGAuthorSignatureMediaAttachment.m b/submodules/LegacyComponents/Sources/TGAuthorSignatureMediaAttachment.m index 4cd356c201..f24d9ef706 100644 --- a/submodules/LegacyComponents/Sources/TGAuthorSignatureMediaAttachment.m +++ b/submodules/LegacyComponents/Sources/TGAuthorSignatureMediaAttachment.m @@ -34,8 +34,15 @@ - (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is { - int32_t length = [is readInt32]; - NSData *data = [is readData:length]; + bool readingError = false; + int32_t length = [is readInt32:&readingError]; + if (readingError) { + return nil; + } + NSData *data = [is readData:length failed:&readingError]; + if (readingError) { + return nil; + } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" diff --git a/submodules/LegacyComponents/Sources/TGBotContextResultAttachment.m b/submodules/LegacyComponents/Sources/TGBotContextResultAttachment.m index bd97cf7b3b..502f98e44c 100644 --- a/submodules/LegacyComponents/Sources/TGBotContextResultAttachment.m +++ b/submodules/LegacyComponents/Sources/TGBotContextResultAttachment.m @@ -36,8 +36,15 @@ - (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is { - int32_t length = [is readInt32]; - NSData *data = [is readData:length]; + bool readingError = false; + int32_t length = [is readInt32:&readingError]; + if (readingError) { + return nil; + } + NSData *data = [is readData:length failed:&readingError]; + if (readingError) { + return nil; + } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [NSKeyedUnarchiver unarchiveObjectWithData:data]; diff --git a/submodules/LegacyComponents/Sources/TGCameraController.m b/submodules/LegacyComponents/Sources/TGCameraController.m index fd9b44075e..ee9724b20e 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -1656,7 +1656,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus if (strongSelf == nil) return; - strongSelf.presentScheduleController(true, ^(int32_t time) { + strongSelf.presentScheduleController(true, ^(int32_t time, bool silentPosting) { __strong TGCameraController *strongSelf = weakSelf; __strong TGMediaPickerGalleryModel *strongModel = weakModel; @@ -1678,7 +1678,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus [[NSUserDefaults standardUserDefaults] setObject:@(!strongSelf->_selectionContext.grouping) forKey:@"TG_mediaGroupingDisabled_v0"]; if (strongSelf.finishedWithResults != nil) - strongSelf.finishedWithResults(strongController, strongSelf->_selectionContext, strongSelf->_editingContext, item.asset, false, time); + strongSelf.finishedWithResults(strongController, strongSelf->_selectionContext, strongSelf->_editingContext, item.asset, silentPosting, time); [strongSelf _dismissTransitionForResultController:strongController]; }); @@ -2751,6 +2751,8 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus return CGRectMake(0, 82, screenSize.width, screenSize.height - 82 - 83); else if (widescreenWidth == 926.0f) return CGRectMake(0, 82, screenSize.width, screenSize.height - 82 - 83); + else if (widescreenWidth == 912.0f) + return CGRectMake(0, 82, screenSize.width, screenSize.height - 82 - 83); else if (widescreenWidth == 896.0f) return CGRectMake(0, 77, screenSize.width, screenSize.height - 77 - 83); else if (widescreenWidth == 874.0f) @@ -2784,6 +2786,8 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus return CGRectMake(0, 136, screenSize.width, screenSize.height - 136 - 223); else if (widescreenWidth == 926.0f) return CGRectMake(0, 121, screenSize.width, screenSize.height - 121 - 234); + else if (widescreenWidth == 912.0f) + return CGRectMake(0, 136, screenSize.width, screenSize.height - 136 - 216); else if (widescreenWidth == 896.0f) return CGRectMake(0, 121, screenSize.width, screenSize.height - 121 - 223); else if (widescreenWidth == 874.0f) diff --git a/submodules/LegacyComponents/Sources/TGFont.mm b/submodules/LegacyComponents/Sources/TGFont.mm index a5d261521f..311787d42e 100644 --- a/submodules/LegacyComponents/Sources/TGFont.mm +++ b/submodules/LegacyComponents/Sources/TGFont.mm @@ -116,13 +116,9 @@ UIFont *TGFixedSystemFontOfSize(CGFloat size) + (UIFont *)roundedFontOfSize:(CGFloat)size { - if (@available(iOSApplicationExtension 13.0, iOS 13.0, *)) { - UIFontDescriptor *descriptor = [UIFont boldSystemFontOfSize:size].fontDescriptor; - descriptor = [descriptor fontDescriptorWithDesign:UIFontDescriptorSystemDesignRounded]; - return [UIFont fontWithDescriptor:descriptor size:size]; - } else { - return [UIFont fontWithName:@".SFCompactRounded-Semibold" size:size]; - } + UIFontDescriptor *descriptor = [UIFont boldSystemFontOfSize:size].fontDescriptor; + descriptor = [descriptor fontDescriptorWithDesign:UIFontDescriptorSystemDesignRounded]; + return [UIFont fontWithDescriptor:descriptor size:size]; } @end diff --git a/submodules/LegacyComponents/Sources/TGGameMediaAttachment.m b/submodules/LegacyComponents/Sources/TGGameMediaAttachment.m index 365ff1d221..d2ccf31bb2 100644 --- a/submodules/LegacyComponents/Sources/TGGameMediaAttachment.m +++ b/submodules/LegacyComponents/Sources/TGGameMediaAttachment.m @@ -48,8 +48,15 @@ - (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is { - int32_t length = [is readInt32]; - NSData *data = [is readData:length]; + bool readingError = false; + int32_t length = [is readInt32:&readingError]; + if (readingError) { + return nil; + } + NSData *data = [is readData:length failed:&readingError]; + if (readingError) { + return nil; + } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [NSKeyedUnarchiver unarchiveObjectWithData:data]; 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/TGInvoiceMediaAttachment.m b/submodules/LegacyComponents/Sources/TGInvoiceMediaAttachment.m index 1d5f1bc4b4..89220c4470 100644 --- a/submodules/LegacyComponents/Sources/TGInvoiceMediaAttachment.m +++ b/submodules/LegacyComponents/Sources/TGInvoiceMediaAttachment.m @@ -87,8 +87,15 @@ - (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is { - int32_t length = [is readInt32]; - NSData *data = [is readData:length]; + bool readingError = false; + int32_t length = [is readInt32:&readingError]; + if (readingError) { + return nil; + } + NSData *data = [is readData:length failed:&readingError]; + if (readingError) { + return nil; + } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [NSKeyedUnarchiver unarchiveObjectWithData:data]; diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m index 1522c48030..4ff21ec6d9 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -478,7 +478,7 @@ static TGVideoEditAdjustments *TGMediaAssetsPatchedLivePhotoAdjustments(PGPhotoE self.pickerController.isSuggesting = isSuggesting; } -- (void)setPresentScheduleController:(void (^)(bool, void (^)(int32_t)))presentScheduleController { +- (void)setPresentScheduleController:(void (^)(bool, void (^)(int32_t, bool)))presentScheduleController { _presentScheduleController = [presentScheduleController copy]; self.pickerController.presentScheduleController = presentScheduleController; } @@ -1890,8 +1890,8 @@ static TGVideoEditAdjustments *TGMediaAssetsPatchedLivePhotoAdjustments(PGPhotoE - (void)schedule:(bool)media { __weak TGMediaAssetsController *weakSelf = self; - self.presentScheduleController(media, ^(int32_t scheduleTime) { - [weakSelf completeWithCurrentItem:nil silentPosting:false scheduleTime:scheduleTime]; + self.presentScheduleController(media, ^(int32_t scheduleTime, bool silentPosting) { + [weakSelf completeWithCurrentItem:nil silentPosting:silentPosting scheduleTime:scheduleTime]; }); } diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m index f8863cee7c..28564775a5 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryInterfaceView.m @@ -439,7 +439,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * }; _captionMixin.stickersContext = stickersContext; - [_captionMixin createInputPanelIfNeeded]; _headerWrapperView = [[TGMediaPickerGalleryWrapperView alloc] init]; [_wrapperView addSubview:_headerWrapperView]; @@ -486,6 +485,8 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) [_wrapperView addSubview:_landscapeToolbarView]; + [_captionMixin createInputPanelIfNeeded]; + if (hasCoverButton) { _cancelCoverButton = [[TGModernButton alloc] init]; _cancelCoverButton.hidden = true; @@ -1480,7 +1481,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _portraitToolbarView.alpha = alpha; _landscapeToolbarView.alpha = alpha; _captionMixin.inputPanelView.alpha = alpha; - _captionMixin.backgroundView.alpha = alpha; _captionMixin.livePhotoButtonView.alpha = alpha; } completion:^(BOOL finished) { @@ -1492,7 +1492,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _portraitToolbarView.userInteractionEnabled = !hidden; _landscapeToolbarView.userInteractionEnabled = !hidden; _captionMixin.inputPanelView.userInteractionEnabled = !hidden; - _captionMixin.backgroundView.userInteractionEnabled = !hidden; _captionMixin.livePhotoButtonView.userInteractionEnabled = !hidden; } }]; @@ -1532,9 +1531,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * _captionMixin.inputPanelView.alpha = alpha; _captionMixin.inputPanelView.userInteractionEnabled = !hidden; - _captionMixin.backgroundView.alpha = alpha; - _captionMixin.backgroundView.userInteractionEnabled = !hidden; - _captionMixin.livePhotoButtonView.alpha = alpha; _captionMixin.livePhotoButtonView.userInteractionEnabled = !hidden; } @@ -1722,7 +1718,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * - (void)immediateEditorTransitionIn { [self setSelectionInterfaceHidden:true animated:false]; _captionMixin.inputPanelView.alpha = 0.0f; - _captionMixin.backgroundView.alpha = 0.0f; _portraitToolbarView.doneButton.alpha = 0.0f; _landscapeToolbarView.doneButton.alpha = 0.0f; @@ -1743,7 +1738,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * [UIView animateWithDuration:0.2 animations:^ { _captionMixin.inputPanelView.alpha = 0.0f; - _captionMixin.backgroundView.alpha = 0.0f; _portraitToolbarView.doneButton.alpha = 0.0f; _landscapeToolbarView.doneButton.alpha = 0.0f; }]; @@ -1756,7 +1750,6 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber * [UIView animateWithDuration:0.3 animations:^ { _captionMixin.inputPanelView.alpha = 1.0f; - _captionMixin.backgroundView.alpha = 1.0f; _portraitToolbarView.doneButton.alpha = 1.0f; _landscapeToolbarView.doneButton.alpha = 1.0f; }]; diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m index a1f83e5124..35a79b3278 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m @@ -201,7 +201,7 @@ if (strongSelf == nil) return; - strongSelf.presentScheduleController(true, ^(int32_t time) { + strongSelf.presentScheduleController(true, ^(int32_t time, bool silentPosting) { __strong TGMediaPickerModernGalleryMixin *strongSelf = weakSelf; if (strongSelf == nil) return; @@ -209,7 +209,7 @@ strongSelf->_galleryModel.dismiss(true, false); if (strongSelf.completeWithItem != nil) - strongSelf.completeWithItem(item, false, time); + strongSelf.completeWithItem(item, silentPosting, time); }); }; controller.sendWithTimer = ^{ diff --git a/submodules/LegacyComponents/Sources/TGMessageEntitiesAttachment.m b/submodules/LegacyComponents/Sources/TGMessageEntitiesAttachment.m index 02e8ce7701..fd959f11b3 100644 --- a/submodules/LegacyComponents/Sources/TGMessageEntitiesAttachment.m +++ b/submodules/LegacyComponents/Sources/TGMessageEntitiesAttachment.m @@ -54,8 +54,15 @@ - (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is { - int32_t length = [is readInt32]; - NSData *data = [is readData:length]; + bool readingError = false; + int32_t length = [is readInt32:&readingError]; + if (readingError) { + return nil; + } + NSData *data = [is readData:length failed:&readingError]; + if (readingError) { + return nil; + } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [NSKeyedUnarchiver unarchiveObjectWithData:data]; diff --git a/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m b/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m index d30009fbb9..bc020a1f1d 100644 --- a/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m +++ b/submodules/LegacyComponents/Sources/TGModernConversationInputMicButton.m @@ -265,7 +265,7 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius _innerCircleView.center = centerPoint; _outerCircleView.center = centerPoint; _decoration.center = centerPoint; - _innerIconWrapperView.center = CGPointMake(_decoration.frame.size.width / 2.0f, _decoration.frame.size.height / 2.0f); + _innerIconWrapperView.center = CGPointMake(CGRectGetMidX(_decoration.bounds), CGRectGetMidY(_decoration.bounds)); _lockPanelWrapperView.frame = CGRectMake(floor(centerPoint.x - _lockPanelWrapperView.frame.size.width / 2.0f), floor(centerPoint.y - 122.0f - _lockPanelWrapperView.frame.size.height / 2.0f), _lockPanelWrapperView.frame.size.width, _lockPanelWrapperView.frame.size.height); @@ -918,6 +918,8 @@ static const CGFloat outerCircleMinScale = innerCircleRadius / outerCircleRadius _currentTranslation = MIN(0.0, _currentTranslation * 0.7f + _targetTranslation * 0.3f); _cancelTranslation = MIN(0.0, _cancelTranslation * 0.7f + _cancelTargetTranslation * 0.3f); + [self updateOverlay]; + if (t > _animationStartTime) { CGFloat outerScale = outerCircleMinScale + _currentLevel * (1.0f - outerCircleMinScale); CGAffineTransform translation = CGAffineTransformMakeTranslation(0, _currentTranslation); diff --git a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m index 7c5c7528f8..f92214b2b1 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m +++ b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m @@ -10,13 +10,14 @@ { TGObserverProxy *_keyboardWillChangeFrameProxy; bool _editing; - + UIGestureRecognizer *_dismissTapRecognizer; - + CGRect _currentFrame; UIEdgeInsets _currentEdgeInsets; - + bool _currentIsCaptionAbove; + CGFloat _effectiveKeyboardHeight; } @end @@ -48,7 +49,7 @@ { if (_inputPanel != nil) return; - + UIView *parentView = [self _parentView]; id inputPanel = nil; @@ -63,7 +64,7 @@ [TGViewController enableAutorotation]; strongSelf->_editing = false; - + if (strongSelf.finishedWithCaption != nil) strongSelf.finishedWithCaption(string); }; @@ -104,10 +105,9 @@ }; _inputPanelView = inputPanel.view; + _inputPanelView.frame = parentView.bounds; + _inputPanelView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - _backgroundView = [[UIView alloc] init]; - _backgroundView.backgroundColor = [TGPhotoEditorInterfaceAssets toolbarTransparentBackgroundColor]; - //[parentView addSubview:_backgroundView]; [parentView addSubview:_inputPanelView]; if (_stickersContext && _stickersContext.livePhotoButton != nil) { @@ -138,6 +138,10 @@ - (void)createDismissViewIfNeeded { + if (_dismissView != nil) { + return; + } + UIView *parentView = [self _parentView]; _dismissView = [[UIView alloc] initWithFrame:parentView.bounds]; @@ -207,11 +211,36 @@ _dismissTapRecognizer.enabled = true; } +- (CGFloat)currentEffectiveKeyboardHeight +{ + return MAX(_keyboardHeight, _inputPanel.additionalInputHeight); +} + +- (bool)usesContainerLayout +{ + return [_inputPanel respondsToSelector:@selector(updateContainerLayoutSize:safeAreaInset:bottomInset:keyboardHeight:animated:)]; +} + +- (CGFloat)updateInputPanelLayoutForFrame:(CGRect)frame edgeInsets:(UIEdgeInsets)edgeInsets keyboardHeight:(CGFloat)keyboardHeight animated:(bool)animated +{ + if ([self usesContainerLayout]) { + return [_inputPanel updateContainerLayoutSize:frame.size safeAreaInset:_safeAreaInset bottomInset:edgeInsets.bottom keyboardHeight:keyboardHeight animated:animated]; + } else { + return [_inputPanel updateLayoutSize:frame.size keyboardHeight:keyboardHeight sideInset:0.0 animated:animated]; + } +} + #pragma mark - - (void)finishEditing { if ([self.inputPanel dismissInput]) { _editing = false; + + [UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + _dismissView.alpha = 0.0f; + } completion:^(BOOL finished) { + + }]; if (self.finishedWithCaption != nil) self.finishedWithCaption([_inputPanel caption]); @@ -261,9 +290,19 @@ } _keyboardHeight = keyboardHeight; - + if (!UIInterfaceOrientationIsPortrait([[LegacyComponentsGlobals provider] applicationStatusBarOrientation]) && !TGIsPad()) + return; + + CGRect frame = _currentFrame; + UIEdgeInsets edgeInsets = _currentEdgeInsets; + bool usesContainerLayout = [self usesContainerLayout]; + CGRect containerFrame = CGRectMake(edgeInsets.left, 0.0, frame.size.width, frame.size.height); + _inputPanelView.frame = containerFrame; + CGFloat panelHeight = [self updateInputPanelLayoutForFrame:frame edgeInsets:edgeInsets keyboardHeight:keyboardHeight animated:usesContainerLayout]; + CGFloat effectiveKeyboardHeight = [self currentEffectiveKeyboardHeight]; + _effectiveKeyboardHeight = effectiveKeyboardHeight; CGFloat fadeAlpha = 1.0; - if (keyboardHeight < FLT_EPSILON) { + if (effectiveKeyboardHeight < FLT_EPSILON) { fadeAlpha = 0.0; } @@ -275,87 +314,90 @@ }]; } - if (!UIInterfaceOrientationIsPortrait([[LegacyComponentsGlobals provider] applicationStatusBarOrientation]) && !TGIsPad()) - return; - - CGRect frame = _currentFrame; - UIEdgeInsets edgeInsets = _currentEdgeInsets; - CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size keyboardHeight:keyboardHeight sideInset:0.0 animated:false]; - - CGFloat panelY = 0.0; - if (frame.size.width > frame.size.height && !TGIsPad()) { - panelY = edgeInsets.top + frame.size.height; - } else { - if (_currentIsCaptionAbove) { - if (_keyboardHeight > 0.0) { - panelY = _safeAreaInset.top + 8.0; - } else { - panelY = _safeAreaInset.top + 8.0 + 40.0; - } + if (!usesContainerLayout) { + CGFloat panelY = 0.0; + if (frame.size.width > frame.size.height && !TGIsPad()) { + panelY = edgeInsets.top + frame.size.height; } else { - panelY = edgeInsets.top + frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight); + if (_currentIsCaptionAbove) { + if (effectiveKeyboardHeight > 0.0) { + panelY = _safeAreaInset.top + 8.0; + } else { + panelY = _safeAreaInset.top + 8.0 + 40.0; + } + } else { + panelY = edgeInsets.top + frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight); + } } + + [UIView animateWithDuration:duration delay:0.0f options:(curve << 16) animations:^{ + _inputPanelView.frame = CGRectMake(edgeInsets.left, panelY, frame.size.width, panelHeight); + } completion:nil]; } - - CGFloat backgroundHeight = panelHeight; - if (_keyboardHeight > 0.0) { - backgroundHeight += _keyboardHeight - edgeInsets.bottom; - } - - [UIView animateWithDuration:duration delay:0.0f options:(curve << 16) animations:^{ - _inputPanelView.frame = CGRectMake(edgeInsets.left, panelY, frame.size.width, panelHeight); - _backgroundView.frame = CGRectMake(edgeInsets.left, panelY, frame.size.width, backgroundHeight); - } completion:nil]; if (self.keyboardHeightChanged != nil) - self.keyboardHeightChanged(keyboardHeight, duration, curve); + self.keyboardHeightChanged(effectiveKeyboardHeight, duration, curve); } - (void)updateLayoutWithFrame:(CGRect)frame edgeInsets:(UIEdgeInsets)edgeInsets animated:(bool)animated { _currentFrame = frame; _currentEdgeInsets = edgeInsets; - - CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size keyboardHeight:_keyboardHeight sideInset:0.0 animated:animated]; - - CGFloat panelY = 0.0; - if (frame.size.width > frame.size.height && !TGIsPad()) { - panelY = edgeInsets.top + frame.size.height; - } else { - if (_currentIsCaptionAbove) { - if (_keyboardHeight > 0.0) { - panelY = _safeAreaInset.top + 8.0; - } else { - panelY = _safeAreaInset.top + 8.0 + 40.0; - } + + bool usesContainerLayout = [self usesContainerLayout]; + CGRect containerFrame = CGRectMake(edgeInsets.left, 0.0, frame.size.width, frame.size.height); + CGFloat panelHeight = [self updateInputPanelLayoutForFrame:frame edgeInsets:edgeInsets keyboardHeight:_keyboardHeight animated:animated]; + CGFloat effectiveKeyboardHeight = [self currentEffectiveKeyboardHeight]; + CGFloat fadeAlpha = effectiveKeyboardHeight < FLT_EPSILON ? 0.0 : 1.0; + if (ABS(_dismissView.alpha - fadeAlpha) > FLT_EPSILON) { + if (animated) { + [UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + _dismissView.alpha = fadeAlpha; + } completion:nil]; } else { - panelY = edgeInsets.top + frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight); + _dismissView.alpha = fadeAlpha; } } - CGFloat backgroundHeight = panelHeight; - if (_keyboardHeight > 0.0) { - backgroundHeight += _keyboardHeight - edgeInsets.bottom; - } - CGFloat livePhotoY = 0.0; if (frame.size.width > frame.size.height && !TGIsPad()) { livePhotoY = 48.0; } else { - livePhotoY = edgeInsets.top + 145.0 - _keyboardHeight; + livePhotoY = edgeInsets.top + 145.0 - effectiveKeyboardHeight; } CGRect livePhotoButtonFrame = CGRectMake(_safeAreaInset.left + edgeInsets.left + 16.0, livePhotoY, 160.0, 18.0); _livePhotoButtonView.frame = livePhotoButtonFrame; - - CGRect panelFrame = CGRectMake(edgeInsets.left, panelY, frame.size.width, panelHeight); - CGRect backgroundFrame = CGRectMake(edgeInsets.left, panelY, frame.size.width, backgroundHeight); - - if (animated) { - [_inputPanel animateView:_inputPanelView frame:panelFrame]; - [_inputPanel animateView:_backgroundView frame:backgroundFrame]; + + if (usesContainerLayout) { + _inputPanelView.frame = containerFrame; } else { - _inputPanelView.frame = panelFrame; - _backgroundView.frame = backgroundFrame; + CGFloat panelY = 0.0; + if (frame.size.width > frame.size.height && !TGIsPad()) { + panelY = edgeInsets.top + frame.size.height; + } else { + if (_currentIsCaptionAbove) { + if (effectiveKeyboardHeight > 0.0) { + panelY = _safeAreaInset.top + 8.0; + } else { + panelY = _safeAreaInset.top + 8.0 + 40.0; + } + } else { + panelY = edgeInsets.top + frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight); + } + } + + CGRect panelFrame = CGRectMake(edgeInsets.left, panelY, frame.size.width, panelHeight); + if (animated) { + [_inputPanel animateView:_inputPanelView frame:panelFrame]; + } else { + _inputPanelView.frame = panelFrame; + } + } + if (ABS(_effectiveKeyboardHeight - effectiveKeyboardHeight) > FLT_EPSILON) { + _effectiveKeyboardHeight = effectiveKeyboardHeight; + if (self.keyboardHeightChanged != nil) { + self.keyboardHeightChanged(effectiveKeyboardHeight, animated ? 0.3 : 0.0, UIViewAnimationCurveEaseInOut); + } } } diff --git a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m index 9aa57316db..c407675ed9 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m +++ b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m @@ -5,6 +5,7 @@ #import #import +#import #import #import @@ -126,9 +127,10 @@ transitionView.alpha = 1.0; dispatch_async(dispatch_get_main_queue(), ^ { - if (completion != nil) + if (completion != nil) { completion(); - dismissed(); + } + dismissed(); }); }]; }; @@ -155,21 +157,18 @@ } } -+ (void)presentWithContext:(id)context controller:(TGViewController *)controller caption:(NSAttributedString *)caption withItem:(id)item paint:(bool)paint adjustments:(bool)adjustments recipientName:(NSString *)recipientName stickersContext:(id)stickersContext fromRect:(CGRect)fromRect mainSnapshot:(UIView *)mainSnapshot snapshots:(NSArray *)snapshots immediate:(bool)immediate activateInput:(bool)activateInput isGif:(bool)isGif appeared:(void (^)(void))appeared completion:(void (^)(id, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed ++ (TGModernGalleryController *)_configuredControllerWithContext:(id _Nonnull)context caption:(NSAttributedString * _Nonnull)caption withItem:(id _Nonnull)item paint:(bool)paint adjustments:(bool)adjustments recipientName:(NSString * _Nonnull)recipientName stickersContext:(id _Nullable)stickersContext fromRect:(CGRect)fromRect mainSnapshot:(UIView * _Nullable)__unused mainSnapshot snapshots:(NSArray * _Nonnull)snapshots immediate:(bool)immediate activateInput:(bool)activateInput isGif:(bool)isGif hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder presentSchedulePicker:(TGPhotoVideoEditorSchedulePicker _Nonnull)presentSchedulePicker appeared:(void (^ _Nonnull)(void))appeared completion:(TGPhotoVideoEditorCompletion _Nonnull)completion completedDismiss:(void (^ _Nullable)(void))completedDismiss customDismiss:(void (^ _Nullable)(void))customDismiss { - id windowManager = [context makeOverlayWindowManager]; - id windowContext = [windowManager context]; - TGMediaEditingContext *editingContext = [[TGMediaEditingContext alloc] init]; [editingContext setForcedCaption:caption]; - TGModernGalleryController *galleryController = [[TGModernGalleryController alloc] initWithContext:windowContext]; + TGModernGalleryController *galleryController = [[TGModernGalleryController alloc] initWithContext:context]; galleryController.adjustsStatusBarVisibility = true; galleryController.animateTransition = !immediate; galleryController.finishedTransitionIn = ^(id item, TGModernGalleryItemView *itemView) { appeared(); }; - //galleryController.hasFadeOutTransition = true; + galleryController.customDismissBlock = customDismiss; id galleryItem = nil; if (item.isVideo) { @@ -180,7 +179,7 @@ galleryItem.editingContext = editingContext; galleryItem.stickersContext = stickersContext; - TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:@[galleryItem] focusItem:galleryItem selectionContext:nil editingContext:editingContext hasCaptions:true allowCaptionEntities:true hasTimer:false onlyCrop:false inhibitDocumentCaptions:false hasSelectionPanel:false hasCamera:false recipientName:recipientName isScheduledMessages:false hasCoverButton:false]; + TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:context items:@[galleryItem] focusItem:galleryItem selectionContext:nil editingContext:editingContext hasCaptions:true allowCaptionEntities:true hasTimer:false onlyCrop:false inhibitDocumentCaptions:false hasSelectionPanel:false hasCamera:false recipientName:recipientName isScheduledMessages:false hasCoverButton:false]; model.controller = galleryController; model.stickersContext = stickersContext; @@ -212,6 +211,7 @@ galleryController.model = model; __weak TGModernGalleryController *weakGalleryController = galleryController; + __weak TGMediaPickerGalleryModel *weakModel = model; [model.interfaceView updateSelectionInterface:1 counterVisible:false animated:false]; model.interfaceView.thumbnailSignalForItem = ^SSignal *(id item) @@ -232,10 +232,62 @@ } if (completion != nil) - completion(item.asset, editingContext); + completion(item.asset, editingContext, false, 0); [strongController dismissWhenReadyAnimated:true]; }; + model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item) + { + __strong TGModernGalleryController *strongController = weakGalleryController; + __strong TGMediaPickerGalleryModel *strongModel = weakModel; + if (strongController == nil || strongModel == nil || !(hasSilentPosting || hasSchedule)) + return; + + if (iosMajorVersion() >= 10) { + UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium]; + [generator impactOccurred]; + } + + TGMediaPickerSendActionSheetController *sendController = [[TGMediaPickerSendActionSheetController alloc] initWithContext:context isDark:true sendButtonFrame:strongModel.interfaceView.doneButtonFrame canSendSilently:hasSilentPosting canSendWhenOnline:hasSchedule canSchedule:hasSchedule reminder:reminder hasTimer:false]; + sendController.modalPresentationStyle = UIModalPresentationOverFullScreen; + sendController.customDismissBlock = ^{ + __strong TGModernGalleryController *strongController = weakGalleryController; + [strongController dismissViewControllerAnimated:false completion:nil]; + }; + void (^complete)(bool, int32_t) = ^(bool silentPosting, int32_t scheduleTime) + { + __strong TGModernGalleryController *strongController = weakGalleryController; + if (strongController == nil) + return; + + if ([item isKindOfClass:[TGMediaPickerGalleryVideoItem class]]) + { + TGMediaPickerGalleryVideoItemView *itemView = (TGMediaPickerGalleryVideoItemView *)[strongController itemViewForItem:item]; + [itemView stop]; + [itemView setPlayButtonHidden:true animated:true]; + } + + if (completion != nil) + completion(item.asset, editingContext, silentPosting, scheduleTime); + + [strongController dismissWhenReadyAnimated:true]; + }; + sendController.send = ^{ + complete(false, 0); + }; + sendController.sendSilently = ^{ + complete(true, 0); + }; + sendController.sendWhenOnline = ^{ + complete(false, 0x7ffffffe); + }; + sendController.schedule = ^{ + presentSchedulePicker(true, ^(int32_t time, bool silentPosting) { + complete(silentPosting, time); + }); + }; + [strongController presentViewController:sendController animated:false completion:nil]; + }; galleryController.beginTransitionIn = ^UIView *(__unused TGMediaPickerGalleryItem *item, __unused TGModernGalleryItemView *itemView) { @@ -246,21 +298,12 @@ { return nil; }; - - galleryController.completedTransitionOut = ^ - { - TGModernGalleryController *strongGalleryController = weakGalleryController; - if (strongGalleryController != nil && strongGalleryController.overlayWindow == nil) + if (completedDismiss != nil) { + galleryController.completedTransitionOut = ^ { - TGNavigationController *navigationController = (TGNavigationController *)strongGalleryController.navigationController; - TGOverlayControllerWindow *window = (TGOverlayControllerWindow *)navigationController.view.window; - if ([window isKindOfClass:[TGOverlayControllerWindow class]]) - [window dismiss]; - } - if (dismissed) { - dismissed(); - } - }; + completedDismiss(); + }; + } if (paint || adjustments) { [model.interfaceView immediateEditorTransitionIn]; @@ -270,8 +313,6 @@ [galleryController.view addSubview:view]; } - TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:windowManager parentController:controller contentController:galleryController]; - controllerWindow.hidden = false; galleryController.view.clipsToBounds = true; if (isGif) { @@ -291,9 +332,33 @@ [model beginEditingCaption]; }); } + + return galleryController; } -+ (void)presentEditorWithContext:(id)context controller:(TGViewController *)controller withItem:(id)item cropRect:(CGRect)cropRect adjustments:(id)adjustments referenceView:(UIView *)referenceView completion:(void (^)(UIImage *, id))completion fullSizeCompletion:(void (^)(UIImage *))fullSizeCompletion beginTransitionOut:(void (^)(bool))beginTransitionOut finishTransitionOut:(void (^)())finishTransitionOut; ++ (TGModernGalleryController * _Nonnull)controllerWithContext:(id _Nonnull)context caption:(NSAttributedString * _Nonnull)caption withItem:(id _Nonnull)item paint:(bool)paint adjustments:(bool)adjustments recipientName:(NSString * _Nonnull)recipientName stickersContext:(id _Nullable)stickersContext fromRect:(CGRect)fromRect mainSnapshot:(UIView * _Nullable)mainSnapshot snapshots:(NSArray * _Nonnull)snapshots immediate:(bool)immediate activateInput:(bool)activateInput isGif:(bool)isGif hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder presentSchedulePicker:(TGPhotoVideoEditorSchedulePicker _Nonnull)presentSchedulePicker appeared:(void (^ _Nonnull)(void))appeared completion:(TGPhotoVideoEditorCompletion _Nonnull)completion dismissed:(void (^ _Nonnull)(void))dismissed +{ + TGModernGalleryController *galleryController = [self _configuredControllerWithContext:context caption:caption withItem:item paint:paint adjustments:adjustments recipientName:recipientName stickersContext:stickersContext fromRect:fromRect mainSnapshot:mainSnapshot snapshots:snapshots immediate:immediate activateInput:activateInput isGif:isGif hasSilentPosting:hasSilentPosting hasSchedule:hasSchedule reminder:reminder presentSchedulePicker:presentSchedulePicker appeared:appeared completion:completion completedDismiss:dismissed customDismiss:nil]; + galleryController.asyncTransitionIn = true; + return galleryController; +} + ++ (void)presentWithContext:(id _Nonnull)context controller:(TGViewController * _Nonnull)controller caption:(NSAttributedString * _Nonnull)caption withItem:(id _Nonnull)item paint:(bool)paint adjustments:(bool)adjustments recipientName:(NSString * _Nonnull)recipientName stickersContext:(id _Nullable)stickersContext fromRect:(CGRect)fromRect mainSnapshot:(UIView * _Nullable)mainSnapshot snapshots:(NSArray * _Nonnull)snapshots immediate:(bool)immediate activateInput:(bool)activateInput isGif:(bool)isGif hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder presentSchedulePicker:(TGPhotoVideoEditorSchedulePicker _Nonnull)presentSchedulePicker appeared:(void (^ _Nonnull)(void))appeared completion:(TGPhotoVideoEditorCompletion _Nonnull)completion dismissed:(void (^ _Nonnull)(void))dismissed +{ + __weak TGViewController *weakController = controller; + TGModernGalleryController *galleryController = [self _configuredControllerWithContext:context caption:caption withItem:item paint:paint adjustments:adjustments recipientName:recipientName stickersContext:stickersContext fromRect:fromRect mainSnapshot:mainSnapshot snapshots:snapshots immediate:immediate activateInput:activateInput isGif:isGif hasSilentPosting:hasSilentPosting hasSchedule:hasSchedule reminder:reminder presentSchedulePicker:presentSchedulePicker appeared:appeared completion:completion completedDismiss:nil customDismiss:^{ + __strong TGViewController *strongController = weakController; + [strongController dismissViewControllerAnimated:false completion:^{ + if (dismissed) { + dismissed(); + } + }]; + }]; + galleryController.modalPresentationStyle = UIModalPresentationFullScreen; + [controller presentViewController:galleryController animated:false completion:nil]; +} + ++ (void)presentEditorWithContext:(id _Nonnull)context controller:(TGViewController * _Nonnull)controller withItem:(id _Nonnull)item cropRect:(CGRect)cropRect adjustments:(id _Nullable)adjustments referenceView:(UIView * _Nonnull)referenceView completion:(void (^ _Nonnull)(UIImage * _Nonnull image, id _Nullable adjustments))completion fullSizeCompletion:(void (^ _Nonnull)(UIImage * _Nonnull image))fullSizeCompletion beginTransitionOut:(void (^ _Nullable)(bool saving))beginTransitionOut finishTransitionOut:(void (^ _Nullable)(void))finishTransitionOut { id windowManager = [context makeOverlayWindowManager]; diff --git a/submodules/LegacyComponents/Sources/TGReplyMarkupAttachment.m b/submodules/LegacyComponents/Sources/TGReplyMarkupAttachment.m index 6a4dac0ad7..cb88e10a15 100644 --- a/submodules/LegacyComponents/Sources/TGReplyMarkupAttachment.m +++ b/submodules/LegacyComponents/Sources/TGReplyMarkupAttachment.m @@ -64,8 +64,15 @@ - (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is { - int32_t length = [is readInt32]; - NSData *data = [is readData:length]; + bool readingError = false; + int32_t length = [is readInt32:&readingError]; + if (readingError) { + return nil; + } + NSData *data = [is readData:length failed:&readingError]; + if (readingError) { + return nil; + } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [NSKeyedUnarchiver unarchiveObjectWithData:data]; diff --git a/submodules/LegacyComponents/Sources/TGViaUserAttachment.m b/submodules/LegacyComponents/Sources/TGViaUserAttachment.m index 51fb7394d8..964941c4e6 100644 --- a/submodules/LegacyComponents/Sources/TGViaUserAttachment.m +++ b/submodules/LegacyComponents/Sources/TGViaUserAttachment.m @@ -36,8 +36,15 @@ - (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is { - int32_t length = [is readInt32]; - NSData *data = [is readData:length]; + bool readingError = false; + int32_t length = [is readInt32:&readingError]; + if (readingError) { + return nil; + } + NSData *data = [is readData:length failed:&readingError]; + if (readingError) { + return nil; + } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return [NSKeyedUnarchiver unarchiveObjectWithData:data]; 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/LegacyComponents/Sources/TGVideoMessageCaptureController.m b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m index 3a410ff2d7..4e0f81e1bc 100644 --- a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m +++ b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m @@ -330,11 +330,7 @@ typedef enum _circleWrapperView.alpha = 0.0f; _circleWrapperView.clipsToBounds = false; [_wrapperView addSubview:_circleWrapperView]; - - _shadowView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"VideoMessageShadow")]; - _shadowView.frame = _circleWrapperView.bounds; - [_circleWrapperView addSubview:_shadowView]; - + _circleView = [[UIView alloc] initWithFrame:CGRectInset(_circleWrapperView.bounds, shadowSize, shadowSize)]; _circleView.clipsToBounds = true; _circleView.layer.cornerRadius = _circleView.frame.size.width / 2.0f; @@ -844,13 +840,13 @@ typedef enum } if (strongSelf.presentScheduleController) { - strongSelf.presentScheduleController(^(int32_t time) { + strongSelf.presentScheduleController(^(int32_t time, bool silentPosting) { __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf == nil) { return; } - [strongSelf finishWithURL:strongSelf->_url dimensions:CGSizeMake(240.0f, 240.0f) duration:strongSelf->_duration liveUploadData:strongSelf->_liveUploadData thumbnailImage:strongSelf->_thumbnailImage isSilent:false scheduleTimestamp:time]; + [strongSelf finishWithURL:strongSelf->_url dimensions:CGSizeMake(240.0f, 240.0f) duration:strongSelf->_duration liveUploadData:strongSelf->_liveUploadData thumbnailImage:strongSelf->_thumbnailImage isSilent:silentPosting scheduleTimestamp:time]; _automaticDismiss = true; [strongSelf dismiss:false]; diff --git a/submodules/LegacyComponents/Sources/TGWebPageMediaAttachment.m b/submodules/LegacyComponents/Sources/TGWebPageMediaAttachment.m index 0550d67ced..413f423174 100644 --- a/submodules/LegacyComponents/Sources/TGWebPageMediaAttachment.m +++ b/submodules/LegacyComponents/Sources/TGWebPageMediaAttachment.m @@ -157,8 +157,15 @@ - (TGMediaAttachment *)parseMediaAttachment:(NSInputStream *)is { - int32_t length = [is readInt32]; - NSData *data = [is readData:length]; + bool readingError = false; + int32_t length = [is readInt32:&readingError]; + if (readingError) { + return nil; + } + NSData *data = [is readData:length failed:&readingError]; + if (readingError) { + return nil; + } @try { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" diff --git a/submodules/LegacyComponents/Sources/ocr.mm b/submodules/LegacyComponents/Sources/ocr.mm index b81744efe8..6ba13ae008 100755 --- a/submodules/LegacyComponents/Sources/ocr.mm +++ b/submodules/LegacyComponents/Sources/ocr.mm @@ -677,6 +677,9 @@ UIImage *normalizeImage(UIImage *image) NSString *recognizeMRZ(UIImage *input, CGRect *outBoundingRect) { + if (@"".length == 0) { + return nil; + } input = normalizeImage(input); UIImage *binaryImage; diff --git a/submodules/LegacyDataImport/Sources/LegacyPreferencesImport.swift b/submodules/LegacyDataImport/Sources/LegacyPreferencesImport.swift index d403653fb5..019bb540ac 100644 --- a/submodules/LegacyDataImport/Sources/LegacyPreferencesImport.swift +++ b/submodules/LegacyDataImport/Sources/LegacyPreferencesImport.swift @@ -3,7 +3,6 @@ import UIKit import TelegramCore import SyncCore import SwiftSignalKit -import Postbox import MtProtoKit import TelegramUIPreferences import LegacyComponents diff --git a/submodules/LegacyDataImport/Sources/LegacyResourceImport.swift b/submodules/LegacyDataImport/Sources/LegacyResourceImport.swift index fd4bc6f2e0..4fc129c16f 100644 --- a/submodules/LegacyDataImport/Sources/LegacyResourceImport.swift +++ b/submodules/LegacyDataImport/Sources/LegacyResourceImport.swift @@ -2,7 +2,6 @@ import Foundation import TelegramCore import SyncCore import SwiftSignalKit -import Postbox import LegacyComponents func resourceFromLegacyImageUrl(_ fileRef: String) -> TelegramMediaResource? { diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index acc28dacf4..a8b9c9cb79 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -131,12 +131,12 @@ public func legacyStoryMediaEditor(context: AccountContext, item: TGMediaEditabl present(legacyController, nil) - TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: NSAttributedString(), withItem: item, paint: false, adjustments: false, recipientName: "", stickersContext: paintStickersContext, from: .zero, mainSnapshot: nil, snapshots: [] as [Any], immediate: true, activateInput: false, isGif: false, appeared: { + TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: NSAttributedString(), withItem: item, paint: false, adjustments: false, recipientName: "", stickersContext: paintStickersContext, from: .zero, mainSnapshot: nil, snapshots: [] as [Any], immediate: true, activateInput: false, isGif: false, hasSilentPosting: false, hasSchedule: false, reminder: false, presentSchedulePicker: { _, _ in }, appeared: { - }, completion: { result, editingContext in + }, completion: { result, editingContext, _, _ in var completionResult: Signal if let photo = result as? TGCameraCapturedPhoto { - if let _ = editingContext?.adjustments(for: result) { + if let _ = editingContext.adjustments(for: result) { completionResult = .single(.image(photo.existingImage)) } else { completionResult = .single(.image(photo.existingImage)) @@ -167,12 +167,16 @@ public func legacyMediaEditor( snapshots: [UIView], transitionCompletion: (() -> Void)?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, + hasSilentPosting: Bool = false, + hasSchedule: Bool = false, + reminder: Bool = false, + presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void = { _, _ in }, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, Bool) -> Void, present: @escaping (ViewController, Any?) -> Void ) { - let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media) + let _ = (fetchMediaData(context: context, userLocation: .other, mediaReference: media) |> deliverOnMainQueue).start(next: { (value, isImage) in - guard case let .data(data) = value, data.complete else { + guard case let .data(data) = value, data.isComplete else { return } @@ -209,32 +213,87 @@ public func legacyMediaEditor( legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true } - let emptyController = LegacyEmptyController(context: legacyController.context)! - emptyController.navigationBarShouldBeHidden = true - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - legacyController.bind(controller: navigationController) - - legacyController.enableSizeClassSignal = true - - present(legacyController, nil) - - TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: initialCaption, withItem: item, paint: mode == .draw, adjustments: mode == .adjustments, recipientName: recipientName, stickersContext: paintStickersContext, from: .zero, mainSnapshot: nil, snapshots: snapshots as [Any], immediate: transitionCompletion != nil, activateInput: mode == .caption, isGif: isGif, appeared: { + let schedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { media, done in + presentSchedulePicker(media, done) + } + let appeared: () -> Void = { transitionCompletion?() - }, completion: { result, editingContext in + } + let completion: (TGMediaEditableItem, TGMediaEditingContext, Bool, Int32) -> Void = { result, editingContext, silentPosting, scheduleTime in let nativeGenerator = legacyAssetPickerItemGenerator() var selectableResult: TGMediaSelectableItem? - if let result = result { - selectableResult = unsafeDowncast(result, to: TGMediaSelectableItem.self) - } + selectableResult = unsafeDowncast(result, to: TGMediaSelectableItem.self) + let signals = TGCameraController.resultSignals(for: nil, editingContext: editingContext, currentItem: selectableResult, storeAssets: false, saveEditedPhotos: false, descriptionGenerator: { _1, _2, _3 in nativeGenerator(_1, _2, _3, nil) }) - let isCaptionAbove = editingContext?.isCaptionAbove() ?? false - sendMessagesWithSignals(signals, false, 0, isCaptionAbove) - }, dismissed: { [weak legacyController] in + let isCaptionAbove = editingContext.isCaptionAbove() + sendMessagesWithSignals(signals, silentPosting, scheduleTime, isCaptionAbove) + } + let dismissed: () -> Void = { [weak legacyController] in legacyController?.dismiss() - }) + } + + legacyController.enableSizeClassSignal = true + + if isGif { + let galleryController = TGPhotoVideoEditor.controller( + with: legacyController.context, + caption: initialCaption, + withItem: item, + paint: mode == .draw, + adjustments: mode == .adjustments, + recipientName: recipientName, + stickersContext: paintStickersContext, + from: .zero, + mainSnapshot: nil, + snapshots: snapshots as [Any], + immediate: transitionCompletion != nil, + activateInput: mode == .caption, + isGif: true, + hasSilentPosting: hasSilentPosting, + hasSchedule: hasSchedule, + reminder: reminder, + presentSchedulePicker: schedulePicker, + appeared: appeared, + completion: completion, + dismissed: dismissed + ) + legacyController.bind(controller: galleryController) + present(legacyController, nil) + } else { + let emptyController = LegacyEmptyController(context: legacyController.context)! + emptyController.navigationBarShouldBeHidden = true + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + legacyController.bind(controller: navigationController) + + present(legacyController, nil) + + TGPhotoVideoEditor.present( + with: legacyController.context, + controller: emptyController, + caption: initialCaption, + withItem: item, + paint: mode == .draw, + adjustments: mode == .adjustments, + recipientName: recipientName, + stickersContext: paintStickersContext, + from: .zero, + mainSnapshot: nil, + snapshots: snapshots as [Any], + immediate: transitionCompletion != nil, + activateInput: mode == .caption, + isGif: false, + hasSilentPosting: hasSilentPosting, + hasSchedule: hasSchedule, + reminder: reminder, + presentSchedulePicker: schedulePicker, + appeared: appeared, + completion: completion, + dismissed: dismissed + ) + } }) } @@ -263,7 +322,7 @@ public func legacyAttachmentMenu( presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, - presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, + presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, @@ -372,8 +431,8 @@ public func legacyAttachmentMenu( carouselItem.hasSchedule = hasSchedule carouselItem.reminder = peer?.id == context.account.peerId carouselItem.presentScheduleController = { media, done in - presentSchedulePicker(media, { time in - done?(time) + presentSchedulePicker(media, { time, silentPosting in + done?(time, silentPosting) }) } carouselItem.presentTimerController = { done in @@ -487,9 +546,9 @@ public func legacyAttachmentMenu( let editCurrentItem = TGMenuSheetButtonItemView(title: title, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) - let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: editCurrentMedia) + let _ = (fetchMediaData(context: context, userLocation: .other, mediaReference: editCurrentMedia) |> deliverOnMainQueue).start(next: { (value, isImage) in - guard case let .data(data) = value, data.complete else { + guard case let .data(data) = value, data.isComplete else { return } @@ -530,13 +589,11 @@ public func legacyAttachmentMenu( present(legacyController, nil) - TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: initialCaption, withItem: item, paint: false, adjustments: false, recipientName: recipientName, stickersContext: paintStickersContext, from: .zero, mainSnapshot: nil, snapshots: [], immediate: false, activateInput: false, isGif: false, appeared: { - }, completion: { result, editingContext in + TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: initialCaption, withItem: item, paint: false, adjustments: false, recipientName: recipientName, stickersContext: paintStickersContext, from: .zero, mainSnapshot: nil, snapshots: [], immediate: false, activateInput: false, isGif: false, hasSilentPosting: false, hasSchedule: false, reminder: false, presentSchedulePicker: { _, _ in }, appeared: { + }, completion: { result, editingContext, _, _ in let nativeGenerator = legacyAssetPickerItemGenerator() - var selectableResult: TGMediaSelectableItem? - if let result = result { - selectableResult = unsafeDowncast(result, to: TGMediaSelectableItem.self) - } + let selectableResult: TGMediaSelectableItem? = unsafeDowncast(result, to: TGMediaSelectableItem.self) + let signals = TGCameraController.resultSignals(for: nil, editingContext: editingContext, currentItem: selectableResult, storeAssets: false, saveEditedPhotos: false, descriptionGenerator: { _1, _2, _3 in nativeGenerator(_1, _2, _3, nil) }) @@ -564,7 +621,7 @@ public func legacyAttachmentMenu( peerSupportsPolls = true } } - if let peer, peerSupportsPolls, canSendMessagesToPeer(peer) && canSendPolls { + if let peer, peerSupportsPolls, canSendMessagesToPeer(EnginePeer(peer)) && canSendPolls { let pollItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_Poll, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in controller?.dismiss(animated: true) openPoll() diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift index d367e03a9d..779e0cffee 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAvatarPicker.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import SwiftSignalKit -import Postbox import TelegramCore import LegacyComponents import TelegramPresentationData @@ -55,18 +54,18 @@ public func presentLegacyAvatarPicker(holder: Atomic, signup: Bool, t public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference, transitionView: UIView?, senderName: String? = nil, present: @escaping (ViewController, Any?) -> Void, imageCompletion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, URL, TGVideoEditAdjustments) -> Void) { let isVideo = !((media.media as? TelegramMediaImage)?.videoRepresentations.isEmpty ?? true) - let imageSignal = fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media, forceVideo: false) + let imageSignal = fetchMediaData(context: context, userLocation: .other, mediaReference: media, forceVideo: false) |> map { (value, _) -> (UIImage?, Bool) in - if case let .data(data) = value, data.complete { + if case let .data(data) = value, data.isComplete { return (UIImage(contentsOfFile: data.path), true) } else { return (nil, false) } } - let videoSignal = isVideo ? fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media, forceVideo: true) + let videoSignal = isVideo ? fetchMediaData(context: context, userLocation: .other, mediaReference: media, forceVideo: true) |> map { (value, isImage) -> (URL?, Bool) in - if case let .data(data) = value, data.complete && !isImage { + if case let .data(data) = value, data.isComplete && !isImage { return (URL(fileURLWithPath: data.path), true) } else { return (nil, false) @@ -108,11 +107,9 @@ public func legacyAvatarEditor(context: AccountContext, media: AnyMediaReference present(legacyController, nil) TGPhotoVideoEditor.present(with: legacyController.context, parentController: emptyController, image: image.0, video: video.0, stickersContext: paintStickersContext, transitionView: transitionView, senderName: senderName, didFinishWithImage: { image in - if let image = image { - imageCompletion(image) - } + imageCompletion(image) }, didFinishWithVideo: { image, url, adjustments in - if let image = image, let url = url, let adjustments = adjustments { + if let adjustments = adjustments { videoCompletion(image, url, adjustments) } }, dismissed: { [weak legacyController] in diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 17ab15a722..a4ebc8517e 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -20,7 +20,7 @@ public func guessMimeTypeByFileExtension(_ ext: String) -> String { return TGMimeTypeMap.mimeType(forExtension: ext) ?? "application/binary" } -public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: Peer, chatLocation: ChatLocation, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: NSAttributedString, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?) { +public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: Peer, chatLocation: ChatLocation, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: NSAttributedString, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?) { let paintStickersContext = LegacyPaintStickersContext(context: context) paintStickersContext.captionPanelView = { return getCaptionPanelView() @@ -43,8 +43,8 @@ public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, co controller.hasCoverButton = true } controller.presentScheduleController = { media, done in - presentSchedulePicker(media, { time in - done?(time) + presentSchedulePicker(media, { time, silentPosting in + done?(time, silentPosting) }) } controller.presentTimerController = { done in diff --git a/submodules/LegacyUI/BUILD b/submodules/LegacyUI/BUILD index 57bdeb0623..958e606004 100644 --- a/submodules/LegacyUI/BUILD +++ b/submodules/LegacyUI/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/SSignalKit/SSignalKit", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", diff --git a/submodules/LiveLocationManager/Sources/LiveLocationSummaryManager.swift b/submodules/LiveLocationManager/Sources/LiveLocationSummaryManager.swift index ce767dadd3..d5aff40e4f 100644 --- a/submodules/LiveLocationManager/Sources/LiveLocationSummaryManager.swift +++ b/submodules/LiveLocationManager/Sources/LiveLocationSummaryManager.swift @@ -167,7 +167,7 @@ private final class LiveLocationPeerSummaryContext { for message in messages { if let author = message.author { if author.id != strongSelf.accountPeerId && message.flags.contains(.Incoming) { - peersAndMessages.append((EnginePeer(author), EngineMessage(message))) + peersAndMessages.append((author, message)) } } } diff --git a/submodules/LocationUI/BUILD b/submodules/LocationUI/BUILD index eb9f0aee55..321fd7dd3a 100644 --- a/submodules/LocationUI/BUILD +++ b/submodules/LocationUI/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/LegacyComponents:LegacyComponents", @@ -49,6 +48,7 @@ swift_library( "//submodules/Components/BundleIconComponent", "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/GlassControls", "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/SearchInputPanelComponent", "//submodules/TelegramUI/Components/ButtonComponent", diff --git a/submodules/LocationUI/Sources/LocationInfoListItem.swift b/submodules/LocationUI/Sources/LocationInfoListItem.swift index fda4a00e51..032f1f621c 100644 --- a/submodules/LocationUI/Sources/LocationInfoListItem.swift +++ b/submodules/LocationUI/Sources/LocationInfoListItem.swift @@ -100,7 +100,7 @@ public final class LocationInfoListItemNode: ListViewItemNode { super.init(layerBacked: false, rotated: false, seeThrough: false) - self.addSubnode(self.backgroundNode) + //self.addSubnode(self.backgroundNode) self.addSubnode(self.buttonNode) self.addSubnode(self.venueIconNode) diff --git a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift index 34d88662f9..c3ef2e37d1 100644 --- a/submodules/LocationUI/Sources/LocationMapHeaderNode.swift +++ b/submodules/LocationUI/Sources/LocationMapHeaderNode.swift @@ -57,7 +57,7 @@ public final class LocationMapHeaderNode: ASDisplayNode { private let options: ComponentView? - private let optionsBackgroundView: GlassBackgroundView? + private let optionsBackgroundView: GlassContextExtractableContainer? private let optionsBackgroundNode: ASImageNode private let optionsSeparatorNode: ASDisplayNode private let optionsSecondSeparatorNode: ASDisplayNode @@ -66,7 +66,7 @@ public final class LocationMapHeaderNode: ASDisplayNode { private let notificationButtonNode: HighlightableButtonNode private let placesBackgroundView: GlassBackgroundView? private let placesBackgroundNode: ASImageNode - private let placesButtonNode: HighlightableButtonNode + private let placesButtonNode: HighlightTrackingButtonNode private let shadowNode: ASImageNode private var validLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGSize)? @@ -132,7 +132,7 @@ public final class LocationMapHeaderNode: ASDisplayNode { self.placesBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme) self.placesBackgroundNode.isUserInteractionEnabled = true - self.placesButtonNode = HighlightableButtonNode() + self.placesButtonNode = HighlightTrackingButtonNode() self.placesButtonNode.setTitle(presentationData.strings.Map_PlacesInThisArea, with: Font.medium(17.0), with: self.glass ? presentationData.theme.rootController.navigationBar.primaryTextColor : buttonColor, for: .normal) self.shadowNode = ASImageNode() @@ -142,7 +142,7 @@ public final class LocationMapHeaderNode: ASDisplayNode { self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false) if glass { - self.optionsBackgroundView = GlassBackgroundView() + self.optionsBackgroundView = GlassContextExtractableContainer() self.optionsBackgroundNode.image = nil self.placesBackgroundView = GlassBackgroundView() @@ -169,16 +169,16 @@ public final class LocationMapHeaderNode: ASDisplayNode { self.view.addSubview(optionsBackgroundView) } self.addSubnode(self.optionsBackgroundNode) - self.optionsBackgroundNode.addSubnode(self.optionsSeparatorNode) - self.optionsBackgroundNode.addSubnode(self.optionsSecondSeparatorNode) - self.optionsBackgroundNode.addSubnode(self.infoButtonNode) - self.optionsBackgroundNode.addSubnode(self.locationButtonNode) - self.optionsBackgroundNode.addSubnode(self.notificationButtonNode) + self.optionsBackgroundView?.contentView.addSubview(self.optionsSeparatorNode.view) + self.optionsBackgroundView?.contentView.addSubview(self.optionsSecondSeparatorNode.view) + self.optionsBackgroundView?.contentView.addSubview(self.infoButtonNode.view) + self.optionsBackgroundView?.contentView.addSubview(self.locationButtonNode.view) + self.optionsBackgroundView?.contentView.addSubview(self.notificationButtonNode.view) } } self.addSubnode(self.placesBackgroundNode) - self.placesBackgroundNode.addSubnode(self.placesButtonNode) + self.placesBackgroundView?.contentView.addSubview(self.placesButtonNode.view) //self.addSubnode(self.shadowNode) self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside) @@ -252,16 +252,16 @@ public final class LocationMapHeaderNode: ASDisplayNode { let inset: CGFloat = 6.0 let placesButtonSize = CGSize(width: 180.0 + panelInset * 2.0, height: 45.0 + panelInset * 2.0) - let placesButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - placesButtonSize.width) / 2.0), y: navigationBarHeight + topPadding - 6.0), size: placesButtonSize) + let placesButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - placesButtonSize.width) / 2.0), y: navigationBarHeight + topPadding - 6.0), size: placesButtonSize).insetBy(dx: 5.0, dy: 6.0) transition.updateFrame(node: self.placesBackgroundNode, frame: placesButtonFrame) if let placesBackgroundView = self.placesBackgroundView { - let backgroundViewFrame = CGRect(origin: .zero, size: placesButtonFrame.size).insetBy(dx: 5.0, dy: 6.0) + let backgroundViewFrame = CGRect(origin: .zero, size: placesButtonFrame.size) transition.updateFrame(view: placesBackgroundView, frame: backgroundViewFrame) - placesBackgroundView.update(size: backgroundViewFrame.size, cornerRadius: backgroundViewFrame.height * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: .immediate) + placesBackgroundView.update(size: backgroundViewFrame.size, cornerRadius: backgroundViewFrame.height * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: .immediate) } - transition.updateFrame(node: self.placesButtonNode, frame: CGRect(origin: CGPoint(), size: placesButtonSize)) + transition.updateFrame(node: self.placesButtonNode, frame: CGRect(origin: CGPoint(), size: placesButtonFrame.size)) transition.updateAlpha(node: self.placesBackgroundNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0) transition.updateAlpha(node: self.placesButtonNode, alpha: self.displayingPlacesButton ? 1.0 : 0.0) @@ -327,7 +327,7 @@ public final class LocationMapHeaderNode: ASDisplayNode { if let optionsBackgroundView = self.optionsBackgroundView { let backgroundViewFrame = backgroundFrame.insetBy(dx: 4.0, dy: 4.0) transition.updateFrame(view: optionsBackgroundView, frame: backgroundViewFrame) - optionsBackgroundView.update(size: backgroundViewFrame.size, cornerRadius: backgroundViewFrame.width * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: .immediate) + optionsBackgroundView.update(size: backgroundViewFrame.size, cornerRadius: backgroundViewFrame.width * 0.5, isDark: self.presentationData.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: .immediate) } let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) diff --git a/submodules/LocationUI/Sources/LocationViewController.swift b/submodules/LocationUI/Sources/LocationViewController.swift index 519c96a91d..637782af41 100644 --- a/submodules/LocationUI/Sources/LocationViewController.swift +++ b/submodules/LocationUI/Sources/LocationViewController.swift @@ -11,7 +11,6 @@ import AppBundle import CoreLocation import PresentationDataUtils import OpenInExternalAppUI -import ShareController import DeviceAccess import UndoUI import MapKit @@ -32,12 +31,6 @@ public class LocationViewParams { } } -enum LocationViewRightBarButton { - case none - case share - case showAll -} - class LocationViewInteraction { let toggleMapModeSelection: () -> Void let updateMapMode: (LocationMapMode) -> Void @@ -49,10 +42,9 @@ class LocationViewInteraction { let updateSendActionHighlight: (Bool) -> Void let sendLiveLocation: (Int32?, Bool, EngineMessage.Id?) -> Void let stopLiveLocation: () -> Void - let updateRightBarButton: (LocationViewRightBarButton) -> Void let present: (ViewController) -> Void - init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?, Bool, EngineMessage.Id?) -> Void, stopLiveLocation: @escaping () -> Void, updateRightBarButton: @escaping (LocationViewRightBarButton) -> Void, present: @escaping (ViewController) -> Void) { + init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, toggleTrackingMode: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping (TelegramMediaMap, String?, OpenInLocationDirections) -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (Bool, EngineMessage.Id?) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (Int32?, Bool, EngineMessage.Id?) -> Void, stopLiveLocation: @escaping () -> Void, present: @escaping (ViewController) -> Void) { self.toggleMapModeSelection = toggleMapModeSelection self.updateMapMode = updateMapMode self.toggleTrackingMode = toggleTrackingMode @@ -63,7 +55,6 @@ class LocationViewInteraction { self.updateSendActionHighlight = updateSendActionHighlight self.sendLiveLocation = sendLiveLocation self.stopLiveLocation = stopLiveLocation - self.updateRightBarButton = updateRightBarButton self.present = present } } @@ -83,9 +74,7 @@ public final class LocationViewController: ViewController { private let locationManager = LocationManager() private var interaction: LocationViewInteraction? - - private var rightBarButtonAction: LocationViewRightBarButton = .none - + public var dismissed: () -> Void = {} public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, subject: EngineMessage, isStoryLocation: Bool = false, isPreview: Bool = false, params: LocationViewParams) { @@ -97,33 +86,19 @@ public final class LocationViewController: ViewController { self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - let navigationBarPresentationData: NavigationBarPresentationData? - if !isPreview { - navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) - } else { - navigationBarPresentationData = nil - } + super.init(navigationBarPresentationData: nil) - super.init(navigationBarPresentationData: navigationBarPresentationData) + self._hasGlassStyle = true self.navigationPresentation = .modal - - if !self.isPreview { - self.title = self.presentationData.strings.Map_LocationTitle - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) - } - + self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) |> deliverOnMainQueue).start(next: { [weak self] presentationData in guard let strongSelf = self, strongSelf.presentationData.theme !== presentationData.theme else { return } strongSelf.presentationData = presentationData - - strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: strongSelf.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: strongSelf.presentationData.strings)), transition: .immediate) - - strongSelf.updateRightBarButton() - + if strongSelf.isNodeLoaded { strongSelf.controllerNode.updatePresentationData(presentationData) } @@ -207,7 +182,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) } @@ -265,17 +240,24 @@ public final class LocationViewController: ViewController { } strongSelf.controllerNode.setProximityIndicator(radius: 0) - let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.subject.id.peerId) + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.subject.id.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { [weak self] peer in guard let strongSelf = self else { return } - + var compactDisplayTitle: String? - if let peer = peer as? TelegramUser { - compactDisplayTitle = EnginePeer(peer).compactDisplayTitle + if case .user = peer { + compactDisplayTitle = peer.compactDisplayTitle } - + let controller = LocationDistancePickerScreen(context: context, style: .default, compactDisplayTitle: compactDisplayTitle, distances: strongSelf.controllerNode.headerNode.mapNode.distancesToAllAnnotations, updated: { [weak self] distance in guard let strongSelf = self else { return @@ -370,17 +352,24 @@ public final class LocationViewController: ViewController { params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 30 * 60, proximityNotificationRadius: distance)) }) - let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.subject.id.peerId) + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.subject.id.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { [weak self] peer in guard let strongSelf = self else { return } - + var compactDisplayTitle: String? - if let peer = peer as? TelegramUser { - compactDisplayTitle = EnginePeer(peer).compactDisplayTitle + if case .user = peer { + compactDisplayTitle = peer.compactDisplayTitle } - + var text: String let distanceString = shortStringForDistance(strings: strongSelf.presentationData.strings, distance: distance) if let compactDisplayTitle = compactDisplayTitle { @@ -407,7 +396,14 @@ public final class LocationViewController: ViewController { ) }) } else { - let _ = (context.account.postbox.loadedPeerWithId(subject.id.peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: subject.id.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { peer in let controller = ActionSheetController(presentationData: strongSelf.presentationData) var title: String @@ -415,8 +411,8 @@ public final class LocationViewController: ViewController { title = strongSelf.presentationData.strings.Map_LiveLocationExtendDescription } else { title = strongSelf.presentationData.strings.Map_LiveLocationGroupNewDescription - if let user = peer as? TelegramUser { - title = strongSelf.presentationData.strings.Map_LiveLocationPrivateNewDescription(EnginePeer(user).compactDisplayTitle).string + if case .user = peer { + title = strongSelf.presentationData.strings.Map_LiveLocationPrivateNewDescription(peer.compactDisplayTitle).string } } @@ -466,15 +462,6 @@ public final class LocationViewController: ViewController { }, stopLiveLocation: { [weak self] in params.stopLiveLocation(nil) self?.dismiss() - }, updateRightBarButton: { [weak self] action in - guard let strongSelf = self else { - return - } - - if action != strongSelf.rightBarButtonAction { - strongSelf.rightBarButtonAction = action - strongSelf.updateRightBarButton() - } }, present: { [weak self] c in if let strongSelf = self { strongSelf.present(c, in: .window(.root)) @@ -515,7 +502,7 @@ public final class LocationViewController: ViewController { return } - self.displayNode = LocationViewControllerNode(context: self.context, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager, isStoryLocation: self.isStoryLocation, isPreview: self.isPreview) + self.displayNode = LocationViewControllerNode(context: self.context, controller: self, presentationData: self.presentationData, subject: self.subject, interaction: interaction, locationManager: self.locationManager, isStoryLocation: self.isStoryLocation, isPreview: self.isPreview) self.displayNodeDidLoad() self.controllerNode.onAnnotationsReady = { [weak self] in @@ -528,39 +515,12 @@ public final class LocationViewController: ViewController { self.controllerNode.headerNode.mapNode.disableHorizontalTransitionGesture = self.isStoryLocation } - private func updateRightBarButton() { - guard !self.isPreview else { - return - } - switch self.rightBarButtonAction { - case .none: - self.navigationItem.rightBarButtonItem = nil - case .share: - self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.sharePressed)) - self.navigationItem.rightBarButtonItem?.accessibilityLabel = self.presentationData.strings.VoiceOver_MessageContextShare - case .showAll: - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Map_LiveLocationShowAll, style: .plain, target: self, action: #selector(self.showAllPressed)) - } - } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } - - @objc private func cancelPressed() { - self.dismiss() - } - - @objc private func sharePressed() { - self.interaction?.share() - } - - @objc private func showAllPressed() { - self.controllerNode.showAll() - } - + private var didDismiss = false public override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index f8c143d244..5f8ec123c2 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -17,6 +17,11 @@ import CoreLocation import Geocoding import DeviceAccess import TooltipUI +import ComponentFlow +import GlassControls +import BundleIconComponent +import EdgeEffect +import MultilineTextComponent func getLocation(from message: EngineMessage) -> TelegramMediaMap? { if let poll = message.media.first(where: { $0 is TelegramMediaPoll } ) as? TelegramMediaPoll, let map = poll.attachedMedia as? TelegramMediaMap { @@ -212,6 +217,12 @@ private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntr return LocationViewTransaction(deletions: deletions, insertions: insertions, updates: updates, gotTravelTimes: gotTravelTimes, count: toEntries.count, animated: animated) } +enum LocationViewRightBarButton { + case none + case share + case showAll +} + public enum LocationViewLocation: Equatable { case initial case user @@ -268,6 +279,7 @@ public struct LocationViewState { final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationManagerDelegate { private let context: AccountContext + private weak var controller: LocationViewController? private var presentationData: PresentationData private let presentationDataPromise: Promise private var subject: EngineMessage @@ -276,9 +288,14 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan private let isStoryLocation: Bool private let isPreview: Bool + private var rightBarButtonAction: LocationViewRightBarButton = .none + + private let topEdgeEffectView = EdgeEffectView() + private let buttons = ComponentView() + private let title = ComponentView() + private let listNode: ListView let headerNode: LocationMapHeaderNode - private let optionsNode: LocationOptionsNode private var enqueuedTransitions: [LocationViewTransaction] = [] @@ -302,8 +319,9 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } private let travelTimesPromise = Promise<[EngineMessage.Id: (Double, ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime)]>([:]) - init(context: AccountContext, presentationData: PresentationData, subject: EngineMessage, interaction: LocationViewInteraction, locationManager: LocationManager, isStoryLocation: Bool, isPreview: Bool) { + init(context: AccountContext, controller: LocationViewController, presentationData: PresentationData, subject: EngineMessage, interaction: LocationViewInteraction, locationManager: LocationManager, isStoryLocation: Bool, isPreview: Bool) { self.context = context + self.controller = controller self.presentationData = presentationData self.presentationDataPromise = Promise(presentationData) self.subject = subject @@ -316,7 +334,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.statePromise = Promise(self.state) self.listNode = ListViewImpl() - self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.listNode.backgroundColor = .clear //self.presentationData.theme.list.plainBackgroundColor self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3) self.listNode.verticalScrollIndicatorFollowsOverscroll = true self.listNode.accessibilityPageScrolledString = { row, count in @@ -326,18 +344,16 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan var setupProximityNotificationImpl: ((Bool) -> Void)? self.headerNode = LocationMapHeaderNode( presentationData: presentationData, - glass: false, - isPreview: true, + glass: true, + isPreview: self.isPreview, toggleMapModeSelection: interaction.toggleMapModeSelection, updateMapMode: interaction.updateMapMode, goToUserLocation: interaction.toggleTrackingMode, setupProximityNotification: { reset in - setupProximityNotificationImpl?(reset) - }) - //self.headerNode.mapNode.isRotateEnabled = false - - self.optionsNode = LocationOptionsNode(presentationData: presentationData, updateMapMode: interaction.updateMapMode) - + setupProximityNotificationImpl?(reset) + } + ) + super.init() self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor @@ -346,9 +362,6 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.addSubnode(self.listNode) } self.addSubnode(self.headerNode) - if !self.isPreview { - self.addSubnode(self.optionsNode) - } let userLocation: Signal = .single(nil) |> then( @@ -391,7 +404,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let liveLocations = context.engine.messages.topPeerActiveLiveLocationMessages(peerId: subject.id.peerId) |> map { _, messages -> [EngineMessage] in - return messages.map(EngineMessage.init) + return messages } setupProximityNotificationImpl = { reset in @@ -643,7 +656,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let transition = preparedTransition(from: previousEntries ?? [], to: entries, context: context, presentationData: presentationData, interaction: strongSelf.interaction, gotTravelTimes: !travelTimes.isEmpty && !previousHadTravelTimes, animated: animated) strongSelf.enqueueTransition(transition) - strongSelf.headerNode.updateState(mapMode: state.mapMode, trackingMode: state.trackingMode, displayingMapModeOptions: state.displayingMapModeOptions, displayingPlacesButton: false, proximityNotification: proximityNotification, animated: false) + strongSelf.headerNode.updateState(mapMode: state.mapMode, trackingMode: state.trackingMode, displayingMapModeOptions: state.displayingMapModeOptions, displayingPlacesButton: false, proximityNotification: proximityNotification, animated: true) if let proximityNotification = proximityNotification, !proximityNotification && !strongSelf.displayedProximityAlertTooltip { strongSelf.displayedProximityAlertTooltip = true @@ -707,7 +720,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } else { rightBarButtonAction = .share } - strongSelf.interaction.updateRightBarButton(rightBarButtonAction) + strongSelf.rightBarButtonAction = rightBarButtonAction if let (layout, navigationBarHeight) = strongSelf.validLayout { var updateLayout = false @@ -801,9 +814,8 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan self.presentationDataPromise.set(.single(presentationData)) self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.listNode.backgroundColor = .clear // self.presentationData.theme.list.plainBackgroundColor self.headerNode.updatePresentationData(self.presentationData) - self.optionsNode.updatePresentationData(self.presentationData) } func updateState(_ f: (LocationViewState) -> LocationViewState) { @@ -922,15 +934,22 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan return } - let _ = (self.context.account.postbox.loadedPeerWithId(self.subject.id.peerId) + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.subject.id.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { [weak self] peer in guard let strongSelf = self else { return } - + var text: String = strongSelf.presentationData.strings.Location_ProximityGroupTip if peer.id.namespace == Namespaces.Peer.CloudUser { - text = strongSelf.presentationData.strings.Location_ProximityTip(EnginePeer(peer).compactDisplayTitle).string + text = strongSelf.presentationData.strings.Location_ProximityTip(peer.compactDisplayTitle).string } strongSelf.interaction.present(TooltipScreen(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, text: .plain(text: text), icon: nil, location: .point(location.offsetBy(dx: -9.0, dy: 0.0), .right), displayDuration: .custom(3.0), shouldDismissOnTouch: { _, _ in @@ -970,10 +989,10 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } else { headerHeight = topInset + overlap } - let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: headerHeight)) + let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)) transition.updateFrame(node: self.headerNode, frame: headerFrame) - self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, controlsTopPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, controlsBottomPadding: 0.0, offset: 0.0, size: headerFrame.size, transition: transition) + self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, controlsTopPadding: 0.0, controlsBottomPadding: 0.0, offset: 0.0, size: CGSize(width: headerFrame.width, height: headerHeight), transition: transition) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) @@ -982,18 +1001,118 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan let listFrame: CGRect = CGRect(origin: CGPoint(), size: layout.size) transition.updateFrame(node: self.listNode, frame: listFrame) + + if !self.isPreview { + let topEdgeEffectFrame = CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: 80.0)) + transition.updateFrame(view: self.topEdgeEffectView, frame: topEdgeEffectFrame) + self.topEdgeEffectView.update(content: self.headerNode.mapNode.mapMode == .map ? self.presentationData.theme.list.plainBackgroundColor : .clear, blur: true, alpha: 0.65, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) + if self.topEdgeEffectView.superview == nil { + self.view.addSubview(self.topEdgeEffectView) + } + + let leftControlItems: [GlassControlGroupComponent.Item] = [ + GlassControlGroupComponent.Item( + id: AnyHashable("close"), + content: .icon("Navigation/Close"), + action: { [weak self] in + guard let self else { + return + } + self.controller?.dismiss() + } + ) + ] + var rightControlItems: [GlassControlGroupComponent.Item] = [] + switch self.rightBarButtonAction { + case .none: + break + case .share: + rightControlItems.append( + GlassControlGroupComponent.Item( + id: AnyHashable("share"), + content: .icon("Navigation/Share"), + action: { [weak self] in + guard let self else { + return + } + self.interaction.share() + } + ) + ) + case .showAll: + rightControlItems.append( + GlassControlGroupComponent.Item( + id: AnyHashable("share"), + content: .text(self.presentationData.strings.Map_LiveLocationShowAll), + action: { [weak self] in + guard let self else { + return + } + self.showAll() + } + ) + ) + } + + let barButtonSideInset: CGFloat = 16.0 + let buttonsSize = self.buttons.update( + transition: ComponentTransition(transition), + component: AnyComponent(GlassControlPanelComponent( + theme: self.presentationData.theme, + leftItem: GlassControlPanelComponent.Item( + items: leftControlItems, + background: .panel + ), + centralItem: nil, + rightItem: rightControlItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: rightControlItems, + background: .panel + ), + centerAlignmentIfPossible: true, + isDark: self.presentationData.theme.overallDarkAppearance + )), + environment: {}, + containerSize: CGSize(width: layout.size.width - barButtonSideInset * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: 44.0) + ) + let buttonsFrame = CGRect(origin: CGPoint(x: barButtonSideInset + layout.safeInsets.left, y: barButtonSideInset), size: buttonsSize) + if let view = self.buttons.view { + if view.superview == nil { + self.view.addSubview(view) + } + view.bounds = CGRect(origin: .zero, size: buttonsFrame.size) + view.center = buttonsFrame.center + } + + let titleSize = self.title.update( + transition: ComponentTransition(transition), + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: self.presentationData.strings.Map_LocationTitle, + font: Font.semibold(17.0), + textColor: self.headerNode.mapNode.mapMode == .map ? self.presentationData.theme.rootController.navigationBar.primaryTextColor : .white + ) + ) + ) + ), + environment: {}, + containerSize: CGSize(width: 200.0, height: 40.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - titleSize.width) / 2.0), y: floorToScreenPixels((navigationHeight - titleSize.height) / 2.0) + 3.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.view.addSubview(titleView) + } + transition.updateFrame(view: titleView, frame: titleFrame) + } + } if isFirstLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } } - - let optionsOffset: CGFloat = self.state.displayingMapModeOptions ? navigationHeight : navigationHeight - optionsHeight - let optionsFrame = CGRect(x: 0.0, y: optionsOffset, width: layout.size.width, height: optionsHeight) - transition.updateFrame(node: self.optionsNode, frame: optionsFrame) - self.optionsNode.updateLayout(size: optionsFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) - self.optionsNode.isUserInteractionEnabled = self.state.displayingMapModeOptions } var coordinate: Signal { diff --git a/submodules/MapResourceToAvatarSizes/BUILD b/submodules/MapResourceToAvatarSizes/BUILD index 0db6335885..48fecbac65 100644 --- a/submodules/MapResourceToAvatarSizes/BUILD +++ b/submodules/MapResourceToAvatarSizes/BUILD @@ -11,7 +11,6 @@ swift_library( ], deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/Display:Display", ], diff --git a/submodules/MapResourceToAvatarSizes/Sources/MapResourceToAvatarSizes.swift b/submodules/MapResourceToAvatarSizes/Sources/MapResourceToAvatarSizes.swift index 440581e686..5ff1f0b855 100644 --- a/submodules/MapResourceToAvatarSizes/Sources/MapResourceToAvatarSizes.swift +++ b/submodules/MapResourceToAvatarSizes/Sources/MapResourceToAvatarSizes.swift @@ -1,15 +1,14 @@ import Foundation import UIKit import SwiftSignalKit -import Postbox import TelegramCore import Display -public func mapResourceToAvatarSizes(postbox: Postbox, resource: MediaResource, representations: [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError> { - return postbox.mediaBox.resourceData(resource) +public func mapResourceToAvatarSizes(engine: TelegramEngine, resource: EngineMediaResource, representations: [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError> { + return engine.resources.data(id: resource.id) |> take(1) |> map { data -> [Int: Data] in - guard data.complete, let image = UIImage(contentsOfFile: data.path) else { + guard data.isComplete, let image = UIImage(contentsOfFile: data.path) else { return [:] } var result: [Int: Data] = [:] diff --git a/submodules/MediaPasteboardUI/BUILD b/submodules/MediaPasteboardUI/BUILD index ced1985f41..e68615cb1a 100644 --- a/submodules/MediaPasteboardUI/BUILD +++ b/submodules/MediaPasteboardUI/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AccountContext:AccountContext", diff --git a/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift b/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift index 71f95913ff..027d14ff00 100644 --- a/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift +++ b/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift @@ -8,7 +8,6 @@ import AttachmentUI import MediaPickerUI import AccountContext import LegacyComponents -import AttachmentTextInputPanelNode public func mediaPasteboardScreen( context: AccountContext, @@ -17,9 +16,17 @@ public func mediaPasteboardScreen( subjects: [MediaPickerScreenImpl.Subject.Media], presentMediaPicker: @escaping (_ subject: MediaPickerScreenImpl.Subject, _ saveEditedPhotos: Bool, _ bannedSendPhotos: (Int32, Bool)?, _ bannedSendVideos: (Int32, Bool)?, _ present: @escaping (MediaPickerScreenImpl, AttachmentMediaPickerContext?) -> Void) -> Void, getSourceRect: (() -> CGRect?)? = nil, - makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil } + customEmojiAvailable: Bool = true ) -> ViewController { - let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: .peer(id: peer.id), buttons: [.standalone], initialButton: .standalone, makeEntityInputView: makeEntityInputView) + let controller = AttachmentController( + context: context, + updatedPresentationData: updatedPresentationData, + style: .glass, + chatLocation: .peer(id: peer.id), + buttons: [.standalone], + initialButton: .standalone, + customEmojiAvailable: customEmojiAvailable + ) controller.requestController = { _, present in presentMediaPicker(.media(subjects), false, nil, nil, { mediaPicker, mediaPickerContext in present(mediaPicker, mediaPickerContext) diff --git a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index 01ecb788f3..a2da740107 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -131,7 +131,7 @@ func presentLegacyMediaPickerGallery( transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?, @escaping () -> Void) -> Void, - presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, + presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void, @@ -369,8 +369,8 @@ func presentLegacyMediaPickerGallery( }) } sheetController.schedule = { - presentSchedulePicker(true, { time in - completed(item.asset, false, time, { + presentSchedulePicker(true, { time, silentPosting in + completed(item.asset, silentPosting, time, { dismissImpl() }) }) diff --git a/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift b/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift index ebb74d6763..79dbd0c8c9 100644 --- a/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaGroupsScreen.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import AccountContext diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 6de88cee9e..8e176c8f43 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -240,8 +240,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att private let cancelButtonNode: WebAppCancelButtonNode private var buttons: ComponentView? - private var cancelButton: ComponentView? - private var rightButton: ComponentView? private let moreButtonPlayOnce = ActionSlot() private let moreButtonNode: MoreButtonNode @@ -250,7 +248,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att public weak var webSearchController: WebSearchController? public var openCamera: ((Any?) -> Void)? - public var presentSchedulePicker: (Bool, @escaping (Int32) -> Void) -> Void = { _, _ in } + public var presentSchedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { _, _ in } public var presentTimerPicker: (@escaping (Int32) -> Void) -> Void = { _ in } public var presentWebSearch: (MediaGroupsScreen, Bool) -> Void = { _, _ in } public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } @@ -441,7 +439,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 @@ -2104,7 +2102,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } if case .glass = style { - self.cancelButton = ComponentView() self.buttons = ComponentView() } self.cancelButtonNode = WebAppCancelButtonNode(theme: self.presentationData.theme, strings: self.presentationData.strings) @@ -2301,8 +2298,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } }, schedule: { [weak self] parameters in if let strongSelf = self { - strongSelf.presentSchedulePicker(false, { [weak self] time in - self?.interaction?.sendSelected(nil, false, time, true, parameters, {}) + strongSelf.presentSchedulePicker(false, { [weak self] time, silentPosting in + self?.interaction?.sendSelected(nil, silentPosting, time, true, parameters, {}) }) } }, dismissInput: { [weak self] in @@ -3923,21 +3920,35 @@ public func avatarMediaPickerController( final class PickerDelegate: NSObject, PHPickerViewControllerDelegate { var completion: ((Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void)? - func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - picker.dismiss(animated: true) - - for item in results { - if item.itemProvider.canLoadObject(ofClass: UIImage.self) { - item.itemProvider.loadObject(ofClass: UIImage.self) { image, error in - if let uiImage = image as? UIImage { - Queue.mainQueue().async { - self.completion?(uiImage, nil, CGRect(), nil, false, { _ in return nil }, {}) - } + private func resolveResult(_ result: PHPickerResult) { + if let assetIdentifier = result.assetIdentifier { + let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [assetIdentifier], options: nil) + if let asset = fetchResult.firstObject { + Queue.mainQueue().async { + self.completion?(asset, nil, CGRect(), nil, false, { _ in return nil }, {}) + } + return + } + } + + if result.itemProvider.canLoadObject(ofClass: UIImage.self) { + result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, _ in + if let uiImage = image as? UIImage { + Queue.mainQueue().async { + self?.completion?(uiImage, nil, CGRect(), nil, false, { _ in return nil }, {}) } } } } } + + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + picker.dismiss(animated: true) + + if let result = results.first { + self.resolveResult(result) + } + } } let holder = PickerDelegate() @@ -3945,7 +3956,7 @@ public func avatarMediaPickerController( let openMediaPicker = { var configuration = PHPickerConfiguration(photoLibrary: .shared()) - configuration.filter = .images + configuration.filter = .any(of: [.images, .videos]) configuration.selectionLimit = 1 let picker = PHPickerViewController(configuration: configuration) diff --git a/submodules/MediaPlayer/Sources/ChunkMediaPlayerDirectFetchSourceImpl.swift b/submodules/MediaPlayer/Sources/ChunkMediaPlayerDirectFetchSourceImpl.swift index 74fce2874b..1a5c25b737 100644 --- a/submodules/MediaPlayer/Sources/ChunkMediaPlayerDirectFetchSourceImpl.swift +++ b/submodules/MediaPlayer/Sources/ChunkMediaPlayerDirectFetchSourceImpl.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import SwiftSignalKit -import Postbox import TelegramCore import FFMpegBinding import RangeSet diff --git a/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift index 16e9619170..67a184a3b9 100644 --- a/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift @@ -7,7 +7,7 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder { private let swrContext: FFMpegSWResample private var timescale: CMTimeScale = 44000 - private let audioFrame: FFMpegAVFrame + private let audioFrame: FFMpegAVFrame? private var resetDecoderOnNextFrame = true private let formatDescription: CMAudioFormatDescription @@ -43,11 +43,15 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder { } func decodeRaw(frame: MediaTrackDecodableFrame) -> Data? { + guard let audioFrame = self.audioFrame else { + return nil + } + let status = frame.packet.send(toDecoder: self.codecContext) if status == 0 { - let result = self.codecContext.receive(into: self.audioFrame) + let result = self.codecContext.receive(into: audioFrame) if case .success = result { - guard let data = self.swrContext.resample(self.audioFrame) else { + guard let data = self.swrContext.resample(audioFrame) else { return nil } @@ -67,10 +71,14 @@ final class FFMpegAudioFrameDecoder: MediaTrackFrameDecoder { } func decode() -> MediaTrackFrame? { + guard let audioFrame = self.audioFrame else { + return nil + } + while true { - let result = self.codecContext.receive(into: self.audioFrame) + let result = self.codecContext.receive(into: audioFrame) if case .success = result { - if let convertedFrame = convertAudioFrame(self.audioFrame) { + if let convertedFrame = convertAudioFrame(audioFrame) { self.delayedFrames.append(convertedFrame) } } else { diff --git a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift index 598916b345..efad3a182f 100644 --- a/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift +++ b/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift @@ -47,7 +47,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { private let codecContext: FFMpegAVCodecContext - private let videoFrame: FFMpegAVFrame + private let videoFrame: FFMpegAVFrame? private var resetDecoderOnNextFrame = true private var isError = false @@ -91,27 +91,29 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { } public func receiveFromDecoder(ptsOffset: CMTime?, displayImmediately: Bool = true) -> ReceiveResult { + guard let videoFrame = self.videoFrame else { + return .error + } if self.isError { return .error } - guard let defaultTimescale = self.defaultTimescale, let defaultDuration = self.defaultDuration else { return .error } - let receiveResult = self.codecContext.receive(into: self.videoFrame) + let receiveResult = self.codecContext.receive(into: videoFrame) switch receiveResult { case .success: - if self.videoFrame.width * self.videoFrame.height > 4 * 1024 * 4 * 1024 { + if videoFrame.width * videoFrame.height > 4 * 1024 * 4 * 1024 { self.isError = true return .error } - var pts = CMTimeMake(value: self.videoFrame.pts, timescale: defaultTimescale) + var pts = CMTimeMake(value: videoFrame.pts, timescale: defaultTimescale) if let ptsOffset = ptsOffset { pts = CMTimeAdd(pts, ptsOffset) } - if let convertedFrame = convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: self.videoFrame.duration > 0 ? CMTimeMake(value: self.videoFrame.duration, timescale: defaultTimescale) : defaultDuration, displayImmediately: displayImmediately) { + if let convertedFrame = convertVideoFrame(videoFrame, pts: pts, dts: pts, duration: videoFrame.duration > 0 ? CMTimeMake(value: videoFrame.duration, timescale: defaultTimescale) : defaultDuration, displayImmediately: displayImmediately) { return .result(convertedFrame) } else { return .error @@ -137,6 +139,9 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { } public func decode(ptsOffset: CMTime?, forceARGB: Bool = false, unpremultiplyAlpha: Bool = true, displayImmediately: Bool = true) -> MediaTrackFrame? { + guard let videoFrame = self.videoFrame else { + return nil + } if self.isError { return nil } @@ -144,23 +149,26 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { return nil } - if self.codecContext.receive(into: self.videoFrame) == .success { - if self.videoFrame.width * self.videoFrame.height > 4 * 1024 * 4 * 1024 { + if self.codecContext.receive(into: videoFrame) == .success { + if videoFrame.width * videoFrame.height > 4 * 1024 * 4 * 1024 { self.isError = true return nil } - var pts = CMTimeMake(value: self.videoFrame.pts, timescale: defaultTimescale) + var pts = CMTimeMake(value: videoFrame.pts, timescale: defaultTimescale) if let ptsOffset = ptsOffset { pts = CMTimeAdd(pts, ptsOffset) } - return convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: defaultDuration, forceARGB: forceARGB, unpremultiplyAlpha: unpremultiplyAlpha, displayImmediately: displayImmediately) + return convertVideoFrame(videoFrame, pts: pts, dts: pts, duration: defaultDuration, forceARGB: forceARGB, unpremultiplyAlpha: unpremultiplyAlpha, displayImmediately: displayImmediately) } return nil } public func receiveRemainingFrames(ptsOffset: CMTime?) -> [MediaTrackFrame] { + guard let videoFrame = self.videoFrame else { + return [] + } guard let defaultTimescale = self.defaultTimescale, let defaultDuration = self.defaultDuration else { return [] } @@ -173,17 +181,17 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { self.delayedFrames.removeAll() while true { - if case .success = self.codecContext.receive(into: self.videoFrame) { - if self.videoFrame.width * self.videoFrame.height > 4 * 1024 * 4 * 1024 { + if case .success = self.codecContext.receive(into: videoFrame) { + if videoFrame.width * videoFrame.height > 4 * 1024 * 4 * 1024 { self.isError = true return [] } - var pts = CMTimeMake(value: self.videoFrame.pts, timescale: defaultTimescale) + var pts = CMTimeMake(value: videoFrame.pts, timescale: defaultTimescale) if let ptsOffset = ptsOffset { pts = CMTimeAdd(pts, ptsOffset) } - if let convertedFrame = convertVideoFrame(self.videoFrame, pts: pts, dts: pts, duration: self.videoFrame.duration > 0 ? CMTimeMake(value: self.videoFrame.duration, timescale: defaultTimescale) : defaultDuration) { + if let convertedFrame = convertVideoFrame(videoFrame, pts: pts, dts: pts, duration: videoFrame.duration > 0 ? CMTimeMake(value: videoFrame.duration, timescale: defaultTimescale) : defaultDuration) { result.append(convertedFrame) } } else { @@ -194,15 +202,18 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { } public func render(frame: MediaTrackDecodableFrame) -> UIImage? { + guard let videoFrame = self.videoFrame else { + return nil + } let status = frame.packet.send(toDecoder: self.codecContext) if status == 0 { - if case .success = self.codecContext.receive(into: self.videoFrame) { - if self.videoFrame.width * self.videoFrame.height > 4 * 1024 * 4 * 1024 { + if case .success = self.codecContext.receive(into: videoFrame) { + if videoFrame.width * videoFrame.height > 4 * 1024 * 4 * 1024 { self.isError = true return nil } - return convertVideoFrameToImage(self.videoFrame) + return convertVideoFrameToImage(videoFrame) } } @@ -229,7 +240,39 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { } } + private static func isPlanarYUV420Safe(_ frame: FFMpegAVFrame) -> Bool { + // Layout must be a planar YUV/YUVA 420 variant. Anything else (BGRA, NV12, gray8, + // YUV422P, …) must not reach the planar 420 vImage conversion or the planar copy + // path that force-unwraps data[1]/data[2]. + let format = frame.pixelFormat + guard format == .YUV || format == .YUVA else { + return false + } + // Reject bottom-up / negative-stride frames before any row-copy loop — ffmpeg + // rawdec flips linesize[0] for codec_tag WRAW/cyuv/BottomUp, and other decoders + // (dxtory, mimic, mjpegdec, scpr) flip the chroma planes too. + if frame.lineSize[0] < 0 || frame.lineSize[1] < 0 || frame.lineSize[2] < 0 { + return false + } + if format == .YUVA && frame.lineSize[3] < 0 { + return false + } + if frame.data[0] == nil || frame.data[1] == nil || frame.data[2] == nil { + return false + } + if format == .YUVA && frame.data[3] == nil { + return false + } + if frame.width <= 0 || frame.height <= 0 { + return false + } + return true + } + private func convertVideoFrameToImage(_ frame: FFMpegAVFrame) -> UIImage? { + guard FFMpegMediaVideoFrameDecoder.isPlanarYUV420Safe(frame) else { + return nil + } var info = vImage_YpCbCrToARGB() var pixelRange: vImage_YpCbCrPixelRange @@ -314,7 +357,7 @@ public final class FFMpegMediaVideoFrameDecoder: MediaTrackFrameDecoder { return MediaTrackFrame(type: .video, sampleBuffer: sampleBuffer!, resetDecoder: resetDecoder, decoded: true) } - if frame.data[0] == nil { + guard FFMpegMediaVideoFrameDecoder.isPlanarYUV420Safe(frame) else { return nil } if frame.lineSize[1] != frame.lineSize[2] { diff --git a/submodules/MediaPlayer/Sources/MediaDataReader.swift b/submodules/MediaPlayer/Sources/MediaDataReader.swift index b07c6ec251..89717fc27e 100644 --- a/submodules/MediaPlayer/Sources/MediaDataReader.swift +++ b/submodules/MediaPlayer/Sources/MediaDataReader.swift @@ -3,7 +3,6 @@ import AVFoundation import CoreMedia import FFMpegBinding import VideoToolbox -import Postbox #if os(macOS) public let internal_isHardwareAv1Supported: Bool = { diff --git a/submodules/MimeTypes/Sources/TGMimeTypeMap.m b/submodules/MimeTypes/Sources/TGMimeTypeMap.m index 89cd7548ef..894c12aa66 100644 --- a/submodules/MimeTypes/Sources/TGMimeTypeMap.m +++ b/submodules/MimeTypes/Sources/TGMimeTypeMap.m @@ -95,7 +95,7 @@ static void initializeMapping() mimeToExtension[@"application/x-dms"] = @"dms"; extensionToMime[@"dms"] = @"application/x-dms"; mimeToExtension[@"application/x-doom"] = @"wad"; extensionToMime[@"wad"] = @"application/x-doom"; mimeToExtension[@"application/x-dvi"] = @"dvi"; extensionToMime[@"dvi"] = @"application/x-dvi"; - mimeToExtension[@"application/x-flac"] = @"flac"; extensionToMime[@"flac"] = @"application/x-flac"; + mimeToExtension[@"audio/flac"] = @"flac"; extensionToMime[@"flac"] = @"audio/flac"; mimeToExtension[@"application/x-font"] = @"pfa"; extensionToMime[@"pfa"] = @"application/x-font"; mimeToExtension[@"application/x-font"] = @"pfb"; extensionToMime[@"pfb"] = @"application/x-font"; mimeToExtension[@"application/x-font"] = @"gsf"; extensionToMime[@"gsf"] = @"application/x-font"; @@ -250,6 +250,7 @@ static void initializeMapping() mimeToExtension[@"text/plain"] = @"text"; extensionToMime[@"text"] = @"text/plain"; mimeToExtension[@"text/plain"] = @"diff"; extensionToMime[@"diff"] = @"text/plain"; mimeToExtension[@"text/plain"] = @"po"; extensionToMime[@"po"] = @"text/plain"; // reserve "pot" for vnd.ms-powerpoint + mimeToExtension[@"text/markdown"] = @"md"; extensionToMime[@"md"] = @"text/markdown"; mimeToExtension[@"text/richtext"] = @"rtx"; extensionToMime[@"rtx"] = @"text/richtext"; mimeToExtension[@"text/rtf"] = @"rtf"; extensionToMime[@"rtf"] = @"text/rtf"; mimeToExtension[@"text/texmacs"] = @"ts"; extensionToMime[@"ts"] = @"text/texmacs"; diff --git a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTInputStream.h b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTInputStream.h index bc0bf67327..d8d8f35e7a 100644 --- a/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTInputStream.h +++ b/submodules/MtProtoKit/PublicHeaders/MtProtoKit/MTInputStream.h @@ -7,19 +7,12 @@ - (instancetype)initWithData:(NSData *)data; - (NSInputStream *)wrappedInputStream; -- (int32_t)readInt32; - (int32_t)readInt32:(bool *)failed __attribute__((nonnull(1))); -- (int64_t)readInt64; - (int64_t)readInt64:(bool *)failed __attribute__((nonnull(1))); -- (double)readDouble; - (double)readDouble:(bool *)failed __attribute__((nonnull(1))); -- (NSData *)readData:(int)length; - (NSData *)readData:(int)length failed:(bool *)failed __attribute__((nonnull(2))); -- (NSMutableData *)readMutableData:(NSUInteger)length; - (NSMutableData *)readMutableData:(NSUInteger)length failed:(bool *)failed __attribute__((nonnull(2))); -- (NSString *)readString; - (NSString *)readString:(bool *)failed __attribute__((nonnull(1))); -- (NSData *)readBytes; - (NSData *)readBytes:(bool *)failed __attribute__((nonnull(1))); @end diff --git a/submodules/MtProtoKit/Sources/MTApiEnvironment.m b/submodules/MtProtoKit/Sources/MTApiEnvironment.m index e42c2bc549..92b6ab69cf 100644 --- a/submodules/MtProtoKit/Sources/MTApiEnvironment.m +++ b/submodules/MtProtoKit/Sources/MTApiEnvironment.m @@ -264,14 +264,11 @@ static NSData *base64_decode(NSString *str) { - (NSString * _Nonnull)serializeToString { NSData *data = [self serialize]; - if ([data respondsToSelector:@selector(base64EncodedDataWithOptions:)]) { - return [[data base64EncodedStringWithOptions:kNilOptions] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"="]]; - } else { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - return [self.serialize base64Encoding]; -#pragma clang diagnostic pop - } + NSString *result = [data base64EncodedStringWithOptions:kNilOptions]; + result = [result stringByReplacingOccurrencesOfString:@"+" withString:@"-"]; + result = [result stringByReplacingOccurrencesOfString:@"/" withString:@"_"]; + result = [result stringByReplacingOccurrencesOfString:@"=" withString:@""]; + return result; } - (BOOL)isEqual:(id)object { diff --git a/submodules/MtProtoKit/Sources/MTBackupAddressSignals.m b/submodules/MtProtoKit/Sources/MTBackupAddressSignals.m index 68e2f921ed..cbafb7b87c 100644 --- a/submodules/MtProtoKit/Sources/MTBackupAddressSignals.m +++ b/submodules/MtProtoKit/Sources/MTBackupAddressSignals.m @@ -95,7 +95,6 @@ static NSData *base64_decode(NSString *str) { + (MTSignal *)fetchBackupIpsResolveGoogle:(bool)isTesting phoneNumber:(NSString *)phoneNumber currentContext:(MTContext *)currentContext addressOverride:(NSString *)addressOverride { NSArray *hosts = @[ @[@"dns.google.com", @""], - @[@"www.google.com", @"dns.google.com"], ]; id encryptionProvider = currentContext.encryptionProvider; @@ -153,6 +152,9 @@ static NSData *base64_decode(NSString *str) { for (NSString *string in strings) { finalString = [finalString stringByAppendingString:[string stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"="]]]; } + while (finalString.length % 4 != 0) { + finalString = [finalString stringByAppendingString:@"="]; + } NSData *result = base64_decode(finalString); NSMutableData *finalData = [[NSMutableData alloc] initWithData:result]; @@ -193,90 +195,6 @@ static NSString *makeRandomPadding() { return string; } -+ (MTSignal *)fetchBackupIpsResolveCloudflare:(bool)isTesting phoneNumber:(NSString *)phoneNumber currentContext:(MTContext *)currentContext addressOverride:(NSString *)addressOverride { - id encryptionProvider = currentContext.encryptionProvider; - - NSArray *hosts = @[ - @[@"mozilla.cloudflare-dns.com", @""], - ]; - - NSMutableArray *signals = [[NSMutableArray alloc] init]; - for (NSArray *hostAndHostname in hosts) { - NSString *host = hostAndHostname[0]; - NSString *hostName = hostAndHostname[1]; - NSMutableDictionary *headers = [[NSMutableDictionary alloc] init]; - headers[@"accept"] = @"application/dns-json"; - if ([hostName length] != 0) { - headers[@"Host"] = hostName; - } - NSString *apvHost = @"apv3.stel.com"; - if (addressOverride != nil) { - apvHost = addressOverride; - } - MTSignal *signal = [[[MTHttpRequestOperation dataForHttpUrl:[NSURL URLWithString:[NSString stringWithFormat:@"https://%@/dns-query?name=%@&type=16&random_padding=%@", host, isTesting ? @"tapv3.stel.com" : apvHost, makeRandomPadding()]] headers:headers] mapToSignal:^MTSignal *(MTHttpResponse *response) { - NSString *dateHeader = response.headers[@"Date"]; - if ([dateHeader isKindOfClass:[NSString class]]) { - NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; - NSLocale *usLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; - [formatter setLocale:usLocale]; - [formatter setDateFormat:@"EEE',' dd' 'MMM' 'yyyy HH':'mm':'ss zzz"]; - NSDate *date = [formatter dateFromString:dateHeader]; - if (date != nil) { - double difference = [date timeIntervalSince1970] - [[NSDate date] timeIntervalSince1970]; - [MTContext setFixedTimeDifference:(int32_t)difference]; - } - } - - NSData *data = response.data; - - NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - if ([dict respondsToSelector:@selector(objectForKey:)]) { - NSArray *answer = dict[@"Answer"]; - NSMutableArray *strings = [[NSMutableArray alloc] init]; - if ([answer respondsToSelector:@selector(objectAtIndex:)]) { - for (NSDictionary *value in answer) { - if ([value respondsToSelector:@selector(objectForKey:)]) { - NSString *part = value[@"data"]; - if ([part respondsToSelector:@selector(characterAtIndex:)]) { - [strings addObject:part]; - } - } - } - [strings sortUsingComparator:^NSComparisonResult(NSString *lhs, NSString *rhs) { - if (lhs.length > rhs.length) { - return NSOrderedAscending; - } else { - return NSOrderedDescending; - } - }]; - - NSString *finalString = @""; - for (NSString *string in strings) { - finalString = [finalString stringByAppendingString:[string stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"="]]]; - } - - NSData *result = base64_decode(finalString); - NSMutableData *finalData = [[NSMutableData alloc] initWithData:result]; - [finalData setLength:256]; - MTBackupDatacenterData *datacenterData = MTIPDataDecode(encryptionProvider, finalData, phoneNumber); - if (datacenterData != nil && [self checkIpData:datacenterData timestamp:(int32_t)[currentContext globalTime] source:@"resolveCloudflare"]) { - return [MTSignal single:datacenterData]; - } - } - } - return [MTSignal complete]; - }] catch:^MTSignal *(__unused id error) { - return [MTSignal complete]; - }]; - if (signals.count != 0) { - signal = [signal delay:signals.count onQueue:[[MTQueue alloc] init]]; - } - [signals addObject:signal]; - } - - return [[MTSignal mergeSignals:signals] take:1]; -} - MTAtomic *sharedFetchConfigKeychains() { static MTAtomic *value = nil; static dispatch_once_t onceToken; @@ -377,7 +295,6 @@ MTAtomic *sharedFetchConfigKeychains() { + (MTSignal * _Nonnull)fetchBackupIps:(bool)isTestingEnvironment currentContext:(MTContext * _Nonnull)currentContext additionalSource:(MTSignal * _Nullable)additionalSource phoneNumber:(NSString * _Nullable)phoneNumber mainDatacenterId:(NSInteger)mainDatacenterId { NSMutableArray *signals = [[NSMutableArray alloc] init]; [signals addObject:[self fetchBackupIpsResolveGoogle:isTestingEnvironment phoneNumber:phoneNumber currentContext:currentContext addressOverride:currentContext.apiEnvironment.accessHostOverride]]; - [signals addObject:[self fetchBackupIpsResolveCloudflare:isTestingEnvironment phoneNumber:phoneNumber currentContext:currentContext addressOverride:currentContext.apiEnvironment.accessHostOverride]]; if (additionalSource != nil) { [signals addObject:[additionalSource mapToSignal:^MTSignal *(MTBackupDatacenterData *datacenterData) { if (![datacenterData isKindOfClass:[MTBackupDatacenterData class]]) { diff --git a/submodules/MtProtoKit/Sources/MTInputStream.m b/submodules/MtProtoKit/Sources/MTInputStream.m index 509427debe..9b470518c7 100644 --- a/submodules/MtProtoKit/Sources/MTInputStream.m +++ b/submodules/MtProtoKit/Sources/MTInputStream.m @@ -53,29 +53,6 @@ static inline int roundUpInput(int numToRound, int multiple) return _wrappedInputStream; } -- (int32_t)readInt32 -{ - int32_t value = 0; - - if ([_wrappedInputStream read:(uint8_t *)&value maxLength:4] != 4) - { - if (MTLogEnabled()) { - MTLog(@"***** Couldn't read int32"); - - @throw [[NSException alloc] initWithName:@"MTInputStreamException" reason:@"readInt32 end of stream" userInfo:@{}]; - } - } - -#if __BYTE_ORDER == __LITTLE_ENDIAN -#elif __BYTE_ORDER == __BIG_ENDIAN -# error "Big endian is not implemented" -#else -# error "Unknown byte order" -#endif - - return value; -} - - (int32_t)readInt32:(bool *)failed { int32_t value = 0; @@ -96,27 +73,6 @@ static inline int roundUpInput(int numToRound, int multiple) return value; } -- (int64_t)readInt64 -{ - int64_t value = 0; - - if ([_wrappedInputStream read:(uint8_t *)&value maxLength:8] != 8) - { - if (MTLogEnabled()) { - MTLog(@"***** Couldn't read int64"); - } - } - -#if __BYTE_ORDER == __LITTLE_ENDIAN -#elif __BYTE_ORDER == __BIG_ENDIAN -# error "Big endian is not implemented" -#else -# error "Unknown byte order" -#endif - - return value; -} - - (int64_t)readInt64:(bool *)failed { int64_t value = 0; @@ -137,27 +93,6 @@ static inline int roundUpInput(int numToRound, int multiple) return value; } -- (double)readDouble -{ - double value = 0.0; - - if ([_wrappedInputStream read:(uint8_t *)&value maxLength:8] != 8) - { - if (MTLogEnabled()) { - MTLog(@"***** Couldn't read double"); - } - } - -#if __BYTE_ORDER == __LITTLE_ENDIAN -#elif __BYTE_ORDER == __BIG_ENDIAN -# error "Big endian is not implemented" -#else -# error "Unknown byte order" -#endif - - return value; -} - - (double)readDouble:(bool *)failed { double value = 0.0; @@ -178,20 +113,6 @@ static inline int roundUpInput(int numToRound, int multiple) return value; } -- (NSData *)readData:(int)length -{ - uint8_t *bytes = (uint8_t *)malloc(length); - NSInteger readLen = [_wrappedInputStream read:bytes maxLength:length]; - if (readLen != length) - { - if (MTLogEnabled()) { - MTLog(@"***** Couldn't read %d bytes", length); - } - } - NSData *data = [[NSData alloc] initWithBytesNoCopy:bytes length:length freeWhenDone:true]; - return data; -} - - (NSData *)readData:(int)length failed:(bool *)failed { uint8_t *bytes = (uint8_t *)malloc(length); @@ -206,20 +127,6 @@ static inline int roundUpInput(int numToRound, int multiple) return data; } -- (NSMutableData *)readMutableData:(NSUInteger)length -{ - uint8_t *bytes = (uint8_t *)malloc(length); - NSInteger readLen = [_wrappedInputStream read:bytes maxLength:length]; - if (readLen != length) - { - if (MTLogEnabled()) { - MTLog(@"***** Couldn't read %d bytes", length); - } - } - NSMutableData *data = [[NSMutableData alloc] initWithBytesNoCopy:bytes length:length freeWhenDone:true]; - return data; -} - - (NSMutableData *)readMutableData:(NSUInteger)length failed:(bool *)failed { uint8_t *bytes = (uint8_t *)malloc(length); @@ -234,60 +141,6 @@ static inline int roundUpInput(int numToRound, int multiple) return data; } -- (NSString *)readString -{ - uint8_t tmp = 0; - [_wrappedInputStream read:&tmp maxLength:1]; - - int paddingBytes = 0; - - int32_t length = tmp; - if (length == 254) - { - length = 0; - [_wrappedInputStream read:((uint8_t *)&length) + 1 maxLength:3]; - length >>= 8; - -#if __BYTE_ORDER == __LITTLE_ENDIAN -#elif __BYTE_ORDER == __BIG_ENDIAN -# error "Big endian is not implemented" -#else -# error "Unknown byte order" -#endif - - paddingBytes = roundUpInput(length, 4) - length; - } - else - { - paddingBytes = roundUpInput(length + 1, 4) - (length + 1); - } - - NSString *string = nil; - - if (length > 0) - { - uint8_t *bytes = (uint8_t *)malloc(length); - NSInteger readLen = [_wrappedInputStream read:bytes maxLength:length]; - if (readLen != length) - { - if (MTLogEnabled()) { - MTLog(@"***** Couldn't read %d bytes", length); - } - } - - string = [[NSString alloc] initWithBytesNoCopy:bytes length:length encoding:NSUTF8StringEncoding freeWhenDone:true]; - } - else - { - string = @""; - } - - for (int i = 0; i < paddingBytes; i++) - [_wrappedInputStream read:&tmp maxLength:1]; - - return string; -} - - (NSString *)readString:(bool *)failed { uint8_t tmp = 0; @@ -342,44 +195,6 @@ static inline int roundUpInput(int numToRound, int multiple) return string; } -- (NSData *)readBytes -{ - uint8_t tmp = 0; - [_wrappedInputStream read:&tmp maxLength:1]; - - int paddingBytes = 0; - - int32_t length = tmp; - if (length == 254) - { - length = 0; - [_wrappedInputStream read:((uint8_t *)&length) + 1 maxLength:3]; - length >>= 8; - - paddingBytes = roundUpInput(length, 4) - length; - } - else - { - paddingBytes = roundUpInput(length + 1, 4) - (length + 1); - } - - uint8_t *bytes = (uint8_t *)malloc(length); - NSInteger readLen = [_wrappedInputStream read:bytes maxLength:length]; - if (readLen != length) - { - if (MTLogEnabled()) { - MTLog(@"***** Couldn't read %d bytes", length); - } - } - - NSData *result = [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:true]; - - for (int i = 0; i < paddingBytes; i++) - [_wrappedInputStream read:&tmp maxLength:1]; - - return result; -} - - (NSData *)readBytes:(bool *)failed { uint8_t tmp = 0; diff --git a/submodules/MtProtoKit/Sources/MTProto.m b/submodules/MtProtoKit/Sources/MTProto.m index efc50c77da..71ac74b747 100644 --- a/submodules/MtProtoKit/Sources/MTProto.m +++ b/submodules/MtProtoKit/Sources/MTProto.m @@ -1800,17 +1800,30 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; MTInputStream *is = [[MTInputStream alloc] initWithData:data]; - int64_t keyId = [is readInt64]; + bool readingError = false; + + int64_t keyId = [is readInt64:&readingError]; + if (readingError) { + return; + } if (keyId == authKey.authKeyId) { - NSData *messageKey = [is readData:16]; + + NSData *messageKey = [is readData:16 failed:&readingError]; + if (readingError) { + return; + } MTMessageEncryptionKey *encryptionKey = [MTMessageEncryptionKey messageEncryptionKeyV2ForAuthKey:effectiveAuthKey.authKey messageKey:messageKey toClient:true]; if (data.length <= 24) { return; } - NSMutableData *encryptedMessageData = [is readMutableData:(data.length - 24)]; + NSMutableData *encryptedMessageData = [is readMutableData:(data.length - 24) failed:&readingError]; + if (readingError) { + return; + } + while (encryptedMessageData.length % 16 != 0) { [encryptedMessageData setLength:encryptedMessageData.length - 1]; } @@ -1819,47 +1832,36 @@ static const NSUInteger MTMaxUnacknowledgedMessageCount = 64; NSData *decryptedData = MTAesDecrypt(encryptedMessageData, encryptionKey.key, encryptionKey.iv); MTInputStream *messageIs = [[MTInputStream alloc] initWithData:decryptedData]; - [messageIs readInt64]; - [messageIs readInt64]; - - [messageIs readInt64]; - [messageIs readInt32]; - [messageIs readInt32]; bool stop = false; + if (!stop) { + [messageIs readInt64:&stop]; + } + if (!stop) { + [messageIs readInt64:&stop]; + } + if (!stop) { + [messageIs readInt64:&stop]; + } + if (!stop) { + [messageIs readInt32:&stop]; + } + if (!stop) { + [messageIs readInt32:&stop]; + } + int64_t reqMsgId = 0; - /*if (true) - {*/ - while (!stop && reqMsgId == 0) - { - int32_t signature = [messageIs readInt32:&stop]; + while (!stop && reqMsgId == 0) { + int32_t signature = [messageIs readInt32:&stop]; + if (!stop) { [self findReqMsgId:messageIs signature:signature reqMsgId:&reqMsgId failed:&stop]; } - /*} - else - { - int32_t signature = [messageIs readInt32]; - if (signature == (int)0xf35c6d01) - reqMsgId = [messageIs readInt64]; - else if (signature == (int)0x73f1f8dc) - { - int count = [messageIs readInt32]; - if (count != 0) - { - [messageIs readInt64]; - [messageIs readInt32]; - [messageIs readInt32]; - - signature = [messageIs readInt32]; - if (signature == (int)0xf35c6d01) - reqMsgId = [messageIs readInt64]; - } - } - }*/ + } - if (reqMsgId != 0) + if (reqMsgId != 0) { completion(token, @(reqMsgId)); + } } } }]; @@ -2306,38 +2308,48 @@ static bool isDataEqualToDataConstTime(NSData *data1, NSData *data2) { if (_useUnauthorizedMode) { - int64_t authKeyId = [is readInt64]; + bool readingError = false; + int64_t authKeyId = [is readInt64:&readingError]; + if (readingError) { + if (parseError != NULL) { + *parseError = true; + } + return nil; + } if (authKeyId != 0) { - if (parseError != NULL) + if (parseError != NULL) { *parseError = true; + } return nil; } embeddedMessageId = [is readInt64:&readError]; if (readError) { - if (parseError != NULL) + if (parseError != NULL) { *parseError = true; + } return nil; } topMessageSize = [is readInt32:&readError]; if (readError || topMessageSize < 4) { - if (parseError != NULL) + if (parseError != NULL) { *parseError = true; + } return nil; } - if (dataMessageId != 0) + if (dataMessageId != 0) { *dataMessageId = embeddedMessageId; + } } else { embeddedSalt = [is readInt64:&readError]; - if (readError) - { + if (readError) { if (parseError != NULL) *parseError = true; return nil; @@ -2346,39 +2358,44 @@ static bool isDataEqualToDataConstTime(NSData *data1, NSData *data2) { embeddedSessionId = [is readInt64:&readError]; if (readError) { - if (parseError != NULL) + if (parseError != NULL) { *parseError = true; + } return nil; } if (embeddedSessionId != _sessionInfo.sessionId) { - if (parseError != NULL) + if (parseError != NULL) { *parseError = true; + } return nil; } embeddedMessageId = [is readInt64:&readError]; if (readError) { - if (parseError != NULL) + if (parseError != NULL) { *parseError = true; + } return nil; } embeddedSeqNo = [is readInt32:&readError]; if (readError) { - if (parseError != NULL) + if (parseError != NULL) { *parseError = true; + } return nil; } [is readInt32:&readError]; if (readError) { - if (parseError != NULL) + if (parseError != NULL) { *parseError = true; + } return nil; } } @@ -2388,31 +2405,33 @@ static bool isDataEqualToDataConstTime(NSData *data1, NSData *data2) { while (true) { NSInteger readBytes = [[is wrappedInputStream] read:buffer maxLength:128]; - if (readBytes <= 0) + if (readBytes <= 0) { break; + } [topMessageData appendBytes:buffer length:readBytes]; } id topObject = [self parseMessage:topMessageData]; if (topObject == nil) { - if (parseError != NULL) + if (parseError != NULL) { *parseError = true; + } return nil; } NSMutableArray *messages = [[NSMutableArray alloc] init]; NSTimeInterval timestamp = embeddedMessageId / 4294967296.0; - if ([topObject isKindOfClass:[MTMsgContainerMessage class]]) - { + if ([topObject isKindOfClass:[MTMsgContainerMessage class]]) { for (MTMessage *subMessage in ((MTMsgContainerMessage *)topObject).messages) { id subObject = [self parseMessage:subMessage.data]; if (subObject == nil) { - if (parseError != NULL) + if (parseError != NULL) { *parseError = true; + } return nil; } @@ -2421,15 +2440,14 @@ static bool isDataEqualToDataConstTime(NSData *data1, NSData *data2) { int32_t subMessageLength = (int32_t)subMessage.data.length; [messages addObject:[[MTIncomingMessage alloc] initWithMessageId:subMessageId seqNo:subMessageSeqNo authKeyId:embeddedAuthKeyId sessionId:embeddedSessionId salt:embeddedSalt timestamp:timestamp size:subMessageLength body:subObject]]; } - } - else if ([topObject isKindOfClass:[MTMessage class]]) - { + } else if ([topObject isKindOfClass:[MTMessage class]]) { MTMessage *message = topObject; id subObject = [self parseMessage:message.data]; if (subObject == nil) { - if (parseError != NULL) + if (parseError != NULL) { *parseError = true; + } return nil; } @@ -2437,9 +2455,9 @@ static bool isDataEqualToDataConstTime(NSData *data1, NSData *data2) { int32_t subMessageSeqNo = message.seqNo; int32_t subMessageLength = (int32_t)message.data.length; [messages addObject:[[MTIncomingMessage alloc] initWithMessageId:subMessageId seqNo:subMessageSeqNo authKeyId:embeddedAuthKeyId sessionId:embeddedSessionId salt:embeddedSalt timestamp:timestamp size:subMessageLength body:subObject]]; - } - else + } else { [messages addObject:[[MTIncomingMessage alloc] initWithMessageId:embeddedMessageId seqNo:embeddedSeqNo authKeyId:embeddedAuthKeyId sessionId:embeddedSessionId salt:embeddedSalt timestamp:timestamp size:topMessageSize body:topObject]]; + } return messages; } diff --git a/submodules/MtProtoKit/Sources/MTTcpConnection.m b/submodules/MtProtoKit/Sources/MTTcpConnection.m index ca07b8f467..407748aa3a 100644 --- a/submodules/MtProtoKit/Sources/MTTcpConnection.m +++ b/submodules/MtProtoKit/Sources/MTTcpConnection.m @@ -113,6 +113,28 @@ static bool MTFillRandomBytes(uint8_t *buffer, size_t length) { return SecRandomCopyBytes(kSecRandomDefault, length, buffer) == errSecSuccess; } +static bool generate_ml_kem_public_key(uint8_t key[1184]) { + for (int i = 0; i < 384; i++) { + uint32_t randA = 0; + uint32_t randB = 0; + if (SecRandomCopyBytes(kSecRandomDefault, 4, (uint8_t *)&randA) != errSecSuccess) { + return false; + } + if (SecRandomCopyBytes(kSecRandomDefault, 4, (uint8_t *)&randB) != errSecSuccess) { + return false; + } + uint32_t a = randA % 3329; + uint32_t b = randB % 3329; + key[i * 3 + 0] = (uint8_t)(a & 0xFF); + key[i * 3 + 1] = (uint8_t)((a >> 8) | ((b & 0x0F) << 4)); + key[i * 3 + 2] = (uint8_t)(b >> 4); + } + if (SecRandomCopyBytes(kSecRandomDefault, 32, key + 1152) != errSecSuccess) { + return false; + } + return true; +} + static bool MTGenerateGreaseValues(uint8_t grease[8]) { if (!MTFillRandomBytes(grease, 8)) { return false; @@ -137,7 +159,12 @@ typedef enum { HelloGenerationCommandGrease = 5, HelloGenerationCommandKey = 6, HelloGenerationCommandPushLengthPosition = 7, - HelloGenerationCommandPopLengthPosition = 8 + HelloGenerationCommandPopLengthPosition = 8, + HelloGenerationCommandMlKemKey = 9, + HelloGenerationCommandBeginChoice = 10, + HelloGenerationCommandBeginAlternative = 11, + HelloGenerationCommandEndAlternative = 12, + HelloGenerationCommandEndChoice = 13 } HelloGenerationCommand; typedef struct { @@ -148,7 +175,7 @@ static HelloGenerationCommand parseCommand(NSString *string, HelloParseState *st while (state->position < string.length) { unichar c = [string characterAtIndex:state->position]; state->position += 1; - if (c == '\n' || c == '\r') { + if (c == '\n' || c == '\r' || c == ' ') { continue; } else if (c == 'S') { return HelloGenerationCommandString; @@ -162,10 +189,20 @@ static HelloGenerationCommand parseCommand(NSString *string, HelloParseState *st return HelloGenerationCommandGrease; } else if (c == 'K') { return HelloGenerationCommandKey; + } else if (c == 'M') { + return HelloGenerationCommandMlKemKey; } else if (c == '[') { return HelloGenerationCommandPushLengthPosition; } else if (c == ']') { return HelloGenerationCommandPopLengthPosition; + } else if (c == '<') { + return HelloGenerationCommandBeginChoice; + } else if (c == '(') { + return HelloGenerationCommandBeginAlternative; + } else if (c == ')') { + return HelloGenerationCommandEndAlternative; + } else if (c == '>') { + return HelloGenerationCommandEndChoice; } else { return HelloGenerationCommandInvalid; } @@ -187,21 +224,6 @@ static bool parseSpace(NSString *string, HelloParseState *state) { return seenSpace; } -static bool parseEndlineOrEnd(NSString *string, HelloParseState *state) { - while (state->position < string.length) { - unichar c = [string characterAtIndex:state->position]; - if (c == '\n') { - state->position += 1; - return true; - } else if (c == ' ' || c == '\r') { - state->position += 1; - continue; - } else { - return false; - } - } - return true; -} static bool parseHexByte(unichar c, uint8_t *output) { if (c >= '0' && c <= '9') { @@ -284,13 +306,11 @@ static bool parseIntArgument(NSString *string, HelloParseState *state, int *outp value = value * 10 + (c - '0'); hasDigit = true; state->position += 1; - } else if (c == ' ') { - state->position += 1; } else if (c == '\n') { state->position += 1; break; } else { - return false; + break; } } @@ -305,14 +325,173 @@ static bool parseIntArgument(NSString *string, HelloParseState *state, int *outp return true; } +static bool executeGenerationCodeRecursive(NSString *code, HelloParseState *state, uint8_t grease[8], NSData *domain, id provider, NSMutableData *resultData, NSMutableArray *lengthStack) { + while (true) { + HelloGenerationCommand command = parseCommand(code, state); + if (command == HelloGenerationCommandInvalid) { + break; + } + + switch (command) { + case HelloGenerationCommandString: { + if (!parseSpace(code, state)) { + return false; + } + NSData *data = parseHexStringArgument(code, state); + if (data == nil) { + return false; + } + [resultData appendData:data]; + break; + } + case HelloGenerationCommandZero: { + if (!parseSpace(code, state)) { + return false; + } + int zeroLength = 0; + if (!parseIntArgument(code, state, &zeroLength)) { + return false; + } + if (zeroLength < 0) { + return false; + } + NSMutableData *zeros = [[NSMutableData alloc] initWithLength:(NSUInteger)zeroLength]; + [resultData appendData:zeros]; + break; + } + case HelloGenerationCommandRandom: { + if (!parseSpace(code, state)) { + return false; + } + int randomLength = 0; + if (!parseIntArgument(code, state, &randomLength)) { + return false; + } + if (randomLength < 0) { + return false; + } + NSMutableData *randomData = [[NSMutableData alloc] initWithLength:(NSUInteger)randomLength]; + if (!MTFillRandomBytes((uint8_t *)randomData.mutableBytes, randomData.length)) { + return false; + } + [resultData appendData:randomData]; + break; + } + case HelloGenerationCommandDomain: { + [resultData appendData:domain]; + break; + } + case HelloGenerationCommandGrease: { + if (!parseSpace(code, state)) { + return false; + } + int greaseIndex = 0; + if (!parseIntArgument(code, state, &greaseIndex)) { + return false; + } + if (greaseIndex < 0 || greaseIndex >= 8) { + return false; + } + uint8_t value = grease[greaseIndex]; + [resultData appendBytes:&value length:1]; + [resultData appendBytes:&value length:1]; + break; + } + case HelloGenerationCommandKey: { + NSMutableData *key = [[NSMutableData alloc] initWithLength:32]; + generate_public_key((unsigned char *)key.mutableBytes, provider); + [resultData appendData:key]; + break; + } + case HelloGenerationCommandMlKemKey: { + NSMutableData *key = [[NSMutableData alloc] initWithLength:1184]; + if (!generate_ml_kem_public_key((uint8_t *)key.mutableBytes)) { + return false; + } + [resultData appendData:key]; + break; + } + case HelloGenerationCommandPushLengthPosition: { + NSUInteger lengthPosition = resultData.length; + uint8_t zeroBytes[2] = { 0, 0 }; + [resultData appendBytes:zeroBytes length:2]; + [lengthStack addObject:@(lengthPosition)]; + break; + } + case HelloGenerationCommandPopLengthPosition: { + if (lengthStack.count == 0) { + return false; + } + NSUInteger position = (NSUInteger)[lengthStack.lastObject unsignedIntegerValue]; + [lengthStack removeLastObject]; + if (resultData.length < position + 2) { + return false; + } + uint16_t blockLength = (uint16_t)(resultData.length - position - 2); + ((uint8_t *)resultData.mutableBytes)[position] = (uint8_t)((blockLength >> 8) & 0xff); + ((uint8_t *)resultData.mutableBytes)[position + 1] = (uint8_t)(blockLength & 0xff); + break; + } + case HelloGenerationCommandBeginChoice: { + NSMutableArray *alternatives = [[NSMutableArray alloc] init]; + while (true) { + HelloGenerationCommand nextCommand = parseCommand(code, state); + if (nextCommand == HelloGenerationCommandEndChoice) { + break; + } + if (nextCommand != HelloGenerationCommandBeginAlternative) { + return false; + } + NSMutableData *altData = [[NSMutableData alloc] init]; + NSMutableArray *altLengthStack = [[NSMutableArray alloc] init]; + if (!executeGenerationCodeRecursive(code, state, grease, domain, provider, altData, altLengthStack)) { + return false; + } + if (altLengthStack.count != 0) { + return false; + } + [alternatives addObject:altData]; + } + if (alternatives.count == 0) { + return false; + } + uint32_t choiceIndex = arc4random_uniform((uint32_t)alternatives.count); + [resultData appendData:alternatives[choiceIndex]]; + break; + } + case HelloGenerationCommandEndAlternative: { + // signals end of current alternative — return to caller + return true; + } + case HelloGenerationCommandBeginAlternative: + case HelloGenerationCommandEndChoice: { + // should not be encountered directly at this level + return false; + } + default: { + return false; + } + } + } + + return true; +} + static NSMutableData *executeGenerationCode(id provider, NSData *domain) { - NSString *code = @"S \"\\x16\\x03\\x01\\x02\\x00\\x01\\x00\\x01\\xfc\\x03\\x03\"\n" + NSString *code = @"S \"\\x16\\x03\\x01\"\n" + "[\n" + "S \"\\x01\\x00\"\n" + "[\n" + "S \"\\x03\\x03\"\n" "Z 32\n" "S \"\\x20\"\n" "R 32\n" - "S \"\\x00\\x2a\"\n" - "G 0\n" - "S \"\\x13\\x01\\x13\\x02\\x13\\x03\\xc0\\x2c\\xc0\\x2b\\xcc\\xa9\\xc0\\x30\\xc0\\x2f\\xcc\\xa8\\xc0\\x0a\\xc0\\x09\\xc0\\x14\\xc0\\x13\\x00\\x9d\\x00\\x9c\\x00\\x35\\x00\\x2f\\xc0\\x08\\xc0\\x12\\x00\\x0a\\x01\\x00\\x01\\x89\"\n" + "<\n" + "( S \"\\x00\\x1c\" G 0 S \"\\x13\\x02\\x13\\x01\\x13\\x03\\xc0\\x2c\\xc0\\x30\\xc0\\x2b\\xcc\\xa9\\xc0\\x2f\\xcc\\xa8\\xc0\\x0a\\xc0\\x09\\xc0\\x14\\xc0\\x13\" )\n" + "( S \"\\x00\\x2a\" G 0 S \"\\x13\\x02\\x13\\x03\\x13\\x01\\xc0\\x2c\\xc0\\x2b\\xcc\\xa9\\xc0\\x30\\xc0\\x2f\\xcc\\xa8\\xc0\\x0a\\xc0\\x09\\xc0\\x14\\xc0\\x13\\x00\\x9d\\x00\\x9c\\x00\\x35\\x00\\x2f\\xc0\\x08\\xc0\\x12\\x00\\x0a\" )\n" + ">\n" + "S \"\\x01\\x00\"\n" + "[\n" "G 2\n" "S \"\\x00\\x00\\x00\\x00\"\n" "[\n" @@ -323,17 +502,28 @@ static NSMutableData *executeGenerationCode(id provider, NSD "]\n" "]\n" "]\n" - "S \"\\x00\\x17\\x00\\x00\\xff\\x01\\x00\\x01\\x00\\x00\\x0a\\x00\\x0c\\x00\\x0a\"\n" + "S \"\\x00\\x17\\x00\\x00\\xff\\x01\\x00\\x01\\x00\\x00\\x0a\\x00\\x0e\\x00\\x0c\"\n" "G 4\n" - "S \"\\x00\\x1d\\x00\\x17\\x00\\x18\\x00\\x19\\x00\\x0b\\x00\\x02\\x01\\x00\\x00\\x10\\x00\\x0e\\x00\\x0c\\x02\\x68\\x32\\x08\\x68\\x74\\x74\\x70\\x2f\\x31\\x2e\\x31\\x00\\x05\\x00\\x05\\x01\\x00\\x00\\x00\\x00\\x00\\x0d\\x00\\x16\\x00\\x14\\x04\\x03\\x08\\x04\\x04\\x01\\x05\\x03\\x08\\x05\\x08\\x05\\x05\\x01\\x08\\x06\\x06\\x01\\x02\\x01\\x00\\x12\\x00\\x00\\x00\\x33\\x00\\x2b\\x00\\x29\"\n" + "S \"\\x11\\xec\\x00\\x1d\\x00\\x17\\x00\\x18\\x00\\x19\\x00\\x0b\\x00\\x02\\x01\\x00\"\n" + "<\n" + "( S \"\\x00\\x10\\x00\\x0b\\x00\\x09\\x08\\x68\\x74\\x74\\x70\\x2f\\x31\\x2e\\x31\" )\n" + "( S \"\\x00\\x10\\x00\\x0e\\x00\\x0c\\x02\\x68\\x32\\x08\\x68\\x74\\x74\\x70\\x2f\\x31\\x2e\\x31\" )\n" + ">\n" + "S \"\\x00\\x05\\x00\\x05\\x01\\x00\\x00\\x00\\x00\\x00\\x0d\\x00\\x16\\x00\\x14\\x04\\x03\\x08\\x04\\x04\\x01\\x05\\x03\\x08\\x05\\x08\\x05\\x05\\x01\\x08\\x06\\x06\\x01\\x02\\x01\\x00\\x12\\x00\\x00\\x00\\x33\\x04\\xef\\x04\\xed\"\n" "G 4\n" - "S \"\\x00\\x01\\x00\\x00\\x1d\\x00\\x20\"\n" + "S \"\\x00\\x01\\x00\\x11\\xec\\x04\\xc0\"\n" + "M\n" "K\n" - "S \"\\x00\\x2d\\x00\\x02\\x01\\x01\\x00\\x2b\\x00\\x0b\\x0a\"\n" + "S \"\\x00\\x1d\\x00\\x20\"\n" + "K\n" + "S \"\\x00\\x2d\\x00\\x02\\x01\\x01\\x00\\x2b\\x00\\x07\\x06\"\n" "G 6\n" - "S \"\\x03\\x04\\x03\\x03\\x03\\x02\\x03\\x01\\x00\\x1b\\x00\\x03\\x02\\x00\\x01\"\n" + "S \"\\x03\\x04\\x03\\x03\\x00\\x1b\\x00\\x03\\x02\\x00\\x01\"\n" "G 3\n" - "S \"\\x00\\x01\\x00\"\n"; + "S \"\\x00\\x01\\x00\"\n" + "]\n" + "]\n" + "]\n"; uint8_t grease[8]; if (!MTGenerateGreaseValues(grease)) { @@ -345,120 +535,8 @@ static NSMutableData *executeGenerationCode(id provider, NSD HelloParseState state = { .position = 0 }; - while (true) { - HelloGenerationCommand command = parseCommand(code, &state); - if (command == HelloGenerationCommandInvalid) { - break; - } - - switch (command) { - case HelloGenerationCommandString: { - if (!parseSpace(code, &state)) { - return nil; - } - NSData *data = parseHexStringArgument(code, &state); - if (data == nil) { - return nil; - } - [resultData appendData:data]; - break; - } - case HelloGenerationCommandZero: { - if (!parseSpace(code, &state)) { - return nil; - } - int zeroLength = 0; - if (!parseIntArgument(code, &state, &zeroLength)) { - return nil; - } - if (zeroLength < 0) { - return nil; - } - NSMutableData *zeros = [[NSMutableData alloc] initWithLength:(NSUInteger)zeroLength]; - [resultData appendData:zeros]; - break; - } - case HelloGenerationCommandRandom: { - if (!parseSpace(code, &state)) { - return nil; - } - int randomLength = 0; - if (!parseIntArgument(code, &state, &randomLength)) { - return nil; - } - if (randomLength < 0) { - return nil; - } - NSMutableData *randomData = [[NSMutableData alloc] initWithLength:(NSUInteger)randomLength]; - if (!MTFillRandomBytes((uint8_t *)randomData.mutableBytes, randomData.length)) { - return nil; - } - [resultData appendData:randomData]; - break; - } - case HelloGenerationCommandDomain: { - [resultData appendData:domain]; - if (!parseEndlineOrEnd(code, &state)) { - return nil; - } - break; - } - case HelloGenerationCommandGrease: { - if (!parseSpace(code, &state)) { - return nil; - } - int greaseIndex = 0; - if (!parseIntArgument(code, &state, &greaseIndex)) { - return nil; - } - if (greaseIndex < 0 || greaseIndex >= 8) { - return nil; - } - uint8_t value = grease[greaseIndex]; - [resultData appendBytes:&value length:1]; - [resultData appendBytes:&value length:1]; - break; - } - case HelloGenerationCommandKey: { - if (!parseEndlineOrEnd(code, &state)) { - return nil; - } - NSMutableData *key = [[NSMutableData alloc] initWithLength:32]; - generate_public_key((unsigned char *)key.mutableBytes, provider); - [resultData appendData:key]; - break; - } - case HelloGenerationCommandPushLengthPosition: { - if (!parseEndlineOrEnd(code, &state)) { - return nil; - } - NSUInteger lengthPosition = resultData.length; - uint8_t zeroBytes[2] = { 0, 0 }; - [resultData appendBytes:zeroBytes length:2]; - [lengthStack addObject:@(lengthPosition)]; - break; - } - case HelloGenerationCommandPopLengthPosition: { - if (!parseEndlineOrEnd(code, &state)) { - return nil; - } - if (lengthStack.count == 0) { - return nil; - } - NSUInteger position = (NSUInteger)[lengthStack.lastObject unsignedIntegerValue]; - [lengthStack removeLastObject]; - if (resultData.length < position + 2) { - return nil; - } - uint16_t blockLength = (uint16_t)(resultData.length - position - 2); - ((uint8_t *)resultData.mutableBytes)[position] = (uint8_t)((blockLength >> 8) & 0xff); - ((uint8_t *)resultData.mutableBytes)[position + 1] = (uint8_t)(blockLength & 0xff); - break; - } - default: { - return nil; - } - } + if (!executeGenerationCodeRecursive(code, &state, grease, domain, provider, resultData, lengthStack)) { + return nil; } if (lengthStack.count != 0) { diff --git a/submodules/NotificationMuteSettingsUI/BUILD b/submodules/NotificationMuteSettingsUI/BUILD index bb062cc530..b2ac776926 100644 --- a/submodules/NotificationMuteSettingsUI/BUILD +++ b/submodules/NotificationMuteSettingsUI/BUILD @@ -11,7 +11,6 @@ swift_library( ], deps = [ "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramStringFormatting:TelegramStringFormatting", diff --git a/submodules/NotificationSoundSelectionUI/BUILD b/submodules/NotificationSoundSelectionUI/BUILD index 0f9fe3632f..6be86d8138 100644 --- a/submodules/NotificationSoundSelectionUI/BUILD +++ b/submodules/NotificationSoundSelectionUI/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/ItemListUI:ItemListUI", diff --git a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift index a744c229dd..680b6eb9ee 100644 --- a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift +++ b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift @@ -476,7 +476,7 @@ public func notificationSoundSelectionController(context: AccountContext, update break } }), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }) ], parseMarkdown: true), in: .window(.root)) } diff --git a/submodules/PasscodeUI/BUILD b/submodules/PasscodeUI/BUILD index 4230fc328a..4acaed6dd7 100644 --- a/submodules/PasscodeUI/BUILD +++ b/submodules/PasscodeUI/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramUIPreferences:TelegramUIPreferences", diff --git a/submodules/PassportUI/Sources/SecureIdAuthAcceptNode.swift b/submodules/PassportUI/Sources/SecureIdAuthAcceptNode.swift index 6bf9688bfb..b8714b2e63 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthAcceptNode.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthAcceptNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData diff --git a/submodules/PassportUI/Sources/SecureIdAuthController.swift b/submodules/PassportUI/Sources/SecureIdAuthController.swift index 6acb72a6af..c0a66f7147 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthController.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthController.swift @@ -227,7 +227,7 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable } let primaryLanguageByCountry = configuration.nativeLanguageByCountry - return .single(SecureIdEncryptedFormData(form: form, primaryLanguageByCountry: primaryLanguageByCountry, accountPeer: accountPeer._asPeer(), servicePeer: servicePeer._asPeer())) + return .single(SecureIdEncryptedFormData(form: form, primaryLanguageByCountry: primaryLanguageByCountry, accountPeer: accountPeer, servicePeer: servicePeer)) } } |> deliverOnMainQueue).start(next: { [weak self] formData in @@ -268,7 +268,7 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable case .form: break case var .list(list): - list.accountPeer = accountPeer._asPeer() + list.accountPeer = accountPeer list.primaryLanguageByCountry = primaryLanguageByCountry list.encryptedValues = values return .list(list) @@ -328,7 +328,14 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable guard let strongSelf = self else { return } - let _ = (strongSelf.context.account.postbox.loadedPeerWithId(mention.peerId) + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: mention.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { peer in guard let strongSelf = self else { return diff --git a/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift b/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift index a4e0ff2753..cc9c224885 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthControllerNode.swift @@ -303,7 +303,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { current.updateValues(formData.values) contentNode = current } else { - let current = SecureIdAuthFormContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peer: EnginePeer(encryptedFormData.servicePeer), privacyPolicyUrl: encryptedFormData.form.termsUrl, form: formData, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, openField: { [weak self] field in + let current = SecureIdAuthFormContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peer: encryptedFormData.servicePeer, privacyPolicyUrl: encryptedFormData.form.termsUrl, form: formData, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, openField: { [weak self] field in if let strongSelf = self { switch field { case .identity, .address: @@ -684,7 +684,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { var currentValue: SecureIdValueWithContext? switch type { case .phone: - if let peer = form.encryptedFormData?.accountPeer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { + if case let .user(peer)? = form.encryptedFormData?.accountPeer, let phone = peer.phone, !phone.isEmpty { immediatelyAvailableValue = .phone(SecureIdPhoneValue(phone: phone)) } currentValue = findValue(formData.values, key: .phone)?.1 @@ -936,7 +936,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode { deleteField(.phone) } else { var immediatelyAvailableValue: SecureIdValue? - if let peer = list.accountPeer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { + if case let .user(peer)? = list.accountPeer, let phone = peer.phone, !phone.isEmpty { immediatelyAvailableValue = .phone(SecureIdPhoneValue(phone: phone)) } self.interaction.push(SecureIdPlaintextFormController(context: self.context, secureIdContext: secureIdContext, type: .phone, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { value in diff --git a/submodules/PassportUI/Sources/SecureIdAuthControllerState.swift b/submodules/PassportUI/Sources/SecureIdAuthControllerState.swift index 632a2de9bb..7a9fb1eaf8 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthControllerState.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthControllerState.swift @@ -6,8 +6,8 @@ import TelegramCore struct SecureIdEncryptedFormData { let form: EncryptedSecureIdForm let primaryLanguageByCountry: [String: String] - let accountPeer: Peer - let servicePeer: Peer + let accountPeer: EnginePeer + let servicePeer: EnginePeer } enum SecureIdAuthPasswordChallengeState { @@ -64,7 +64,7 @@ struct SecureIdAuthControllerFormState: Equatable { } struct SecureIdAuthControllerListState: Equatable { - var accountPeer: Peer? + var accountPeer: EnginePeer? var twoStepEmail: String? var verificationState: SecureIdAuthControllerVerificationState? var encryptedValues: EncryptedAllSecureIdValues? @@ -73,7 +73,7 @@ struct SecureIdAuthControllerListState: Equatable { var removingValues: Bool = false static func ==(lhs: SecureIdAuthControllerListState, rhs: SecureIdAuthControllerListState) -> Bool { - if !arePeersEqual(lhs.accountPeer, rhs.accountPeer) { + if lhs.accountPeer != rhs.accountPeer { return false } if let lhsTwoStepEmail = lhs.twoStepEmail, let rhsTwoStepEmail = rhs.twoStepEmail, lhsTwoStepEmail != rhsTwoStepEmail { diff --git a/submodules/PassportUI/Sources/SecureIdAuthHeaderNode.swift b/submodules/PassportUI/Sources/SecureIdAuthHeaderNode.swift index 732cfa73af..5744bbaedf 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthHeaderNode.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthHeaderNode.swift @@ -52,8 +52,8 @@ final class SecureIdAuthHeaderNode: ASDisplayNode { func updateState(formData: SecureIdEncryptedFormData?, verificationState: SecureIdAuthControllerVerificationState) { if let formData = formData { - self.serviceAvatarNode.setPeer(context: self.context, theme: self.theme, peer: EnginePeer(formData.servicePeer)) - let titleData = self.strings.Passport_RequestHeader(EnginePeer(formData.servicePeer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder)) + self.serviceAvatarNode.setPeer(context: self.context, theme: self.theme, peer: formData.servicePeer) + let titleData = self.strings.Passport_RequestHeader(formData.servicePeer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder)) let titleString = NSMutableAttributedString() titleString.append(NSAttributedString(string: titleData.string, font: textFont, textColor: self.theme.list.freeTextColor)) diff --git a/submodules/PassportUI/Sources/SecureIdAuthListContentNode.swift b/submodules/PassportUI/Sources/SecureIdAuthListContentNode.swift index 5dc349a96d..4c087691bb 100644 --- a/submodules/PassportUI/Sources/SecureIdAuthListContentNode.swift +++ b/submodules/PassportUI/Sources/SecureIdAuthListContentNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData diff --git a/submodules/PassportUI/Sources/SecureIdDocumentFormController.swift b/submodules/PassportUI/Sources/SecureIdDocumentFormController.swift index 1cd73e5d88..8444f283e4 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentFormController.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentFormController.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import ProgressNavigationButtonNode diff --git a/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift b/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift index e3f85c953c..93db136475 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentFormControllerNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import TelegramCore -import Postbox import SwiftSignalKit import TelegramPresentationData import TelegramStringFormatting @@ -2169,7 +2168,7 @@ final class SecureIdDocumentFormControllerNode: FormControllerNode Void)? - self.uploadContext = SecureIdVerificationDocumentsContext(postbox: self.context.account.postbox, network: self.context.account.network, context: self.secureIdContext, update: { id, state in + self.uploadContext = SecureIdVerificationDocumentsContext(engine: self.context.engine, context: self.secureIdContext, update: { id, state in updateImpl?(id, state) }) diff --git a/submodules/PassportUI/Sources/SecureIdDocumentGalleryController.swift b/submodules/PassportUI/Sources/SecureIdDocumentGalleryController.swift index 524683feb3..99d0e833bb 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentGalleryController.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentGalleryController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import QuickLook -import Postbox import SwiftSignalKit import AsyncDisplayKit import TelegramCore diff --git a/submodules/PassportUI/Sources/SecureIdDocumentGalleryFooterContentNode.swift b/submodules/PassportUI/Sources/SecureIdDocumentGalleryFooterContentNode.swift index aefb7de1ec..8b38c4cb0a 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentGalleryFooterContentNode.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentGalleryFooterContentNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import SwiftSignalKit import Photos diff --git a/submodules/PassportUI/Sources/SecureIdDocumentImageGalleryItem.swift b/submodules/PassportUI/Sources/SecureIdDocumentImageGalleryItem.swift index 45d6a4c48d..b570bacb93 100644 --- a/submodules/PassportUI/Sources/SecureIdDocumentImageGalleryItem.swift +++ b/submodules/PassportUI/Sources/SecureIdDocumentImageGalleryItem.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import AccountContext diff --git a/submodules/PassportUI/Sources/SecureIdPlaintextFormController.swift b/submodules/PassportUI/Sources/SecureIdPlaintextFormController.swift index e393672c69..e96cbb7105 100644 --- a/submodules/PassportUI/Sources/SecureIdPlaintextFormController.swift +++ b/submodules/PassportUI/Sources/SecureIdPlaintextFormController.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import ProgressNavigationButtonNode diff --git a/submodules/PassportUI/Sources/SecureIdPlaintextFormControllerNode.swift b/submodules/PassportUI/Sources/SecureIdPlaintextFormControllerNode.swift index 5e98f57294..5c2e9015d3 100644 --- a/submodules/PassportUI/Sources/SecureIdPlaintextFormControllerNode.swift +++ b/submodules/PassportUI/Sources/SecureIdPlaintextFormControllerNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import TelegramCore -import Postbox import SwiftSignalKit import TelegramPresentationData import AccountContext diff --git a/submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift b/submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift index f72286c482..6ffec45c81 100644 --- a/submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift +++ b/submodules/PassportUI/Sources/SecureIdVerificationDocumentsContext.swift @@ -1,5 +1,4 @@ import Foundation -import Postbox import TelegramCore import SwiftSignalKit @@ -17,15 +16,13 @@ private final class DocumentContext { final class SecureIdVerificationDocumentsContext { private let context: SecureIdAccessContext - private let postbox: Postbox - private let network: Network + private let engine: TelegramEngine private let update: (Int64, SecureIdVerificationLocalDocumentState) -> Void private var contexts: [Int64: DocumentContext] = [:] private(set) var uploadedFiles: [Data: Data] = [:] - init(postbox: Postbox, network: Network, context: SecureIdAccessContext, update: @escaping (Int64, SecureIdVerificationLocalDocumentState) -> Void) { - self.postbox = postbox - self.network = network + init(engine: TelegramEngine, context: SecureIdAccessContext, update: @escaping (Int64, SecureIdVerificationLocalDocumentState) -> Void) { + self.engine = engine self.context = context self.update = update } @@ -40,7 +37,7 @@ final class SecureIdVerificationDocumentsContext { if self.contexts[info.id] == nil { let disposable = MetaDisposable() self.contexts[info.id] = DocumentContext(disposable: disposable) - disposable.set((uploadSecureIdFile(context: self.context, postbox: self.postbox, network: self.network, resource: info.resource) + disposable.set((uploadSecureIdFile(context: self.context, engine: self.engine, resource: EngineMediaResource(info.resource)) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { switch result { diff --git a/submodules/PasswordSetupUI/BUILD b/submodules/PasswordSetupUI/BUILD index 00943960e7..a57d6a5239 100644 --- a/submodules/PasswordSetupUI/BUILD +++ b/submodules/PasswordSetupUI/BUILD @@ -27,7 +27,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationController.swift index e6c938946d..01405a99a2 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, accentDisabledButtonColor: self.presentationData.theme.chat.inputPanel.panelControlDisabledColor, 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/SetupTwoStepVerificationControllerNode.swift b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift index 692d1e07cf..aa64693954 100644 --- a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift +++ b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift @@ -697,7 +697,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { }, transition: .animated(duration: 0.5, curve: .spring)) } if case let .enterEmail(enterEmailState, enterEmailEmail)? = self.innerState.data.state, case .create = enterEmailState, enterEmailEmail.isEmpty { - self.present(textAlertController(sharedContext: self.context.sharedContext, title: nil, text: self.presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.TwoStepAuth_EmailSkip, action: { + self.present(textAlertController(sharedContext: self.context.sharedContext, title: nil, text: self.presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.TwoStepAuth_EmailSkip, action: { continueImpl() })]), nil) } else { diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index 54843c67d3..cf77e35974 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, accentDisabledButtonColor: defaultTheme.accentDisabledButtonColor, accentForegroundColor: defaultTheme.accentForegroundColor) super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: NavigationBarStrings(back: self.presentationData.strings.Common_Back, close: self.presentationData.strings.Common_Close))) @@ -534,7 +534,7 @@ public final class TwoFactorDataInputScreen: ViewController { strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }) }), - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}) + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}) ]), in: .window(.root)) case let .passwordHint(recovery, password, doneText): if let recovery = recovery { @@ -550,7 +550,7 @@ public final class TwoFactorDataInputScreen: ViewController { } strongSelf.performRecovery(recovery: recovery, password: "", hint: "") }), - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}) + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}) ]), in: .window(.root)) case let .rememberPassword(doneText): guard case let .authorized(engine) = strongSelf.engine else { diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthSplashScreen.swift index 6b0209ed8c..670043340b 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, accentDisabledButtonColor: defaultTheme.accentDisabledButtonColor, 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/PaymentMethodUI/BUILD b/submodules/PaymentMethodUI/BUILD index 50a49ff478..171123463a 100644 --- a/submodules/PaymentMethodUI/BUILD +++ b/submodules/PaymentMethodUI/BUILD @@ -29,7 +29,6 @@ swift_library( "//submodules/InviteLinksUI:InviteLinksUI", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/ItemListUI:ItemListUI", diff --git a/submodules/PeerAvatarGalleryUI/BUILD b/submodules/PeerAvatarGalleryUI/BUILD index 9e7b1b5a5f..4afd265a5b 100644 --- a/submodules/PeerAvatarGalleryUI/BUILD +++ b/submodules/PeerAvatarGalleryUI/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AccountContext:AccountContext", diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift index 53f8b99fb7..3b05051ac9 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryController.swift @@ -20,6 +20,28 @@ public enum AvatarGalleryEntryId: Hashable { case resource(String) } +private func avatarGalleryEntryMatchesImage(_ entry: AvatarGalleryEntry, _ image: TelegramMediaImage) -> Bool { + guard + let entryRepresentation = largestImageRepresentation(entry.representations.map({ $0.representation })), + let imageRepresentation = largestImageRepresentation(image.representations) + else { + return false + } + guard let entryResource = entryRepresentation.resource as? CloudPeerPhotoSizeMediaResource, let imageResource = imageRepresentation.resource as? CloudPhotoSizeMediaResource else { + return false + } + return entryResource.photoId == imageResource.photoId +} + +private func avatarGalleryEntryWithVideoRepresentations(_ entry: AvatarGalleryEntry, videoRepresentations: [VideoRepresentationWithReference], immediateThumbnailData: Data?) -> AvatarGalleryEntry { + switch entry { + case let .topImage(representations, _, peer, indexData, _, category): + return .topImage(representations, videoRepresentations, peer, indexData, immediateThumbnailData, category) + default: + return entry + } +} + public func peerInfoProfilePhotos(context: AccountContext, peerId: EnginePeer.Id) -> Signal { return context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> mapToSignal { peer -> Signal<[AvatarGalleryEntry]?, NoError> in @@ -48,6 +70,12 @@ public func peerInfoProfilePhotos(context: AccountContext, peerId: EnginePeer.Id } if case let .known(photo) = cachedData.fallbackPhoto { lastEntry = photo + if let photo, firstEntry.videoRepresentations.isEmpty, !photo.videoRepresentations.isEmpty, avatarGalleryEntryMatchesImage(firstEntry, photo), let peerReference = PeerReference(peer) { + let videoRepresentations = photo.videoRepresentations.map { + VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) + } + firstEntry = avatarGalleryEntryWithVideoRepresentations(firstEntry, videoRepresentations: videoRepresentations, immediateThumbnailData: firstEntry.immediateThumbnailData ?? photo.immediateThumbnailData) + } } } return fetchedAvatarGalleryEntries(engine: context.engine, account: context.account, peer: EnginePeer(peer), firstEntry: firstEntry, secondEntry: secondEntry, lastEntry: lastEntry) @@ -205,11 +233,11 @@ public func normalizeEntries(_ entries: [AvatarGalleryEntry]) -> [AvatarGalleryE public func initialAvatarGalleryEntries(account: Account, engine: TelegramEngine, peer: EnginePeer) -> Signal<[AvatarGalleryEntry]?, NoError> { var initialEntries: [AvatarGalleryEntry] = [] - if !peer.profileImageRepresentations.isEmpty, let peerReference = PeerReference(peer._asPeer()) { + if !peer.profileImageRepresentations.isEmpty, let peerReference = PeerReference(peer) { initialEntries.append(.topImage(peer.profileImageRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) }), [], peer, nil, nil, nil)) } - guard let peerReference = PeerReference(peer._asPeer()) else { + guard let peerReference = PeerReference(peer) else { return .single(initialEntries) } switch peer { @@ -255,7 +283,7 @@ public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account var result: [AvatarGalleryEntry] = [] if photos.isEmpty { result = initialEntries - } else if let peerReference = PeerReference(peer._asPeer()) { + } else if let peerReference = PeerReference(peer) { var index: Int32 = 0 if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(peer.id.namespace) { var initialMediaIds = Set() @@ -311,7 +339,13 @@ public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account let initialEntries = [firstEntry] if photos.isEmpty { result = initialEntries - } else if let peerReference = PeerReference(peer._asPeer()) { + if let lastEntry, let firstEntry = result.first, firstEntry.videoRepresentations.isEmpty, !lastEntry.videoRepresentations.isEmpty, avatarGalleryEntryMatchesImage(firstEntry, lastEntry), let peerReference = PeerReference(peer) { + let videoRepresentations = lastEntry.videoRepresentations.map { + VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) + } + result[0] = avatarGalleryEntryWithVideoRepresentations(firstEntry, videoRepresentations: videoRepresentations, immediateThumbnailData: firstEntry.immediateThumbnailData ?? lastEntry.immediateThumbnailData) + } + } else if let peerReference = PeerReference(peer) { var index: Int32 = 0 if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(peer.id.namespace) { @@ -812,7 +846,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr } else { } case let .image(_, reference, _, _, _, _, _, _, _, _, _, _): - if self.peer.id == self.context.account.peerId, let peerReference = PeerReference(self.peer._asPeer()) { + if self.peer.id == self.context.account.peerId, let peerReference = PeerReference(self.peer) { if let reference = reference { let _ = (self.context.engine.accountData.updatePeerPhotoExisting(reference: reference) |> deliverOnMainQueue).start(next: { [weak self] photo in @@ -821,12 +855,12 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr for (lhs, rhs) in zip(firstEntry.representations, updatedEntry.representations) { if lhs.representation.dimensions == rhs.representation.dimensions { - strongSelf.context.account.postbox.mediaBox.copyResourceData(from: lhs.representation.resource.id, to: rhs.representation.resource.id, synchronous: true) + strongSelf.context.engine.resources.copyResourceData(from: EngineMediaResource.Id(lhs.representation.resource.id), to: EngineMediaResource.Id(rhs.representation.resource.id), synchronous: true) } } for (lhs, rhs) in zip(firstEntry.videoRepresentations, updatedEntry.videoRepresentations) { if lhs.representation.dimensions == rhs.representation.dimensions { - strongSelf.context.account.postbox.mediaBox.copyResourceData(from: lhs.representation.resource.id, to: rhs.representation.resource.id, synchronous: true) + strongSelf.context.engine.resources.copyResourceData(from: EngineMediaResource.Id(lhs.representation.resource.id), to: EngineMediaResource.Id(rhs.representation.resource.id), synchronous: true) } } diff --git a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift index 7bea9520d7..acae546563 100644 --- a/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift +++ b/submodules/PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift @@ -125,7 +125,7 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode { } if let peer = peer { - canShare = !peer._asPeer().isCopyProtectionEnabled + canShare = !peer.isCopyProtectionEnabled } default: break diff --git a/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift b/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift index 3d29daa132..d6fed42e2b 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 @@ -186,7 +185,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { if let strongSelf = self, let entry = strongSelf.entry, !entry.representations.isEmpty { let subject: ShareControllerSubject var actionCompletionText: String? - if let video = entry.videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) { + if let video = entry.videoRepresentations.last, let peerReference = PeerReference(peer) { let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) subject = .media(videoFileReference.abstract, nil) actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved @@ -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) } } @@ -262,7 +260,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { self.zoomableContent = (largestSize.dimensions.cgSize, self.contentNode) if let largestIndex = representations.firstIndex(where: { $0.representation == largestSize }) { - self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[largestIndex].reference).start()) + self.fetchDisposable.set(self.context.engine.resources.fetch(reference: representations[largestIndex].reference, userLocation: .other, userContentType: .image).start()) } var id: Int64 @@ -276,7 +274,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { id = id &+ resource.photoId } } - if let video = entry.videoRepresentations.last, let peerReference = PeerReference(self.peer._asPeer()) { + if let video = entry.videoRepresentations.last, let peerReference = PeerReference(self.peer) { if video != previousVideoRepresentations?.last { let mediaManager = self.context.sharedContext.mediaManager let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: entry.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) @@ -601,7 +599,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { if let entry = self.entry, let largestSize = largestImageRepresentation(entry.representations.map({ $0.representation })), let status = self.status { switch status { case .Fetching: - self.context.account.postbox.mediaBox.cancelInteractiveResourceFetch(largestSize.resource) + self.context.engine.resources.cancelInteractiveResourceFetch(id: EngineMediaResource.Id(largestSize.resource.id)) case .Remote: let representations: [ImageRepresentationWithReference] switch entry { @@ -612,7 +610,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode { } if let largestIndex = representations.firstIndex(where: { $0.representation == largestSize }) { - self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[largestIndex].reference).start()) + self.fetchDisposable.set(self.context.engine.resources.fetch(reference: representations[largestIndex].reference, userLocation: .other, userContentType: .image).start()) } default: break diff --git a/submodules/PeerInfoAvatarListNode/BUILD b/submodules/PeerInfoAvatarListNode/BUILD index 64d85a6c6d..e772d8c798 100644 --- a/submodules/PeerInfoAvatarListNode/BUILD +++ b/submodules/PeerInfoAvatarListNode/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AvatarNode:AvatarNode", diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index 6d2ebebd3c..f9d6c51e3a 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -534,7 +534,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode { self.didSetReady = true self.isReady.set(.single(true)) } - } else if let video = videoRepresentations.last, let peerReference = PeerReference(self.peer._asPeer()) { + } else if let video = videoRepresentations.last, let peerReference = PeerReference(self.peer) { let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: [])) let videoContent = NativeVideoContent(id: .profileVideo(id, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: fullSizeOnly, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil) diff --git a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD index ff18627371..ecd04a15a4 100644 --- a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD +++ b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/TelegramPresentationData", "//submodules/AccountContext", diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index e592965d63..ab6d4cfe48 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -385,7 +385,7 @@ private enum ChannelAdminEntry: ItemListNodeEntry { return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: true), spacing: 0.0, clearType: enabled ? .always : .none, enabled: enabled, tag: ChannelAdminEntryTag.rank, sectionId: self.section, textUpdated: { updatedText in arguments.updateRank(text, updatedText) }, shouldUpdateText: { text in - if text.containsEmoji { + if text.containsGraphicEmoji { arguments.animateError() return false } @@ -673,6 +673,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s .direct(.canManageRanks), .direct(.canPinMessages), .direct(.canManageTopics), + .sub(.stories, storiesRelatedFlags), .direct(.canManageCalls), .direct(.canBeAnonymous), .direct(.canAddAdmins) @@ -1229,7 +1230,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD guard let peer else { return } - if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { pushControllerImpl?(controller) } }) @@ -1328,7 +1329,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD return current } - if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { + if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsGraphicEmoji { errorImpl?() return } @@ -1360,7 +1361,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD } let effectiveRank = updateRank ?? currentRank - if effectiveRank?.containsEmoji ?? false { + if effectiveRank?.containsGraphicEmoji ?? false { errorImpl?() return } @@ -1435,7 +1436,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD return current } - if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { + if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsGraphicEmoji { errorImpl?() return } @@ -1515,7 +1516,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD return current } - if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { + if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsGraphicEmoji { errorImpl?() return } @@ -1553,6 +1554,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD dismissImpl?() }, completed: { + updated(TelegramChatAdminRights(rights: updateFlags)) dismissImpl?() })) } else if updateFlags != defaultFlags || updateRank != nil { diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift index a554523895..21f5dd4927 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminsController.swift @@ -294,7 +294,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { if peer.id == participant.peer.id { peerText = strings.Channel_Management_LabelAdministrator } else { - peerText = strings.Channel_Management_PromotedBy(EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string + peerText = strings.Channel_Management_PromotedBy(peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string } } else { peerText = "" @@ -323,7 +323,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { .init(type: .destructive, title: presentationData.strings.Channel_Management_DismissAdmin, action: { arguments.removeAdmin(participant.peer.id) }) ]) - return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(participant.peer), presence: participant.presences[participant.peer.id].flatMap { EnginePeer.Presence($0) }, text: peerText.isEmpty ? .presence : .text(peerText, .secondary), label: label, editing: editing, revealOptions: revealOptions, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: action, setPeerIdWithRevealedOptions: { previousId, id in + return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: participant.presences[participant.peer.id].flatMap { EnginePeer.Presence($0) }, text: peerText.isEmpty ? .presence : .text(peerText, .secondary), label: label, editing: editing, revealOptions: revealOptions, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: action, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removeAdmin(peerId) @@ -731,7 +731,7 @@ public func channelAdminsController(context: AccountContext, updatedPresentation } if case .legacyGroup = peer { } else { - pushControllerImpl?(context.sharedContext.makeChatRecentActionsController(context: context, peer: peer._asPeer(), adminPeerId: nil, starsState: nil)) + pushControllerImpl?(context.sharedContext.makeChatRecentActionsController(context: context, peer: peer, adminPeerId: nil, starsState: nil)) } }) }) @@ -918,12 +918,12 @@ public func channelAdminsController(context: AccountContext, updatedPresentation } switch participant { case let .creator(_, rank): - result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: rank), peer: peer._asPeer(), presences: presences)) + result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: rank), peer: peer, presences: presences)) case let .admin(_, _, _, rank): var peers: [EnginePeer.Id: EnginePeer] = [:] peers[creator.id] = creator peers[peer.id] = peer - result.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: .internal_groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: rank, subscriptionUntilDate: nil), peer: peer._asPeer(), peers: peers.mapValues({ $0._asPeer() }), presences: presences)) + result.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: .internal_groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: rank, subscriptionUntilDate: nil), peer: peer, peers: peers, presences: presences)) case .member: break } diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index 2c0fe628f0..ef2cb34f5a 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -382,7 +382,7 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: true), spacing: 0.0, clearType: enabled ? .always : .none, enabled: enabled, tag: ChannelBannedMemberEntryTag.rank, sectionId: self.section, textUpdated: { updatedText in arguments.updateRank(text, updatedText) }, shouldUpdateText: { text in - if text.containsEmoji { + if text.containsGraphicEmoji { arguments.animateError() return false } @@ -782,7 +782,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen guard let peer else { return } - if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { pushControllerImpl?(controller) } }) @@ -860,7 +860,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen updateRank = current.updatedRank?.trimmingCharacters(in: .whitespacesAndNewlines) return current } - if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { + if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsGraphicEmoji { errorImpl?() return } diff --git a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift index 86dd2e6d09..4ac22010a9 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift @@ -162,12 +162,12 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry { switch participant.participant { case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { - text = .text(strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string, .secondary) + text = .text(strings.Channel_Management_RemovedBy(peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string, .secondary) } default: break } - return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(participant.peer), presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { arguments.openPeer(participant) }, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) @@ -313,9 +313,16 @@ public func channelBlacklistController(context: AccountContext, updatedPresentat } } } - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { channel in - guard let _ = channel as? TelegramChannel else { + guard case .channel = channel else { return } @@ -356,20 +363,20 @@ public func channelBlacklistController(context: AccountContext, updatedPresentat let presentationData = context.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(presentationData: presentationData) var items: [ActionSheetItem] = [] - if !EnginePeer(participant.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder).isEmpty { - items.append(ActionSheetTextItem(title: EnginePeer(participant.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))) + if !participant.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder).isEmpty { + items.append(ActionSheetTextItem(title: participant.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))) } let viewInfoTitle: String - if participant.peer is TelegramChannel { + if case .channel = participant.peer { viewInfoTitle = presentationData.strings.GroupRemoved_ViewChannelInfo } else { viewInfoTitle = presentationData.strings.GroupRemoved_ViewUserInfo } items.append(ActionSheetButtonItem(title: viewInfoTitle, action: { [weak actionSheet] in actionSheet?.dismissAnimated() - if participant.peer is TelegramChannel { + if case .channel = participant.peer { if let navigationController = getNavigationControllerImpl?() { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(participant.peer)))) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(participant.peer))) } } else if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: participant.peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { pushControllerImpl?(infoController) diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift index 036bdd4a28..92ddbb53ed 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift @@ -265,10 +265,19 @@ private enum ChannelMembersEntry: ItemListNodeEntry { func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! ChannelMembersControllerArguments switch self { - case let .hideMembers(text, disabledReason, isInteractive, value): - return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: isInteractive, enabled: true, displayLocked: !value && disabledReason != nil, sectionId: self.section, style: .blocks, updated: { value in + case let .hideMembers(text, disabledReason, isInteractive, currentValue): + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: currentValue, enableInteractiveChanges: isInteractive, enabled: true, displayLocked: !currentValue && disabledReason != nil, sectionId: self.section, style: .blocks, updated: { value in if let disabledReason { - arguments.displayHideMembersTip(disabledReason) + switch disabledReason { + case .notEnoughMembers: + if currentValue && !value { + arguments.updateHideMembers(value) + } else { + arguments.displayHideMembersTip(disabledReason) + } + case .notAllowed: + arguments.displayHideMembersTip(disabledReason) + } } else { arguments.updateHideMembers(value) } @@ -293,7 +302,7 @@ private enum ChannelMembersEntry: ItemListNodeEntry { return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .peerItem(_, _, strings, dateTimeFormat, nameDisplayOrder, participant, editing, enabled, _, isGroup): let text: ItemListPeerItemText - if let user = participant.peer as? TelegramUser, let _ = user.botInfo { + if case let .user(user) = participant.peer, let _ = user.botInfo { text = .text(strings.Bot_GenericBotStatus, .secondary) } else { text = .presence @@ -321,7 +330,7 @@ private enum ChannelMembersEntry: ItemListNodeEntry { label = .none } - return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(participant.peer), presence: participant.presences[participant.peer.id].flatMap(EnginePeer.Presence.init), text: text, label: label, editing: editing, switchValue: nil, enabled: enabled, selectable: participant.peer.id != arguments.context.account.peerId, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: participant.presences[participant.peer.id].flatMap(EnginePeer.Presence.init), text: text, label: label, editing: editing, switchValue: nil, enabled: enabled, selectable: participant.peer.id != arguments.context.account.peerId, sectionId: self.section, action: { arguments.openParticipant(participant, isGroup) }, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) @@ -773,7 +782,7 @@ public func channelMembersController(context: AccountContext, updatedPresentatio return state.withUpdatedSearchingMembers(false) } }, openPeer: { peer, _ in - if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { pushControllerImpl?(infoController) } }, pushController: { c in diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift index 13fd8cad95..131210bdf4 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift @@ -209,7 +209,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable { displayOrder: nameDisplayOrder, context: context, peerMode: .peer, - peer: .peer(peer: EnginePeer(participant.peer), chatPeer: EnginePeer(participant.peer)), + peer: .peer(peer: participant.peer, chatPeer: participant.peer), status: status, rightLabelText: label.flatMap { .init(text: $0, color: labelColor, hasBackground: labelBackground) }, enabled: enabled, @@ -220,7 +220,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable { index: nil, header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) }), action: { _ in - interaction.peerSelected(EnginePeer(participant.peer), participant) + interaction.peerSelected(participant.peer, participant) }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in interaction.setPeerIdWithRevealedOptions(RevealedPeerId(peerId: participant.peer.id, section: self.section), fromPeerId.flatMap({ RevealedPeerId(peerId: $0, section: self.section) })) @@ -452,7 +452,14 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon state.revealedPeerId = nil return state } - let signal = context.account.postbox.loadedPeerWithId(memberId) + let signal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: memberId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue |> mapToSignal { peer -> Signal in let result = ValuePromise() @@ -742,7 +749,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon continue } - if excludeBots, let user = participant.peer as? TelegramUser, user.botInfo != nil { + if excludeBots, case let .user(user) = participant.peer, user.botInfo != nil { continue } @@ -829,7 +836,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon if peer.id == participant.peer.id { label = presentationData.strings.Channel_Management_LabelAdministrator } else { - label = presentationData.strings.Channel_Management_PromotedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string + label = presentationData.strings.Channel_Management_PromotedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string } } } @@ -860,7 +867,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon switch participant.participant { case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { - label = presentationData.strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string + label = presentationData.strings.Channel_Management_RemovedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string } default: break @@ -874,7 +881,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } for participant in foundMembers { - if excludeBots, let user = participant.peer as? TelegramUser, user.botInfo != nil { + if excludeBots, case let .user(user) = participant.peer, user.botInfo != nil { continue } @@ -918,27 +925,27 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon for foundPeer in foundRemotePeers.0 { let peer = foundPeer.peer - - if excludeBots, let user = peer as? TelegramUser, user.botInfo != nil { + + if excludeBots, case let .user(user) = peer, user.botInfo != nil { continue } - - if !existingPeerIds.contains(peer.id) && peer is TelegramUser { + + if !existingPeerIds.contains(peer.id), case .user = peer { existingPeerIds.insert(peer.id) - entries.append(ChannelMembersSearchEntry(index: index, content: .peer(EnginePeer(peer)), section: .global, dateTimeFormat: presentationData.dateTimeFormat)) + entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: presentationData.dateTimeFormat)) index += 1 } } - + for foundPeer in foundRemotePeers.1 { let peer = foundPeer.peer - if excludeBots, let user = peer as? TelegramUser, user.botInfo != nil { + if excludeBots, case let .user(user) = peer, user.botInfo != nil { continue } - - if !existingPeerIds.contains(peer.id) && peer is TelegramUser { + + if !existingPeerIds.contains(peer.id), case .user = peer { existingPeerIds.insert(peer.id) - entries.append(ChannelMembersSearchEntry(index: index, content: .peer(EnginePeer(peer)), section: .global, dateTimeFormat: presentationData.dateTimeFormat)) + entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: presentationData.dateTimeFormat)) index += 1 } } @@ -977,18 +984,18 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon let renderedParticipant: RenderedChannelParticipant switch participant { case .creator: - renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer) + renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: EnginePeer(peer)) case .admin: var peers: [EnginePeer.Id: EnginePeer] = [:] if let creator = creatorPeer { peers[creator.id] = creator } peers[peer.id] = EnginePeer(peer) - renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: .legacyGroup(group))), promotedBy: creatorPeer?.id ?? context.account.peerId, canBeEditedByAccountPeer: creatorPeer?.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() })) + renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: .legacyGroup(group))), promotedBy: creatorPeer?.id ?? context.account.peerId, canBeEditedByAccountPeer: creatorPeer?.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: EnginePeer(peer), peers: peers) case .member: var peers: [EnginePeer.Id: EnginePeer] = [:] peers[peer.id] = EnginePeer(peer) - renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() })) + renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: EnginePeer(peer), peers: peers) } matchingMembers.append(renderedParticipant) } @@ -1042,7 +1049,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon var index = 0 for participant in foundGroupMembers { - if excludeBots, let user = participant.peer as? TelegramUser, user.botInfo != nil { + if excludeBots, case let .user(user) = participant.peer, user.botInfo != nil { continue } @@ -1081,7 +1088,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon if peer.id == participant.peer.id { label = presentationData.strings.Channel_Management_LabelAdministrator } else { - label = presentationData.strings.Channel_Management_PromotedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string + label = presentationData.strings.Channel_Management_PromotedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string } } } @@ -1112,7 +1119,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon switch participant.participant { case let .member(_, _, _, banInfo, _, _): if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { - label = presentationData.strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string + label = presentationData.strings.Channel_Management_RemovedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string } default: break @@ -1126,7 +1133,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon } for participant in foundMembers { - if excludeBots, let user = participant.peer as? TelegramUser, user.botInfo != nil { + if excludeBots, case let .user(user) = participant.peer, user.botInfo != nil { continue } @@ -1160,28 +1167,28 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon for foundPeer in foundRemotePeers.0 { let peer = foundPeer.peer - - if excludeBots, let user = peer as? TelegramUser, user.botInfo != nil { + + if excludeBots, case let .user(user) = peer, user.botInfo != nil { continue } - - if !existingPeerIds.contains(peer.id) && peer is TelegramUser { + + if !existingPeerIds.contains(peer.id), case .user = peer { existingPeerIds.insert(peer.id) - entries.append(ChannelMembersSearchEntry(index: index, content: .peer(EnginePeer(peer)), section: .global, dateTimeFormat: presentationData.dateTimeFormat)) + entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: presentationData.dateTimeFormat)) index += 1 } } - + for foundPeer in foundRemotePeers.1 { let peer = foundPeer.peer - - if excludeBots, let user = peer as? TelegramUser, user.botInfo != nil { + + if excludeBots, case let .user(user) = peer, user.botInfo != nil { continue } - - if !existingPeerIds.contains(peer.id) && peer is TelegramUser { + + if !existingPeerIds.contains(peer.id), case .user = peer { existingPeerIds.insert(peer.id) - entries.append(ChannelMembersSearchEntry(index: index, content: .peer(EnginePeer(peer)), section: .global, dateTimeFormat: presentationData.dateTimeFormat)) + entries.append(ChannelMembersSearchEntry(index: index, content: .peer(peer), section: .global, dateTimeFormat: presentationData.dateTimeFormat)) index += 1 } } diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift index 2925bbd07f..cbbe58d850 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift @@ -144,8 +144,8 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable { headerType = isChannel ? .subscribers : .groupMembers } - return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: EnginePeer(participant.peer), chatPeer: nil), status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings), action: { _ in - interaction.openPeer(EnginePeer(participant.peer), participant) + return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: nil), status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings), action: { _ in + interaction.openPeer(participant.peer, participant) }) case let .contact(_, peer, presence): let status: ContactsPeerItemStatus @@ -401,16 +401,16 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { let renderedParticipant: RenderedChannelParticipant switch participant { case .creator: - renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer, presences: peerView.peerPresences) + renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: EnginePeer(peer), presences: peerView.peerPresences) case .admin: var peers: [EnginePeer.Id: EnginePeer] = [:] peers[creator.id] = creator peers[peer.id] = EnginePeer(peer) - renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: EnginePeer(mainPeer))), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences) + renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: EnginePeer(mainPeer))), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: EnginePeer(peer), peers: peers, presences: peerView.peerPresences) case .member: var peers: [EnginePeer.Id: EnginePeer] = [:] peers[peer.id] = EnginePeer(peer) - renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences) + renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: EnginePeer(peer), peers: peers, presences: peerView.peerPresences) } entries.append(.peer(index, renderedParticipant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled, false, false)) @@ -513,7 +513,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { case .excludeNonMembers: break case .excludeBots: - if let user = participant.peer as? TelegramUser, user.botInfo != nil { + if case let .user(user) = participant.peer, user.botInfo != nil { continue contactsLoop } } @@ -555,7 +555,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { case .excludeNonMembers: break case .excludeBots: - if let user = participant.peer as? TelegramUser, user.botInfo != nil { + if case let .user(user) = participant.peer, user.botInfo != nil { continue participantsLoop } } @@ -568,7 +568,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { if participant.peer.id == context.account.peerId { continue } - if let user = participant.peer as? TelegramUser, user.botInfo != nil || user.flags.contains(.isSupport) { + if case let .user(user) = participant.peer, user.botInfo != nil || user.flags.contains(.isSupport) { continue } for filter in filters { @@ -584,7 +584,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { case .excludeNonMembers: break case .excludeBots: - if let user = participant.peer as? TelegramUser, user.botInfo != nil { + if case let .user(user) = participant.peer, user.botInfo != nil { continue participantsLoop } } diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index d03290333d..e035ee457c 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -477,10 +477,10 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry { default: break } - return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(participant.peer), presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: canOpen ? { + return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: canOpen ? { arguments.openPeer(participant.participant) } : { - arguments.openPeerInfo(EnginePeer(participant.peer)) + arguments.openPeerInfo(participant.peer) }, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in @@ -534,6 +534,8 @@ func stringForGroupPermission(strings: PresentationStrings, right: TelegramChatB return strings.Channel_BanUser_PermissionSendVoiceMessage } else if right.contains(.banSendInstantVideos) { return strings.Channel_BanUser_PermissionSendVideoMessage + } else if right.contains(.banSendReactions) { + return strings.Channel_BanUser_PermissionSendReactions } else if right.contains(.banEditRank) { if defaultPermissions { return strings.Channel_BanUser_PermissionEditOwnRank @@ -568,6 +570,8 @@ func compactStringForGroupPermission(strings: PresentationStrings, right: Telegr return strings.GroupPermission_NoSendLinks } else if right.contains(.banSendPolls) { return strings.GroupPermission_NoSendPolls + } else if right.contains(.banSendReactions) { + return strings.GroupPermission_NoSendReactions } else if right.contains(.banChangeInfo) { return strings.GroupPermission_NoChangeInfo } else if right.contains(.banAddMembers) { @@ -595,6 +599,7 @@ private let internal_allPossibleGroupPermissionList: [(TelegramChatBannedRightsF (.banSendInstantVideos, .banMembers), (.banEmbedLinks, .banMembers), (.banSendPolls, .banMembers), + (.banSendReactions, .banMembers), (.banAddMembers, .banMembers), (.banPinMessages, .pinMessages), (.banManageTopics, .manageTopics), @@ -647,6 +652,7 @@ public func banSendMediaSubList() -> [(TelegramChatBannedRightsFlags, TelegramCh (.banSendInstantVideos, .banMembers), (.banEmbedLinks, .banMembers), (.banSendPolls, .banMembers), + (.banSendReactions, .banMembers) ] } @@ -1051,7 +1057,14 @@ public func channelPermissionsController(context: AccountContext, updatedPresent } } } - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { channel in dismissController?() presentControllerImpl?(channelBannedMemberController(context: context, peerId: peerId, memberId: peer.id, initialParticipant: participant?.participant, updated: { _ in @@ -1095,7 +1108,7 @@ public func channelPermissionsController(context: AccountContext, updatedPresent }), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) }, openPeerInfo: { peer in - if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { pushControllerImpl?(controller) } }, openKicked: { diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index 5328856fd3..032111510f 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -1475,7 +1475,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta let (limits, premiumLimits) = data let isPremium = accountPeer?.isPremium ?? false - let hasAdditionalUsernames = (peer?._asPeer().usernames.firstIndex(where: { !$0.flags.contains(.isEditable) }) ?? nil) != nil + let hasAdditionalUsernames = (peer?.usernames.firstIndex(where: { !$0.flags.contains(.isEditable) }) ?? nil) != nil if let peers = peers { let count = Int32(peers.count) @@ -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 { @@ -1601,10 +1600,17 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: peerId)) |> deliverOnMainQueue).start(next: { invite in if let invite = invite { - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { peer in let isGroup: Bool - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } else { isGroup = true @@ -1620,10 +1626,17 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta }, action: { _, f in f(.dismissWithoutContent) - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { peer in let isGroup: Bool - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { isGroup = false } else { isGroup = true diff --git a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift index 73cdece2cb..c699a9fe67 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 { @@ -410,7 +410,7 @@ private enum DeviceContactInfoEntry: ItemListNodeEntry { if let context = arguments.context as? ShareControllerAppAccountContext { itemContext = .accountContext(context.context) } else { - itemContext = .other(accountPeerId: arguments.context.accountPeerId, postbox: arguments.context.stateManager.postbox, network: arguments.context.stateManager.network) + itemContext = .other(accountPeerId: arguments.context.accountPeerId, stateManager: arguments.context.stateManager) } return ItemListAvatarAndNameInfoItem(itemContext: itemContext, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .contact, peer: peer, presence: nil, label: jobSummary, memberCount: nil, state: state, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks(withTopInset: false, withExtendedBottomInset: true), editingNameUpdated: { editingName in arguments.updateEditingName(editingName) @@ -846,7 +846,7 @@ public func deviceContactInfoController(context: ShareControllerAccountContext, var peerPhoneNumber: String? var firstName = contactData.basicData.firstName var lastName = contactData.basicData.lastName - if let peer = peer as? TelegramUser { + if case let .user(peer) = peer { firstName = peer.firstName ?? "" lastName = peer.lastName ?? "" if let phone = peer.phone { @@ -939,11 +939,11 @@ public func deviceContactInfoController(context: ShareControllerAccountContext, var shareViaException = false switch subject { case let .vcard(peer, id, data): - contactData = .single((peer.flatMap(EnginePeer.init), id, data)) + contactData = .single((peer, id, data)) case let .filter(peer, id, data, _): - contactData = .single((peer.flatMap(EnginePeer.init), id, data)) + contactData = .single((peer, id, data)) case let .create(peer, data, share, shareViaExceptionValue, _): - contactData = .single((peer.flatMap(EnginePeer.init), nil, data)) + contactData = .single((peer, nil, data)) isShare = share shareViaException = shareViaExceptionValue } @@ -1102,7 +1102,7 @@ public func deviceContactInfoController(context: ShareControllerAccountContext, } else if case let .filter(_, _, _, completion) = subject { let filteredData = filteredContactData(contactData: peerAndContactData.2, excludedComponents: state.excludedComponents) rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.ShareMenu_Send), style: .bold, enabled: !filteredData.basicData.phoneNumbers.isEmpty, action: { - completion(peerAndContactData.0?._asPeer(), filteredData) + completion(peerAndContactData.0, filteredData) dismissImpl?(true) }) } else if case let .create(createForPeer, _, _, _, completion) = subject { @@ -1221,7 +1221,7 @@ public func deviceContactInfoController(context: ShareControllerAccountContext, return state } if let contactIdAndData = contactIdAndData { - completion(contactIdAndData.2?._asPeer(), contactIdAndData.0, contactIdAndData.1) + completion(contactIdAndData.2, contactIdAndData.0, contactIdAndData.1) } completed?() dismissImpl?(true) @@ -1286,7 +1286,7 @@ public func deviceContactInfoController(context: ShareControllerAccountContext, return } addContactToExisting(context: accountContext, parentController: controller, contactData: subject.contactData, completion: { peer, contactId, contactData in - replaceControllerImpl?(deviceContactInfoController(context: context, environment: environment, subject: .vcard(peer?._asPeer(), contactId, contactData), completed: nil, cancelled: nil)) + replaceControllerImpl?(deviceContactInfoController(context: context, environment: environment, subject: .vcard(peer, contactId, contactData), completed: nil, cancelled: nil)) }) } openChatImpl = { [weak controller] peerId in @@ -1416,7 +1416,7 @@ private func addContactToExisting(context: AccountContext, parentController: Vie let dataSignal: Signal<(EnginePeer?, DeviceContactStableId?), NoError> switch peer { case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + guard case let .user(contact) = contact, let phoneNumber = contact.phone else { return } dataSignal = (context.sharedContext.contactDataManager?.basicData() ?? .single([:])) @@ -1440,7 +1440,7 @@ private func addContactToExisting(context: AccountContext, parentController: Vie let _ = (dataSignal |> deliverOnMainQueue).start(next: { peer, stableId in guard let stableId = stableId else { - parentController.present(deviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer?._asPeer(), contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in + parentController.present(deviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer, contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in }), completed: nil, cancelled: nil), in: .window(.root)) return } @@ -1486,7 +1486,7 @@ func addContactOptionsController(context: AccountContext, peer: EnginePeer?, con controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Profile_CreateNewContact, action: { [weak controller] in - controller?.present(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 + controller?.present(context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer, contactData: contactData, isSharing: peer != nil, shareViaException: false, completion: { _, _, _ in }), completed: nil, cancelled: nil), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) dismissAction() }), @@ -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/GroupInfoSearchNavigationContentNode.swift b/submodules/PeerInfoUI/Sources/GroupInfoSearchNavigationContentNode.swift index eb85784b40..5fb1aaf5d9 100644 --- a/submodules/PeerInfoUI/Sources/GroupInfoSearchNavigationContentNode.swift +++ b/submodules/PeerInfoUI/Sources/GroupInfoSearchNavigationContentNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import ItemListUI diff --git a/submodules/PeerInfoUI/Sources/UserInfoController.swift b/submodules/PeerInfoUI/Sources/UserInfoController.swift index 6f8b7694b2..66f16dbb5b 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 @@ -62,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/PeersNearbyUI/Sources/PeersNearbyController.swift b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift index 28fc481656..8dae8cc38d 100644 --- a/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift +++ b/submodules/PeersNearbyUI/Sources/PeersNearbyController.swift @@ -597,11 +597,11 @@ public func peersNearbyController(context: AccountContext) -> ViewController { |> delay(1.0, queue: Queue.mainQueue()) ) - let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), chatLocationPromise.get(), displayLoading, expandedPromise.get(), context.account.postbox.preferencesView(keys: [PreferencesKeys.peersNearby])) + let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), chatLocationPromise.get(), displayLoading, expandedPromise.get(), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.peersNearby))) |> deliverOnMainQueue |> map { presentationData, data, chatLocation, displayLoading, expanded, view -> (ItemListControllerState, (ItemListNodeState, Any)) in let previous = previousData.swap(data) - let state = view.values[PreferencesKeys.peersNearby]?.get(PeersNearbyState.self) ?? .default + let state = view?.get(PeersNearbyState.self) ?? .default var crossfade = false if (data?.users.isEmpty ?? true) != (previous?.users.isEmpty ?? true) { @@ -626,7 +626,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController { controller?.clearItemNodesHighlight(animated: true) } navigateToProfileImpl = { [weak controller] peer, distance in - if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .nearbyPeer(distance: distance), avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { + if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .nearbyPeer(distance: distance), avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { navigationController.pushViewController(controller) } } diff --git a/submodules/PhotoResources/BUILD b/submodules/PhotoResources/BUILD index 31205c31e0..5afacafb6d 100644 --- a/submodules/PhotoResources/BUILD +++ b/submodules/PhotoResources/BUILD @@ -24,7 +24,7 @@ swift_library( "//submodules/WebPBinding:WebPBinding", "//submodules/AppBundle:AppBundle", "//submodules/MusicAlbumArtResources:MusicAlbumArtResources", - "//submodules/Svg:Svg", + "//submodules/Svg/LegacyImpl", "//submodules/Utils/RangeSet:RangeSet", "//submodules/ImageCompression", ], diff --git a/submodules/PhotoResources/Sources/PhotoResources.swift b/submodules/PhotoResources/Sources/PhotoResources.swift index 0d2a373bb0..86a270792e 100644 --- a/submodules/PhotoResources/Sources/PhotoResources.swift +++ b/submodules/PhotoResources/Sources/PhotoResources.swift @@ -17,10 +17,10 @@ import TinyThumbnail import ImageTransparency import AppBundle import MusicAlbumArtResources -import Svg import RangeSet import Accelerate import ImageCompression +import LegacyImpl private enum ResourceFileData { case data(Data) @@ -2076,7 +2076,7 @@ public func chatMessagePhotoStatus(context: AccountContext, messageId: MessageId if let range = representationFetchRangeForDisplayAtSize(representation: largestRepresentation, dimension: displayAtSize) { return combineLatest( context.fetchManager.fetchStatus(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: largestRepresentation.resource), - context.account.postbox.mediaBox.resourceRangesStatus(largestRepresentation.resource) + context.engine.resources.resourceRangesStatus(resource: EngineMediaResource(largestRepresentation.resource)) ) |> map { status, rangeStatus -> MediaResourceStatus in if rangeStatus.isSuperset(of: RangeSet(range)) { @@ -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/Postbox/Sources/AllTypingDraftsView.swift b/submodules/Postbox/Sources/AllTypingDraftsView.swift new file mode 100644 index 0000000000..5290154e63 --- /dev/null +++ b/submodules/Postbox/Sources/AllTypingDraftsView.swift @@ -0,0 +1,49 @@ +import Foundation + +final class MutableAllTypingDraftsView: MutablePostboxView { + fileprivate var keys: Set + + init(postbox: PostboxImpl) { + self.keys = Set(postbox.currentTypingDrafts.keys) + } + + func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool { + if transaction.updatedTypingDrafts.isEmpty { + return false + } + var updated = false + for (key, update) in transaction.updatedTypingDrafts { + if update.value != nil { + if self.keys.insert(key).inserted { + updated = true + } + } else { + if self.keys.remove(key) != nil { + updated = true + } + } + } + return updated + } + + func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool { + let new = Set(postbox.currentTypingDrafts.keys) + if new == self.keys { + return false + } + self.keys = new + return true + } + + func immutableView() -> PostboxView { + return AllTypingDraftsView(self) + } +} + +public final class AllTypingDraftsView: PostboxView { + public let keys: Set + + init(_ view: MutableAllTypingDraftsView) { + self.keys = view.keys + } +} diff --git a/submodules/Postbox/Sources/Media.swift b/submodules/Postbox/Sources/Media.swift index 567e6ccfd2..136df74c98 100644 --- a/submodules/Postbox/Sources/Media.swift +++ b/submodules/Postbox/Sources/Media.swift @@ -80,6 +80,7 @@ public protocol Media: AnyObject, PostboxCoding { var id: MediaId? { get } var peerIds: [PeerId] { get } var storyIds: [StoryId] { get } + var mediaIds: [MediaId] { get } var indexableText: String? { get } @@ -95,6 +96,9 @@ public extension Media { var storyIds: [StoryId] { return [] } + var mediaIds: [MediaId] { + return [] + } } public func areMediaArraysEqual(_ lhs: [Media], _ rhs: [Media]) -> Bool { diff --git a/submodules/Postbox/Sources/MessageHistoryTable.swift b/submodules/Postbox/Sources/MessageHistoryTable.swift index cd004d82cc..e20aef448b 100644 --- a/submodules/Postbox/Sources/MessageHistoryTable.swift +++ b/submodules/Postbox/Sources/MessageHistoryTable.swift @@ -1263,13 +1263,13 @@ final class MessageHistoryTable: Table { if let mediaId = media.id { let mediaInsertResult = self.messageMediaTable.set(media, index: message.index, messageHistoryTable: self) switch mediaInsertResult { - case let .Embed(media): - embeddedMedia.append(media) - case .Reference: - referencedMedia.append(mediaId) - if media.isLikelyToBeUpdated() { - updateExistingMedia[mediaId] = media - } + case let .Embed(media): + embeddedMedia.append(media) + case .Reference: + referencedMedia.append(mediaId) + if media.isLikelyToBeUpdated() { + updateExistingMedia[mediaId] = media + } } } else { embeddedMedia.append(media) @@ -2759,12 +2759,6 @@ final class MessageHistoryTable: Table { } } - /*#if DEBUG - for key in associatedStories.keys { - associatedStories[key] = CodableEntry(data: Data()) - } - #endif*/ - associatedMessageIds.append(contentsOf: attribute.associatedMessageIds) if addAssociatedMessages { for messageId in attribute.associatedMessageIds { @@ -2786,6 +2780,16 @@ final class MessageHistoryTable: Table { peers[possibleThreadPeer.id] = possibleThreadPeer } + for media in parsedMedia { + for id in media.mediaIds { + if associatedMedia[id] == nil { + if let media = self.getMedia(id) { + associatedMedia[id] = media + } + } + } + } + return Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, groupInfo: message.groupInfo, threadId: message.threadId, timestamp: message.timestamp, flags: message.flags, tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, customTags: message.customTags, forwardInfo: forwardInfo, author: author, text: message.text, attributes: parsedAttributes, media: parsedMedia, peers: peers, associatedMessages: associatedMessages, associatedMessageIds: associatedMessageIds, associatedMedia: associatedMedia, associatedThreadInfo: associatedThreadInfo, associatedStories: associatedStories) } diff --git a/submodules/Postbox/Sources/MessageHistoryThreadIndexView.swift b/submodules/Postbox/Sources/MessageHistoryThreadIndexView.swift index cb8976110b..40a72ada71 100644 --- a/submodules/Postbox/Sources/MessageHistoryThreadIndexView.swift +++ b/submodules/Postbox/Sources/MessageHistoryThreadIndexView.swift @@ -1,39 +1,72 @@ import Foundation -final class MutableMessageHistoryThreadIndexView: MutablePostboxView { - final class Item { - let id: Int64 - let pinnedIndex: Int? - let index: MessageIndex - var info: CodableEntry - var tagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo] - var topMessage: Message? - var embeddedInterfaceState: StoredPeerChatInterfaceState? - - init( - id: Int64, - pinnedIndex: Int?, - index: MessageIndex, - info: CodableEntry, - tagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo], - topMessage: Message?, - embeddedInterfaceState: StoredPeerChatInterfaceState? - ) { - self.id = id - self.pinnedIndex = pinnedIndex - self.index = index - self.info = info - self.tagSummaryInfo = tagSummaryInfo - self.topMessage = topMessage - self.embeddedInterfaceState = embeddedInterfaceState - } +public final class MessageHistoryThreadIndexItem: Equatable { + public let id: Int64 + public let pinnedIndex: Int? + public let index: MessageIndex + public var info: CodableEntry + public var tagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo] + public var topMessage: Message? + public var embeddedInterfaceState: StoredPeerChatInterfaceState? + + public init( + id: Int64, + pinnedIndex: Int?, + index: MessageIndex, + info: CodableEntry, + tagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo], + topMessage: Message?, + embeddedInterfaceState: StoredPeerChatInterfaceState? + ) { + self.id = id + self.pinnedIndex = pinnedIndex + self.index = index + self.info = info + self.tagSummaryInfo = tagSummaryInfo + self.topMessage = topMessage + self.embeddedInterfaceState = embeddedInterfaceState } - + + public static func ==(lhs: MessageHistoryThreadIndexItem, rhs: MessageHistoryThreadIndexItem) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.pinnedIndex != rhs.pinnedIndex { + return false + } + if lhs.index != rhs.index { + return false + } + if lhs.info != rhs.info { + return false + } + if lhs.tagSummaryInfo != rhs.tagSummaryInfo { + return false + } + if let lhsMessage = lhs.topMessage, let rhsMessage = rhs.topMessage { + if lhsMessage.index != rhsMessage.index { + return false + } + if lhsMessage.stableVersion != rhsMessage.stableVersion { + return false + } + } else if (lhs.topMessage == nil) != (rhs.topMessage == nil) { + return false + } + if lhs.embeddedInterfaceState != rhs.embeddedInterfaceState { + return false + } + + return true + } +} + +final class MutableMessageHistoryThreadIndexView: MutablePostboxView { fileprivate let peerId: PeerId fileprivate let summaryComponents: ChatListEntrySummaryComponents fileprivate var peer: Peer? fileprivate var peerNotificationSettings: PeerNotificationSettings? - fileprivate var items: [Item] = [] + fileprivate var items: [MessageHistoryThreadIndexItem] = [] private var hole: ForumTopicListHolesEntry? fileprivate var isLoading: Bool = false @@ -99,7 +132,7 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView { var embeddedInterfaceState: StoredPeerChatInterfaceState? embeddedInterfaceState = postbox.peerChatThreadInterfaceStateTable.get(PeerChatThreadId(peerId: self.peerId, threadId: item.threadId)) - self.items.append(Item( + self.items.append(MessageHistoryThreadIndexItem( id: item.threadId, pinnedIndex: pinnedIndex, index: item.index, @@ -152,93 +185,16 @@ final class MutableMessageHistoryThreadIndexView: MutablePostboxView { } } -public final class EngineMessageHistoryThread { - public final class Item: Equatable { - public let id: Int64 - public let pinnedIndex: Int? - public let index: MessageIndex - public let info: CodableEntry - public let tagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo] - public let topMessage: Message? - public let embeddedInterfaceState: StoredPeerChatInterfaceState? - - public init( - id: Int64, - pinnedIndex: Int?, - index: MessageIndex, - info: CodableEntry, - tagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo], - topMessage: Message?, - embeddedInterfaceState: StoredPeerChatInterfaceState? - ) { - self.id = id - self.pinnedIndex = pinnedIndex - self.index = index - self.info = info - self.tagSummaryInfo = tagSummaryInfo - self.topMessage = topMessage - self.embeddedInterfaceState = embeddedInterfaceState - } - - public static func ==(lhs: Item, rhs: Item) -> Bool { - if lhs.id != rhs.id { - return false - } - if lhs.pinnedIndex != rhs.pinnedIndex { - return false - } - if lhs.index != rhs.index { - return false - } - if lhs.info != rhs.info { - return false - } - if lhs.tagSummaryInfo != rhs.tagSummaryInfo { - return false - } - if let lhsMessage = lhs.topMessage, let rhsMessage = rhs.topMessage { - if lhsMessage.index != rhsMessage.index { - return false - } - if lhsMessage.stableVersion != rhsMessage.stableVersion { - return false - } - } else if (lhs.topMessage == nil) != (rhs.topMessage == nil) { - return false - } - if lhs.embeddedInterfaceState != rhs.embeddedInterfaceState { - return false - } - - return true - } - } -} - public final class MessageHistoryThreadIndexView: PostboxView { public let peer: Peer? public let peerNotificationSettings: PeerNotificationSettings? - public let items: [EngineMessageHistoryThread.Item] + public let items: [MessageHistoryThreadIndexItem] public let isLoading: Bool - + init(_ view: MutableMessageHistoryThreadIndexView) { self.peer = view.peer self.peerNotificationSettings = view.peerNotificationSettings - - var items: [EngineMessageHistoryThread.Item] = [] - for item in view.items { - items.append(EngineMessageHistoryThread.Item( - id: item.id, - pinnedIndex: item.pinnedIndex, - index: item.index, - info: item.info, - tagSummaryInfo: item.tagSummaryInfo, - topMessage: item.topMessage, - embeddedInterfaceState: item.embeddedInterfaceState - )) - } - self.items = items - + self.items = view.items self.isLoading = view.isLoading } } diff --git a/submodules/Postbox/Sources/MessageHistoryView.swift b/submodules/Postbox/Sources/MessageHistoryView.swift index f9e4792913..f517f5b3d9 100644 --- a/submodules/Postbox/Sources/MessageHistoryView.swift +++ b/submodules/Postbox/Sources/MessageHistoryView.swift @@ -1069,28 +1069,30 @@ final class MutableMessageHistoryView: MutablePostboxView { } } - switch self.peerIds { - case let .single(peerId, threadId): - let location = PeerAndThreadId(peerId: peerId, threadId: threadId) - if let typingDraftUpdate = transaction.updatedTypingDrafts[location] { - if let typingDraft = typingDraftUpdate.value, self.namespaces.contains(typingDraft.namespace) { - self.typingDraft = self.renderTypingDraft(postbox: postbox, typingDraft: typingDraft) - } else { - self.typingDraft = nil + if self.tag == nil { + switch self.peerIds { + case let .single(peerId, threadId): + let location = PeerAndThreadId(peerId: peerId, threadId: threadId) + if let typingDraftUpdate = transaction.updatedTypingDrafts[location] { + if let typingDraft = typingDraftUpdate.value, self.namespaces.contains(typingDraft.namespace) { + self.typingDraft = self.renderTypingDraft(postbox: postbox, typingDraft: typingDraft) + } else { + self.typingDraft = nil + } + hasChanges = true } - hasChanges = true + case .external: + break + case .associated: + break } - case .external: - break - case .associated: - break } return hasChanges } private func reloadTypingDraft(postbox: PostboxImpl) { - guard case let .single(peerId, threadId) = self.peerIds else { + guard case let .single(peerId, threadId) = self.peerIds, self.tag == nil else { self.typingDraft = nil return } @@ -1558,13 +1560,15 @@ public final class MessageHistoryView: PostboxView { } if !self.holeLater, let typingDraft = mutableView.typingDraft { - entries.append(MessageHistoryEntry( + let newEntry = MessageHistoryEntry( message: typingDraft, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false) - )) + ) + entries.append(newEntry) + entries.sort() } self.entries = entries 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/Postbox/Sources/TypingDraftsView.swift b/submodules/Postbox/Sources/TypingDraftsView.swift new file mode 100644 index 0000000000..cb8a1bf361 --- /dev/null +++ b/submodules/Postbox/Sources/TypingDraftsView.swift @@ -0,0 +1,98 @@ +import Foundation + +final class MutableTypingDraftsView: MutablePostboxView { + fileprivate let peerAndThreadId: PeerAndThreadId + fileprivate var typingDraft: Message? + + init(postbox: PostboxImpl, peerAndThreadId: PeerAndThreadId) { + self.peerAndThreadId = peerAndThreadId + + self.reload(postbox: postbox) + } + + private func reload(postbox: PostboxImpl) { + if let typingDraft = postbox.currentTypingDrafts[self.peerAndThreadId] { + self.typingDraft = self.renderTypingDraft(postbox: postbox, typingDraft: typingDraft) + } else { + self.typingDraft = nil + } + } + + func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool { + var updated = false + + if let typingDraftUpdate = transaction.updatedTypingDrafts[self.peerAndThreadId] { + if let typingDraft = typingDraftUpdate.value { + self.typingDraft = self.renderTypingDraft(postbox: postbox, typingDraft: typingDraft) + } else { + self.typingDraft = nil + } + updated = true + } + + return updated + } + + private func renderTypingDraft(postbox: PostboxImpl, typingDraft: PostboxImpl.TypingDraft) -> Message? { + guard let peer = postbox.peerTable.get(self.peerAndThreadId.peerId), let author = postbox.peerTable.get(typingDraft.authorId) else { + return nil + } + + var peers = SimpleDictionary() + peers[peer.id] = peer + peers[author.id] = author + + var associatedThreadInfo: Message.AssociatedThreadInfo? + if let threadId = typingDraft.threadId, let data = postbox.messageHistoryThreadIndexTable.get(peerId: self.peerAndThreadId.peerId, threadId: threadId) { + associatedThreadInfo = postbox.seedConfiguration.decodeMessageThreadInfo(data.data) + } + + return Message( + stableId: typingDraft.stableId, + stableVersion: typingDraft.stableVersion, + id: MessageId( + peerId: self.peerAndThreadId.peerId, + namespace: 1, + id: Int32.max - 50000), + globallyUniqueId: nil, + groupingKey: nil, + groupInfo: nil, + threadId: typingDraft.threadId, + timestamp: typingDraft.timestamp, + flags: [.Incoming], + tags: [], + globalTags: [], + localTags: [], + customTags: [], + forwardInfo: nil, + author: author, + text: typingDraft.text, + attributes: typingDraft.attributes, + media: [], + peers: peers, + associatedMessages: SimpleDictionary(), + associatedMessageIds: [], + associatedMedia: [:], + associatedThreadInfo: associatedThreadInfo, + associatedStories: [:] + ) + } + + func refreshDueToExternalTransaction(postbox: PostboxImpl) -> Bool { + self.reload(postbox: postbox) + + return true + } + + func immutableView() -> PostboxView { + return TypingDraftsView(self) + } +} + +public final class TypingDraftsView: PostboxView { + public let typingDraft: Message? + + init(_ view: MutableTypingDraftsView) { + self.typingDraft = view.typingDraft + } +} diff --git a/submodules/Postbox/Sources/Views.swift b/submodules/Postbox/Sources/Views.swift index ab21dcf429..6d5453d416 100644 --- a/submodules/Postbox/Sources/Views.swift +++ b/submodules/Postbox/Sources/Views.swift @@ -102,6 +102,8 @@ public enum PostboxViewKey: Hashable { case savedMessagesStats(peerId: PeerId) case chatInterfaceState(peerId: PeerId) case historyView(HistoryView) + case typingDrafts(PeerAndThreadId) + case allTypingDrafts public func hash(into hasher: inout Hasher) { switch self { @@ -187,7 +189,7 @@ public enum PostboxViewKey: Hashable { case let .topChatMessage(peerIds): hasher.combine(peerIds) case .contacts: - hasher.combine(16) + hasher.combine(24) case let .deletedMessages(peerId): hasher.combine(peerId) case let .notice(key): @@ -228,6 +230,11 @@ public enum PostboxViewKey: Hashable { hasher.combine(20) hasher.combine(historyView.peerId) hasher.combine(historyView.threadId) + case let .typingDrafts(peerId): + hasher.combine(23) + hasher.combine(peerId) + case .allTypingDrafts: + hasher.combine(22) } } @@ -545,6 +552,18 @@ public enum PostboxViewKey: Hashable { } else { return false } + case let .typingDrafts(peerId): + if case .typingDrafts(peerId) = rhs { + return true + } else { + return false + } + case .allTypingDrafts: + if case .allTypingDrafts = rhs { + return true + } else { + return false + } } } } @@ -672,5 +691,9 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost topTaggedMessages: [:], additionalDatas: [] ) + case let .typingDrafts(peerId): + return MutableTypingDraftsView(postbox: postbox, peerAndThreadId: peerId) + case .allTypingDrafts: + return MutableAllTypingDraftsView(postbox: postbox) } } diff --git a/submodules/PremiumUI/Sources/CreateGiveawayController.swift b/submodules/PremiumUI/Sources/CreateGiveawayController.swift index 6271348638..c05f255806 100644 --- a/submodules/PremiumUI/Sources/CreateGiveawayController.swift +++ b/submodules/PremiumUI/Sources/CreateGiveawayController.swift @@ -1607,6 +1607,7 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio let stateContext = CountriesMultiselectionScreen.StateContext( context: context, subject: .countries, + maxCount: context.userLimits.maxGiveawayCountriesCount, initialSelectedCountries: state.countries ) let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { _ in diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 3a16ecefbf..d4bef601c4 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -329,8 +329,8 @@ public enum PremiumSource: Equatable { } else { return false } - case let .auth(lhsPrice): - if case let .auth(rhsPrice) = rhs, lhsPrice == rhsPrice { + case let .auth(lhsPrice, lhsDays): + if case let .auth(rhsPrice, rhsDays) = rhs, lhsPrice == rhsPrice, lhsDays == rhsDays { return true } else { return false @@ -391,7 +391,7 @@ public enum PremiumSource: Equatable { case todo case copyProtection case aiTools - case auth(String) + case auth(String, Int32) case premiumGift(TelegramMediaFile) var identifier: String? { @@ -3786,13 +3786,26 @@ private final class PremiumIntroScreenComponent: CombinedComponent { if !buttonIsHidden { let buttonTitle: String var buttonSubtitle: String? - if case let .auth(price) = context.component.source { + if case let .auth(price, days) = context.component.source { buttonTitle = environment.strings.Premium_Week_SignUp(price).string - buttonSubtitle = environment.strings.Premium_Week_SignUpInfo + if days == 7 { + buttonSubtitle = environment.strings.Premium_Week_SignUpInfo + } else if days > 0 { + let daysString = environment.strings.Premium_SignUp_SignUpNewInfo_Days(days) + buttonSubtitle = environment.strings.Premium_SignUp_SignUpNewInfo(daysString).string + } else { + buttonSubtitle = environment.strings.Premium_SignUp_SignUpNewInfoNone + } } 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 +3816,6 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } } - - - let controller = environment.controller let button = button.update( component: SolidRoundedButtonComponent( diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index b64edab58f..b487f1ea42 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -448,6 +448,34 @@ public class PremiumLimitDisplayComponent: Component { } view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize) } + + let activeValueSize = self.activeValueLabel.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.activeValue, + font: Font.semibold(15.0), + textColor: rightTextColor + ) + ) + ) + ), + environment: {}, + containerSize: availableSize + ) + let activeValueLabelFrame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize) + if let view = self.activeValueLabel.view { + if view.superview == nil { + self.container.addSubview(view) + + if component.invertProgress { + self.container.bringSubviewToFront(self.activeContainer) + } + } + view.frame = activeValueLabelFrame + } let activeTitleSize = self.activeTitleLabel.update( transition: .immediate, @@ -470,33 +498,12 @@ public class PremiumLimitDisplayComponent: Component { self.container.addSubview(view) } view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize) - } - - let activeValueSize = self.activeValueLabel.update( - transition: .immediate, - component: AnyComponent( - MultilineTextComponent( - text: .plain( - NSAttributedString( - string: component.activeValue, - font: Font.semibold(15.0), - textColor: rightTextColor - ) - ) - ) - ), - environment: {}, - containerSize: availableSize - ) - if let view = self.activeValueLabel.view { - if view.superview == nil { - self.container.addSubview(view) - - if component.invertProgress { - self.container.bringSubviewToFront(self.activeContainer) - } + + if view.frame.maxX > activeValueLabelFrame.minX - 8.0 { + view.alpha = 0.0 + } else { + view.alpha = 1.0 } - view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize) } } diff --git a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift index 4b12336fea..5a6b093777 100644 --- a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift +++ b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift @@ -116,7 +116,7 @@ private class StickerNode: ASDisplayNode { let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 240.0, height: 240.0)) - let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let pathPrefix = context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.resource.id)) animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width * 1.6), height: Int(fittedDimensions.height * 1.6), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .other, file: file, small: false, size: fittedDimensions)) @@ -133,7 +133,7 @@ private class StickerNode: ASDisplayNode { additionalAnimationNode = DirectAnimatedStickerNode() var pathPrefix: String? - pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(effect.resource.id) + pathPrefix = context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(effect.resource.id)) pathPrefix = nil additionalAnimationNode.setup(source: source, width: Int(fittedDimensions.width * 1.5), height: Int(fittedDimensions.height * 1.5), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) diff --git a/submodules/PresentationDataUtils/BUILD b/submodules/PresentationDataUtils/BUILD index b1861ea36d..2b33e0a691 100644 --- a/submodules/PresentationDataUtils/BUILD +++ b/submodules/PresentationDataUtils/BUILD @@ -12,6 +12,7 @@ swift_library( deps = [ "//submodules/Display:Display", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AlertUI:AlertUI", diff --git a/submodules/PresentationDataUtils/Sources/OpenUrl.swift b/submodules/PresentationDataUtils/Sources/OpenUrl.swift index aa2dc5d708..f9c4ba3a1f 100644 --- a/submodules/PresentationDataUtils/Sources/OpenUrl.swift +++ b/submodules/PresentationDataUtils/Sources/OpenUrl.swift @@ -1,7 +1,7 @@ import Foundation import Display import SwiftSignalKit -import Postbox +import TelegramCore import AccountContext import OverlayStatusController import UrlWhitelist @@ -9,7 +9,7 @@ import TelegramPresentationData import AlertComponent import UrlHandling -public func openUserGeneratedUrl(context: AccountContext, peerId: PeerId?, url: String, concealed: Bool, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, forceDark: Bool = false, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void, progress: Promise? = nil, alertDisplayUpdated: ((ViewController?) -> Void)? = nil) -> Disposable { +public func openUserGeneratedUrl(context: AccountContext, peerId: EnginePeer.Id?, url: String, concealed: Bool, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, forceDark: Bool = false, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void, progress: Promise? = nil, alertDisplayUpdated: ((ViewController?) -> Void)? = nil) -> Disposable { var concealed = concealed var presentationData = context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/PromptUI/BUILD b/submodules/PromptUI/BUILD index 80d83f292f..66de9a799b 100644 --- a/submodules/PromptUI/BUILD +++ b/submodules/PromptUI/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/AccountContext", "//submodules/TelegramPresentationData", diff --git a/submodules/PromptUI/Sources/PromptController.swift b/submodules/PromptUI/Sources/PromptController.swift index d359be83de..27f583cee8 100644 --- a/submodules/PromptUI/Sources/PromptController.swift +++ b/submodules/PromptUI/Sources/PromptController.swift @@ -3,7 +3,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import AccountContext diff --git a/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScanScreen.swift index ceab808e4a..3a54062f87 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, accentDisabledButtonColor: .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/ReactionSelectionNode/BUILD b/submodules/ReactionSelectionNode/BUILD index cb3ce76c7c..623c6ad991 100644 --- a/submodules/ReactionSelectionNode/BUILD +++ b/submodules/ReactionSelectionNode/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/TextFormat:TextFormat", diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 45045f3107..ebd644eea5 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -20,6 +20,7 @@ import EntityKeyboard import ComponentDisplayAdapters import AnimationCache import MultiAnimationRenderer +import DCTMultiAnimationRendererImpl import EmojiTextAttachmentView import TextFormat import GZip @@ -294,6 +295,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { var id: AnyHashable var version: Int var isPreset: Bool + var canLoadMore: Bool } private struct EmojiSearchState { @@ -424,6 +426,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { self.emojiSearchState.set(.single(self.emojiSearchStateValue)) } } + private var emojiSearchContext: EmojiSearchContext? private var emptyResultEmojis: [TelegramMediaFile] = [] private var stableEmptyResultEmoji: TelegramMediaFile? @@ -472,8 +475,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } return Signal { subscriber in let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file)).start() - let dataDisposable = (context.account.postbox.mediaBox.resourceData(file.resource) - |> filter(\.complete) + let dataDisposable = (context.engine.resources.data(resource: EngineMediaResource(file.resource)) + |> filter(\.isComplete) |> take(1)).start(next: { data in subscriber.putNext(data.path) subscriber.putCompletion() @@ -504,8 +507,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { self.reactionsLocked = reactionsLocked self.animationCache = animationCache - self.animationRenderer = MultiAnimationRendererImpl() - (self.animationRenderer as? MultiAnimationRendererImpl)?.useYuvA = context.sharedContext.immediateExperimentalUISettings.compressedEmojiCache + self.animationRenderer = DCTMultiAnimationRendererImpl() + (self.animationRenderer as? DCTMultiAnimationRendererImpl)?.useYuvA = context.sharedContext.immediateExperimentalUISettings.compressedEmojiCache self.backgroundMaskNode = ASDisplayNode() var backgroundGlassParams: ReactionContextBackgroundNode.GlassParams? @@ -688,7 +691,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } else { strongSelf.stableEmptyResultEmoji = nil } - emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : .active) + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : .active, canLoadMore: emojiSearchResult.canLoadMore) } else { strongSelf.stableEmptyResultEmoji = nil @@ -1812,17 +1815,20 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { switch query { case .none: + self.emojiSearchContext = nil self.emojiSearchDisposable.set(nil) - self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) case let .text(rawQuery, languageCode): let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) if query.isEmpty { + self.emojiSearchContext = nil self.emojiSearchDisposable.set(nil) - self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) } else { let context = self.context let isEmojiOnly = self.isEmojiOnly + self.emojiSearchContext = nil var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) if !languageCode.lowercased().hasPrefix("en") { @@ -1847,10 +1853,10 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } |> distinctUntilChanged - let resultSignal: Signal<[EmojiPagerContentComponent.ItemGroup], NoError> + let resultSignal: Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?), NoError> if self.isMessageEffects { resultSignal = signal - |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + |> mapToSignal { keywords -> Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?), NoError> in var allEmoticons: [String: String] = [:] for keyword in keywords { for emoticon in keyword.emoticons { @@ -1862,9 +1868,9 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { context.availableMessageEffects |> take(1), hasPremium |> take(1) ) - |> mapToSignal { availableMessageEffects, hasPremium -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + |> mapToSignal { availableMessageEffects, hasPremium -> Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?), NoError> in guard let availableMessageEffects else { - return .single([]) + return .single(([], false, false, nil)) } var filteredEffects: [AvailableMessageEffects.MessageEffect] = [] @@ -1971,7 +1977,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { ) } - return .single(allItemGroups) + return .single((allItemGroups, false, false, nil)) } } } else { @@ -1984,7 +1990,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { let localPacksSignal: Signal = context.engine.stickers.searchEmojiSets(query: query) resultSignal = signal - |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + |> mapToSignal { keywords -> Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?), NoError> in var allEmoticons: [String: String] = [:] for keyword in keywords { for emoticon in keyword.emoticons { @@ -2027,9 +2033,10 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { fillWithLoadingPlaceholders: false, items: items )) - return .single(resultGroups) + return .single((resultGroups, false, false, nil)) } else { - let remoteSignal = context.engine.stickers.searchEmoji(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode) + let emojiSearchContext = context.engine.stickers.emojiSearchContext(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode) + let remoteSignal = emojiSearchContext.state return combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1), @@ -2039,7 +2046,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { remoteSignal, localPacksSignal ) - |> map { view, availableReactions, hasPremium, foundPacks, foundEmoji, foundLocalPacks -> [EmojiPagerContentComponent.ItemGroup] in + |> map { view, availableReactions, hasPremium, foundPacks, foundEmoji, foundLocalPacks -> (groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?) in var result: [(String, TelegramMediaFile.Accessor?, String)] = [] var allEmoticons: [String: String] = [:] @@ -2182,7 +2189,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { )) } } - return resultGroups + return (resultGroups, foundEmoji.canLoadMore, foundEmoji.items.isEmpty && foundEmoji.isLoadingMore, emojiSearchContext) } } } @@ -2197,12 +2204,14 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + self.emojiSearchContext = result.searchContext + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.groups, id: AnyHashable(query), version: version, isPreset: false, canLoadMore: result.canLoadMore), isSearching: result.isSearching) version += 1 })) } case let .category(value): let context = self.context + self.emojiSearchContext = nil let resultSignal: Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> if self.isMessageEffects { let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) @@ -2413,11 +2422,11 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) version += 1 })) } @@ -2425,6 +2434,9 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { updateScrollingToItemGroup: { }, onScroll: {}, + loadMore: { [weak self] in + self?.emojiSearchContext?.loadMore() + }, chatPeerId: nil, peekBehavior: nil, customLayout: emojiContentLayout, @@ -2757,7 +2769,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } let additionalAnimationFile = additionalAnimation._parse() - additionalAnimationNodeValue.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: additionalAnimationFile.resource), width: Int(effectFrame.width * 2.0), height: Int(effectFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(additionalAnimationFile.resource.id))) + additionalAnimationNodeValue.setup(source: AnimatedStickerResourceSource(account: itemNode.context.account, resource: additionalAnimationFile.resource), width: Int(effectFrame.width * 2.0), height: Int(effectFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(additionalAnimationFile.resource.id)))) additionalAnimationNodeValue.frame = effectFrame additionalAnimationNodeValue.updateLayout(size: effectFrame.size) self.addSubnode(additionalAnimationNodeValue) @@ -3341,7 +3353,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { if let currentItemNode = currentItemNode { itemNode = currentItemNode } else { - let animationRenderer = MultiAnimationRendererImpl() + let animationRenderer = DCTMultiAnimationRendererImpl() itemNode = ReactionNode(context: context, theme: theme, item: reaction, icon: .none, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false, isLocked: false) } self.itemNode = itemNode @@ -3909,7 +3921,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { } } - additionalAnimationNodeValue.setup(source: AnimatedStickerResourceSource(account: context.account, resource: additionalAnimation.resource), width: Int(effectFrame.width * 2.0), height: Int(effectFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(additionalAnimation.resource.id))) + additionalAnimationNodeValue.setup(source: AnimatedStickerResourceSource(account: context.account, resource: additionalAnimation.resource), width: Int(effectFrame.width * 2.0), height: Int(effectFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(additionalAnimation.resource.id)))) additionalAnimationNodeValue.frame = effectFrame additionalAnimationNodeValue.updateLayout(size: effectFrame.size) self.addSubnode(additionalAnimationNodeValue) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 3ee76da2e9..65e1e35966 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import AppBundle @@ -239,11 +238,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { strongSelf.animateInAnimationNode = nil } - self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: item.appearAnimation._parse().resource)).start() - self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: item.stillAnimation._parse().resource)).start() - self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: item.listAnimation._parse().resource)).start() + self.fetchStickerDisposable = context.engine.resources.fetch(reference: .standalone(resource: item.appearAnimation._parse().resource), userLocation: .other, userContentType: .sticker).start() + self.fetchStickerDisposable = context.engine.resources.fetch(reference: .standalone(resource: item.stillAnimation._parse().resource), userLocation: .other, userContentType: .sticker).start() + self.fetchStickerDisposable = context.engine.resources.fetch(reference: .standalone(resource: item.listAnimation._parse().resource), userLocation: .other, userContentType: .sticker).start() if let applicationAnimation = item.applicationAnimation { - self.fetchFullAnimationDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: applicationAnimation._parse().resource)).start() + self.fetchFullAnimationDisposable = context.engine.resources.fetch(reference: .standalone(resource: applicationAnimation._parse().resource), userLocation: .other, userContentType: .sticker).start() } if self.isLocked { @@ -360,10 +359,10 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { if largeExpanded { let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation._parse().resource, isVideo: self.item.largeListAnimation.isVideoSticker || self.item.largeListAnimation.isVideoEmoji || self.item.largeListAnimation.isStaticSticker || self.item.largeListAnimation.isStaticEmoji) - animationNode.setup(source: source, width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.largeListAnimation._parse().resource.id))) + animationNode.setup(source: source, width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(self.item.largeListAnimation._parse().resource.id)))) } else { let source = AnimatedStickerResourceSource(account: self.context.account, resource: self.item.listAnimation._parse().resource, isVideo: self.item.listAnimation.isVideoSticker || self.item.listAnimation.isVideoEmoji || self.item.listAnimation.isVideoSticker || self.item.listAnimation.isStaticSticker || self.item.listAnimation.isStaticEmoji) - animationNode.setup(source: source, width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.listAnimation._parse().resource.id))) + animationNode.setup(source: source, width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(self.item.listAnimation._parse().resource.id)))) } animationNode.frame = expandedAnimationFrame animationNode.updateLayout(size: expandedAnimationFrame.size) @@ -447,7 +446,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { self.stillAnimationNode = stillAnimationNode self.addSubnode(stillAnimationNode) - stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation._parse().resource, isVideo: self.item.stillAnimation.isVideoEmoji || self.item.stillAnimation.isVideoSticker || self.item.stillAnimation.isStaticSticker || self.item.stillAnimation.isStaticEmoji), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: self.loopIdle ? .loop : .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation._parse().resource.id))) + stillAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation._parse().resource, isVideo: self.item.stillAnimation.isVideoEmoji || self.item.stillAnimation.isVideoSticker || self.item.stillAnimation.isStaticSticker || self.item.stillAnimation.isStaticEmoji), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: self.loopIdle ? .loop : .still(.start), mode: .direct(cachePathPrefix: self.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(self.item.stillAnimation._parse().resource.id)))) stillAnimationNode.position = animationFrame.center stillAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) stillAnimationNode.updateLayout(size: animationFrame.size) @@ -532,9 +531,9 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { self.staticAnimationNode.automaticallyLoadFirstFrame = true if !self.hasAppearAnimation { - self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation._parse().resource, isVideo: self.item.largeListAnimation.isVideoEmoji || self.item.largeListAnimation.isVideoSticker || self.item.largeListAnimation.isStaticSticker || self.item.largeListAnimation.isStaticEmoji), width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.largeListAnimation._parse().resource.id))) + self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation._parse().resource, isVideo: self.item.largeListAnimation.isVideoEmoji || self.item.largeListAnimation.isVideoSticker || self.item.largeListAnimation.isStaticSticker || self.item.largeListAnimation.isStaticEmoji), width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(self.item.largeListAnimation._parse().resource.id)))) } else { - self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation._parse().resource, isVideo: self.item.stillAnimation.isVideoEmoji || self.item.stillAnimation.isVideoSticker || self.item.stillAnimation.isStaticSticker || self.item.stillAnimation.isStaticEmoji), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation._parse().resource.id))) + self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation._parse().resource, isVideo: self.item.stillAnimation.isVideoEmoji || self.item.stillAnimation.isVideoSticker || self.item.stillAnimation.isStaticSticker || self.item.stillAnimation.isStaticEmoji), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(self.item.stillAnimation._parse().resource.id)))) } self.staticAnimationNode.position = animationFrame.center self.staticAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) @@ -547,7 +546,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { } if let animateInAnimationNode = self.animateInAnimationNode { - animateInAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.appearAnimation._parse().resource, isVideo: self.item.appearAnimation.isVideoEmoji || self.item.appearAnimation.isVideoSticker || self.item.appearAnimation.isStaticSticker || self.item.appearAnimation.isStaticEmoji), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.appearAnimation._parse().resource.id))) + animateInAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.appearAnimation._parse().resource, isVideo: self.item.appearAnimation.isVideoEmoji || self.item.appearAnimation.isVideoSticker || self.item.appearAnimation.isStaticSticker || self.item.appearAnimation.isStaticEmoji), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(self.item.appearAnimation._parse().resource.id)))) animateInAnimationNode.position = animationFrame.center animateInAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) animateInAnimationNode.updateLayout(size: animationFrame.size) diff --git a/submodules/SaveToCameraRoll/BUILD b/submodules/SaveToCameraRoll/BUILD index d7db544f62..f5410229c0 100644 --- a/submodules/SaveToCameraRoll/BUILD +++ b/submodules/SaveToCameraRoll/BUILD @@ -11,7 +11,6 @@ swift_library( ], deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/Display:Display", "//submodules/AccountContext:AccountContext", diff --git a/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift b/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift index dd92620f00..2f8b30b155 100644 --- a/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift +++ b/submodules/SaveToCameraRoll/Sources/SaveToCameraRoll.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import SwiftSignalKit -import Postbox import TelegramCore import Photos import Display @@ -12,11 +11,11 @@ import LegacyComponents public enum FetchMediaDataState { case progress(Float) - case data(MediaResourceData) + case data(EngineMediaResource.ResourceData) } -public func fetchMediaData(context: AccountContext, postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, mediaReference: AnyMediaReference, forceVideo: Bool = false) -> Signal<(FetchMediaDataState, Bool), NoError> { - var resource: MediaResource? +public func fetchMediaData(context: AccountContext, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, mediaReference: AnyMediaReference, forceVideo: Bool = false) -> Signal<(FetchMediaDataState, Bool), NoError> { + var resource: TelegramMediaResource? var isImage = true var fileExtension: String? var userContentType: MediaResourceUserContentType = .other @@ -53,11 +52,16 @@ public func fetchMediaData(context: AccountContext, postbox: Postbox, userLocati if let customUserContentType { userContentType = customUserContentType } - + if let resource = resource { + let engineResource = EngineMediaResource(resource) let fetchedData: Signal = Signal { subscriber in - let fetched = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: mediaReference.resourceReference(resource)).start() - let status = postbox.mediaBox.resourceStatus(resource).start(next: { status in + let fetched = context.engine.resources.fetch( + reference: mediaReference.resourceReference(resource), + userLocation: userLocation, + userContentType: userContentType + ).start() + let status = context.engine.resources.status(resource: engineResource).start(next: { status in switch status { case .Local: subscriber.putNext(.progress(1.0)) @@ -69,7 +73,11 @@ public func fetchMediaData(context: AccountContext, postbox: Postbox, userLocati subscriber.putNext(.progress(progress)) } }) - let data = postbox.mediaBox.resourceData(resource, pathExtension: fileExtension, option: .complete(waitUntilFetchStatus: true)).start(next: { next in + let data = context.engine.resources.data( + resource: engineResource, + pathExtension: fileExtension, + waitUntilFetchStatus: true + ).start(next: { next in subscriber.putNext(.data(next)) }, completed: { subscriber.putCompletion() @@ -89,61 +97,122 @@ public func fetchMediaData(context: AccountContext, postbox: Postbox, userLocati } } -public func saveToCameraRoll(context: AccountContext, postbox: Postbox, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, mediaReference: AnyMediaReference) -> Signal { - return fetchMediaData(context: context, postbox: postbox, userLocation: userLocation, customUserContentType: customUserContentType, mediaReference: mediaReference) - |> mapToSignal { state, isImage -> Signal in - switch state { +public func saveToCameraRoll(context: AccountContext, userLocation: MediaResourceUserLocation, customUserContentType: MediaResourceUserContentType? = nil, mediaReference: AnyMediaReference, video: AnyMediaReference? = nil) -> Signal { + let mediaData: Signal<(FetchMediaDataState, Bool), NoError> = fetchMediaData(context: context, userLocation: userLocation, customUserContentType: customUserContentType, mediaReference: mediaReference) + let videoData: Signal + if let video { + videoData = fetchMediaData(context: context, userLocation: userLocation, customUserContentType: customUserContentType, mediaReference: video) + |> map { state, _ in + return state + } + |> map(Optional.init) + } else { + videoData = .single(nil) + } + + return combineLatest( + queue: Queue.mainQueue(), + mediaData, + videoData + ) + |> mapToSignal { stateAndIsImage, videoStateAndIsImage -> Signal in + let isImage = stateAndIsImage.1 + var mainData: EngineMediaResource.ResourceData? + var videoData: EngineMediaResource.ResourceData? + var waitForVideo = false + if let videoState = videoStateAndIsImage { + switch videoState { + case let .progress(value): + return .single(value * 0.95) + case let .data(data): + videoData = data + } + switch stateAndIsImage.0 { + case let .progress(value): + return .single(0.95 + 0.05 * value) + case let .data(data): + mainData = data + } + waitForVideo = true + } else { + switch stateAndIsImage.0 { case let .progress(value): return .single(value) case let .data(data): - if data.complete { - return Signal { subscriber in - DeviceAccess.authorizeAccess(to: .mediaLibrary(.save), presentationData: context.sharedContext.currentPresentationData.with { $0 }, present: { c, a in - context.sharedContext.presentGlobalController(c, a) - }, openSettings: context.sharedContext.applicationBindings.openSettings, { authorized in - if !authorized { - subscriber.putCompletion() - return - } - - let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4" + mainData = data + } + } + if let mainData, mainData.isComplete, videoData != nil || !waitForVideo { + return Signal { subscriber in + DeviceAccess.authorizeAccess(to: .mediaLibrary(.save), presentationData: context.sharedContext.currentPresentationData.with { $0 }, present: { c, a in + context.sharedContext.presentGlobalController(c, a) + }, openSettings: context.sharedContext.applicationBindings.openSettings, { authorized in + if !authorized { + subscriber.putCompletion() + return + } + + let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4" + if isImage, let videoData, let imageData = try? Data(contentsOf: URL(fileURLWithPath: mainData.path)) { + let id = UUID().uuidString + + let jpegWithID = addAssetIdentifierToJPEG(imageData, assetIdentifier: id)! + let outputVideoURL = URL(fileURLWithPath: NSTemporaryDirectory() + "\(id).mov") + + try? FileManager.default.copyItem(atPath: videoData.path, toPath: tempVideoPath) + + addAssetIdentifierToVideo(inputURL: URL(fileURLWithPath: tempVideoPath), outputURL: outputVideoURL, assetIdentifier: id) { success in + guard success else { return } + PHPhotoLibrary.shared().performChanges({ - if isImage { - if let fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { - PHAssetCreationRequest.forAsset().addResource(with: .photo, data: fileData, options: nil) - } - } else { - if let _ = try? FileManager.default.copyItem(atPath: data.path, toPath: tempVideoPath) { - PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: tempVideoPath)) - } - } + let request = PHAssetCreationRequest.forAsset() + + request.addResource(with: .photo, data: jpegWithID, options: nil) + request.addResource(with: .pairedVideo, fileURL: outputVideoURL, options: nil) }, completionHandler: { _, error in - if let error = error { - print("\(error)") - } let _ = try? FileManager.default.removeItem(atPath: tempVideoPath) subscriber.putNext(1.0) subscriber.putCompletion() }) - }) - - return ActionDisposable { } + } else { + PHPhotoLibrary.shared().performChanges({ + if isImage { + if let imageData = try? Data(contentsOf: URL(fileURLWithPath: mainData.path)) { + PHAssetCreationRequest.forAsset().addResource(with: .photo, data: imageData, options: nil) + } + } else { + if let _ = try? FileManager.default.copyItem(atPath: mainData.path, toPath: tempVideoPath) { + PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: tempVideoPath)) + } + } + }, completionHandler: { _, error in + if let error { + print("\(error)") + } + let _ = try? FileManager.default.removeItem(atPath: tempVideoPath) + subscriber.putNext(1.0) + subscriber.putCompletion() + }) } - } else { - return .complete() + }) + + return ActionDisposable { } + } + } else { + return .complete() } } } -public func copyToPasteboard(context: AccountContext, postbox: Postbox, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference) -> Signal { - return fetchMediaData(context: context, postbox: postbox, userLocation: userLocation, mediaReference: mediaReference) +public func copyToPasteboard(context: AccountContext, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference) -> Signal { + return fetchMediaData(context: context, userLocation: userLocation, mediaReference: mediaReference) |> mapToSignal { state, isImage -> Signal in - if case let .data(data) = state, data.complete { + if case let .data(data) = state, data.isComplete { return Signal { subscriber in let pasteboard = UIPasteboard.general - + if mediaReference.media is TelegramMediaImage { if let fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: .mappedIfSafe) { pasteboard.setData(fileData, forPasteboardType: kUTTypeJPEG as String) @@ -151,7 +220,7 @@ public func copyToPasteboard(context: AccountContext, postbox: Postbox, userLoca } subscriber.putNext(Void()) subscriber.putCompletion() - + return EmptyDisposable } } else { @@ -160,3 +229,56 @@ public func copyToPasteboard(context: AccountContext, postbox: Postbox, userLoca } |> mapToSignal { _ -> Signal in return .complete() } } + +private func addAssetIdentifierToJPEG(_ imageData: Data, assetIdentifier: String) -> Data? { + guard let source = CGImageSourceCreateWithData(imageData as CFData, nil), let uti = CGImageSourceGetType(source), let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else { + return nil + } + + let mutableData = NSMutableData() + guard let destination = CGImageDestinationCreateWithData(mutableData, uti, 1, nil) else { + return nil + } + + var metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any] ?? [:] + + var maker = metadata[kCGImagePropertyMakerAppleDictionary as String] as? [String: Any] ?? [:] + maker["17"] = assetIdentifier + metadata[kCGImagePropertyMakerAppleDictionary as String] = maker + + CGImageDestinationAddImage(destination, cgImage, metadata as CFDictionary) + CGImageDestinationFinalize(destination) + + return mutableData as Data +} + +private func addAssetIdentifierToVideo(inputURL: URL, outputURL: URL, assetIdentifier: String, completion: @escaping (Bool) -> Void) { + let asset = AVAsset(url: inputURL) + + guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough) else { + completion(false) + return + } + + let identifierItem = AVMutableMetadataItem() + identifierItem.keySpace = .quickTimeMetadata + identifierItem.key = AVMetadataKey.quickTimeMetadataKeyContentIdentifier as NSString + identifierItem.value = assetIdentifier as NSString + + let stillImageTimeItem = AVMutableMetadataItem() + let keyStillImageTime = "com.apple.quicktime.still-image-time" + let keySpaceQuickTimeMetadata = "mdta" + stillImageTimeItem.key = keyStillImageTime as (NSCopying & NSObjectProtocol)? + stillImageTimeItem.keySpace = AVMetadataKeySpace(rawValue: keySpaceQuickTimeMetadata) + stillImageTimeItem.value = 0 as (NSCopying & NSObjectProtocol)? + stillImageTimeItem.dataType = "com.apple.metadata.datatype.int8" + + exportSession.outputURL = outputURL + exportSession.outputFileType = .mov + exportSession.metadata = [identifierItem, stillImageTimeItem] + exportSession.shouldOptimizeForNetworkUse = true + + exportSession.exportAsynchronously { + completion(exportSession.status == .completed) + } +} 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/SearchBarNode/Sources/SearchBarPlaceholderNode.swift b/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift index 2e8250db45..31bed130c4 100644 --- a/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarPlaceholderNode.swift @@ -54,6 +54,7 @@ public final class SearchBarPlaceholderContentView: UIView { let fieldStyle: SearchBarStyle let plainBackgroundView: UIImageView + let glassBackgroundContainerView: GlassBackgroundContainerView? let glassBackgroundView: GlassBackgroundView? private var fillBackgroundColor: UIColor private var foregroundColor: UIColor @@ -82,8 +83,10 @@ public final class SearchBarPlaceholderContentView: UIView { switch fieldStyle { case .legacy, .modern: + self.glassBackgroundContainerView = nil self.glassBackgroundView = nil case .inlineNavigation, .glass: + self.glassBackgroundContainerView = GlassBackgroundContainerView() self.glassBackgroundView = GlassBackgroundView() } @@ -111,8 +114,9 @@ public final class SearchBarPlaceholderContentView: UIView { self.plainBackgroundView.addSubview(self.plainIconNode.view) self.plainBackgroundView.addSubview(self.plainLabelNode.view) - if let glassBackgroundView = self.glassBackgroundView { - self.addSubview(glassBackgroundView) + if let glassBackgroundContainerView = self.glassBackgroundContainerView, let glassBackgroundView = self.glassBackgroundView { + self.addSubview(glassBackgroundContainerView) + glassBackgroundContainerView.contentView.addSubview(glassBackgroundView) glassBackgroundView.contentView.addSubview(self.iconNode.view) glassBackgroundView.contentView.addSubview(self.labelNode.view) @@ -300,9 +304,14 @@ public final class SearchBarPlaceholderContentView: UIView { transition.updateFrame(view: self.plainBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: params.constrainedSize.width, height: height))) } - if let glassBackgroundView = self.glassBackgroundView { - transition.updatePosition(layer: glassBackgroundView.layer, position: backgroundFrame.center) + if let glassBackgroundContainerView = self.glassBackgroundContainerView, let glassBackgroundView = self.glassBackgroundView { + + transition.updatePosition(layer: glassBackgroundContainerView.layer, position: backgroundFrame.center) + transition.updateBounds(layer: glassBackgroundContainerView.layer, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + + transition.updatePosition(layer: glassBackgroundView.layer, position: CGRect(origin: CGPoint(), size: backgroundFrame.size).center) transition.updateBounds(layer: glassBackgroundView.layer, bounds: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + var backgroundAlpha: CGFloat = 1.0 if backgroundFrame.height < 16.0 { backgroundAlpha = max(0.0, min(1.0, backgroundFrame.height / 16.0)) @@ -310,9 +319,10 @@ public final class SearchBarPlaceholderContentView: UIView { if !params.isActive { backgroundAlpha = 0.0 } - ComponentTransition(transition).setAlpha(view: glassBackgroundView, alpha: backgroundAlpha) + ComponentTransition(transition).setAlpha(view: glassBackgroundContainerView, alpha: backgroundAlpha) let isDark = params.backgroundColor.hsb.b < 0.5 if params.isActive { + glassBackgroundContainerView.update(size: backgroundFrame.size, isDark: isDark, transition: ComponentTransition(transition)) glassBackgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, isDark: isDark, tintColor: .init(kind: params.preferClearGlass ? .clear : .panel), isInteractive: true, transition: ComponentTransition(transition)) } diff --git a/submodules/SearchPeerMembers/BUILD b/submodules/SearchPeerMembers/BUILD index 2dfde4f9ac..ac0a190373 100644 --- a/submodules/SearchPeerMembers/BUILD +++ b/submodules/SearchPeerMembers/BUILD @@ -11,7 +11,6 @@ swift_library( ], deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/StringTransliteration:StringTransliteration", diff --git a/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift b/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift index 5581cdb9d3..5b282a77d0 100644 --- a/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift +++ b/submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift @@ -27,13 +27,13 @@ public func searchPeerMembers(context: AccountContext, peerId: EnginePeer.Id, ch return nil } if normalizedQuery.isEmpty { - return EnginePeer(participant.peer) + return participant.peer } else { if participant.peer.indexName.matchesByTokens(normalizedQuery) || participant.peer.indexName.matchesByTokens(transformedQuery) { - return EnginePeer(participant.peer) + return participant.peer } if let addressName = participant.peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) || addressName.lowercased().hasPrefix(transformedQuery) { - return EnginePeer(participant.peer) + return participant.peer } return nil @@ -58,7 +58,7 @@ public func searchPeerMembers(context: AccountContext, peerId: EnginePeer.Id, ch if participant.peer.isDeleted { return nil } - return EnginePeer(participant.peer) + return participant.peer }, true)) } }) @@ -73,7 +73,7 @@ public func searchPeerMembers(context: AccountContext, peerId: EnginePeer.Id, ch if participant.peer.isDeleted { return nil } - return EnginePeer(participant.peer) + return participant.peer }, true)) } }) diff --git a/submodules/SearchUI/Sources/SearchDisplayController.swift b/submodules/SearchUI/Sources/SearchDisplayController.swift index 53cdd002ce..518b730a05 100644 --- a/submodules/SearchUI/Sources/SearchDisplayController.swift +++ b/submodules/SearchUI/Sources/SearchDisplayController.swift @@ -312,6 +312,11 @@ public final class SearchDisplayController { if let searchBar = self.searchBar { searchBar.deactivate(clear: false) } + if !self.searchBarIsExternal, let searchBar = self.searchBar { + searchBar.isUserInteractionEnabled = false + } + self.backgroundNode.isUserInteractionEnabled = false + self.contentNode.isUserInteractionEnabled = false if !self.searchBarIsExternal, let searchBar = self.searchBar { if let placeholder = placeholder { diff --git a/submodules/SelectablePeerNode/BUILD b/submodules/SelectablePeerNode/BUILD index b13c4278f4..add7a0f4d8 100644 --- a/submodules/SelectablePeerNode/BUILD +++ b/submodules/SelectablePeerNode/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/TelegramCore:TelegramCore", - "//submodules/Postbox:Postbox", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift index f4ba6fd9fc..13eee759a7 100644 --- a/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift +++ b/submodules/SelectablePeerNode/Sources/SelectablePeerNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import TelegramCore -import Postbox import SwiftSignalKit import TelegramPresentationData import AvatarNode @@ -108,7 +107,7 @@ public final class SelectablePeerNode: ASDisplayNode { didSet { if !self.theme.isEqual(to: oldValue) { if let peer = self.peer, let mainPeer = peer.chatMainPeer { - self.textNode.attributedText = NSAttributedString(string: mainPeer.debugDisplayTitle, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : (peer.peerId.namespace == Namespaces.Peer.SecretChat ? self.theme.secretTextColor : self.theme.textColor), paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: mainPeer.debugDisplayTitle, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : (peer.peerId.isSecretChat ? self.theme.secretTextColor : self.theme.textColor), paragraphAlignment: .center) } } } @@ -159,8 +158,7 @@ public final class SelectablePeerNode: ASDisplayNode { public func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, requiresStars: Int64? = nil, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) { self.setup( accountPeerId: context.account.peerId, - postbox: context.account.postbox, - network: context.account.network, + stateManager: context.account.stateManager, energyUsageSettings: context.sharedContext.energyUsageSettings, contentSettings: context.currentContentSettings.with { $0 }, animationCache: context.animationCache, @@ -182,7 +180,7 @@ public final class SelectablePeerNode: ASDisplayNode { ) } - public func setupStoryRepost(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool, storyMode: StoryMode) { + public func setupStoryRepost(accountPeerId: EnginePeer.Id, stateManager: AccountStateManager, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool, storyMode: StoryMode) { self.peer = nil let title: String @@ -202,14 +200,14 @@ public final class SelectablePeerNode: ASDisplayNode { self.textNode.maximumNumberOfLines = 2 self.textNode.attributedText = NSAttributedString(string: title, font: textFont, textColor: self.theme.textColor, paragraphAlignment: .center) - self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: postbox, network: network, contentSettings: ContentSettings.default, theme: theme, peer: nil, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: .round, synchronousLoad: synchronousLoad) + self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: stateManager.postbox, network: stateManager.network, contentSettings: ContentSettings.default, theme: theme, peer: nil, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: .round, synchronousLoad: synchronousLoad) if case .repostIcon = overrideImage { self.avatarNode.playRepostAnimation() } } - public func setup(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, requiresStars: Int64? = nil, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) { + public func setup(accountPeerId: EnginePeer.Id, stateManager: AccountStateManager, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, requiresStars: Int64? = nil, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) { let isFirstTime = self.peer == nil self.peer = peer guard let mainPeer = peer.chatOrMonoforumMainPeer else { @@ -222,7 +220,7 @@ public final class SelectablePeerNode: ASDisplayNode { if requiresPremiumForMessaging { defaultColor = self.theme.textColor.withMultipliedAlpha(0.4) } else { - defaultColor = peer.peerId.namespace == Namespaces.Peer.SecretChat ? self.theme.secretTextColor : self.theme.textColor + defaultColor = peer.peerId.isSecretChat ? self.theme.secretTextColor : self.theme.textColor } var isForum = false @@ -256,7 +254,7 @@ public final class SelectablePeerNode: ASDisplayNode { } else { clipStyle = .round } - self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: postbox, network: network, contentSettings: contentSettings, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad) + self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: stateManager.postbox, network: stateManager.network, contentSettings: contentSettings, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad) if let requiresStars { let avatarBadgeOutline: UIImageView @@ -373,7 +371,7 @@ public final class SelectablePeerNode: ASDisplayNode { let iconSize = self.iconView.update( transition: .easeInOut(duration: 0.2), component: AnyComponent(EmojiStatusComponent( - postbox: postbox, + postbox: stateManager.postbox, energyUsageSettings: energyUsageSettings, resolveInlineStickers: resolveInlineStickers, animationCache: animationCache, @@ -404,7 +402,7 @@ public final class SelectablePeerNode: ASDisplayNode { self.currentSelected = selected if let attributedText = self.textNode.attributedText { - self.textNode.attributedText = NSAttributedString(string: attributedText.string, font: textFont, textColor: selected ? self.theme.selectedTextColor : (self.peer?.peerId.namespace == Namespaces.Peer.SecretChat ? self.theme.secretTextColor : self.theme.textColor), paragraphAlignment: .center) + self.textNode.attributedText = NSAttributedString(string: attributedText.string, font: textFont, textColor: selected ? self.theme.selectedTextColor : ((self.peer?.peerId.isSecretChat ?? false) ? self.theme.secretTextColor : self.theme.textColor), paragraphAlignment: .center) } var isForum = false diff --git a/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift b/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift index 1daffea084..60bf80644e 100644 --- a/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift +++ b/submodules/SettingsUI/Sources/ChangePhoneNumberCodeController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import ItemListUI diff --git a/submodules/SettingsUI/Sources/Data and Storage/IntentsSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/IntentsSettingsController.swift index 4ea381815b..cea27124d2 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/IntentsSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/IntentsSettingsController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift index 606fbfaf99..1e3adc0e28 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 @@ -260,7 +260,11 @@ private func proxySettingsControllerEntries(theme: PresentationTheme, strings: P entries.append(.serversHeader(theme, strings.SocksProxySetup_SavedProxies)) entries.append(.addServer(theme, strings.SocksProxySetup_AddProxy, state.editing)) var index = 0 + var existingServers = Set() for server in proxySettings.servers { + if !existingServers.insert(server).inserted { + continue + } let status: ProxyServerStatus = statuses[server] ?? .checking let displayStatus: DisplayProxyServerStatus if proxySettings.enabled && server == proxySettings.activeServer { @@ -301,7 +305,7 @@ private func proxySettingsControllerEntries(theme: PresentationTheme, strings: P entries.append(.server(index, theme, strings, server, server == proxySettings.activeServer, displayStatus, ProxySettingsServerItemEditing(editable: true, editing: state.editing, revealed: state.revealedServer == server), proxySettings.enabled)) index += 1 } - if !proxySettings.servers.isEmpty { + if !existingServers.isEmpty { entries.append(.shareProxyList(theme, strings.SocksProxySetup_ShareProxyList)) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift index 5f35c78e37..560bd3fce2 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift @@ -10,14 +10,13 @@ import PresentationDataUtils import AccountContext import UrlEscaping import UrlHandling -import ShareController private func shareLink(for server: ProxyServerSettings) -> String { var link: String switch server.connection { case let .mtp(secret): let secret = MTProxySecret.parseData(secret)?.serializeToString() ?? "" - link = "https://t.me/proxy?server=\(server.host)&port=\(server.port)" + link = "tg://proxy?server=\(server.host)&port=\(server.port)" link += "&secret=\(secret.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" case let .socks5(username, password): link = "https://t.me/socks?server=\(server.host)&port=\(server.port)" @@ -333,10 +332,10 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: let signal = combineLatest(updatedPresentationData, statePromise.get()) |> deliverOnMainQueue |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in - let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + let leftNavigationButton = ItemListNavigationButton(content: .text("___close"), style: .regular, enabled: true, action: { dismissImpl?() }) - let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: { + let rightNavigationButton = ItemListNavigationButton(content: .text("___done"), style: .bold, enabled: state.isComplete, action: { if let proxyServerSettings = proxyServerSettings(with: state) { let _ = (updateProxySettingsInteractively(accountManager: accountManager, { settings in var settings = settings @@ -348,8 +347,11 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: } } } else { - settings.servers.append(proxyServerSettings) - if settings.servers.count == 1 { + let wasEmpty = settings.servers.isEmpty + if !settings.servers.contains(proxyServerSettings) { + settings.servers.append(proxyServerSettings) + } + if wasEmpty && settings.servers.count == 1 { settings.activeServer = proxyServerSettings } } @@ -389,4 +391,3 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: return controller } - 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/Data and Storage/StorageUsageExceptionsScreen.swift b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift index 7be3cda903..1569334394 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/StorageUsageExceptionsScreen.swift @@ -170,10 +170,10 @@ private enum StorageUsageExceptionsEntry: ItemListNodeEntry { if peer.peer.id == arguments.context.account.peerId { title = presentationData.strings.DialogList_SavedMessages } else { - title = EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: .firstLast) + title = peer.peer.displayTitle(strings: presentationData.strings, displayOrder: .firstLast) } - return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: EnginePeer(peer.peer), title: title, enabled: true, titleFont: .bold, label: optionText, labelStyle: .text, additionalDetailLabel: additionalDetailLabel, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { + return ItemListDisclosureItem(presentationData: presentationData, icon: nil, context: arguments.context, iconPeer: peer.peer, title: title, enabled: true, titleFont: .bold, label: optionText, labelStyle: .text, additionalDetailLabel: additionalDetailLabel, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: { arguments.openPeerMenu(peer.peer.id, value) }, tag: StorageUsageExceptionsEntryTag.peer(peer.peer.id)) } @@ -285,7 +285,7 @@ public func storageUsageExceptionsScreen( continue } - result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value)) + result.append((peer: FoundPeer(peer: EnginePeer(peer), subscribers: subscriberCount), value: value)) } return result.sorted(by: { lhs, rhs in diff --git a/submodules/SettingsUI/Sources/Data and Storage/VoiceCallDataSavingController.swift b/submodules/SettingsUI/Sources/Data and Storage/VoiceCallDataSavingController.swift index 2c85919d39..53db4bc1c9 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/VoiceCallDataSavingController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/VoiceCallDataSavingController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift index 85fd21740e..5998909cc5 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/WebBrowserSettingsController.swift @@ -465,7 +465,7 @@ private func fetchDomainExceptionInfo(context: AccountContext, url: String) -> S var image: TelegramMediaImage? if let imageData, let parsedImage = UIImage(data: imageData) { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: imageData) + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: imageData) image = TelegramMediaImage( imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: [ diff --git a/submodules/SettingsUI/Sources/DeleteAccountPeersItem.swift b/submodules/SettingsUI/Sources/DeleteAccountPeersItem.swift index 7ccbca5f40..9f8db50830 100644 --- a/submodules/SettingsUI/Sources/DeleteAccountPeersItem.swift +++ b/submodules/SettingsUI/Sources/DeleteAccountPeersItem.swift @@ -48,8 +48,7 @@ private struct PeersEntry: Comparable, Identifiable { strings: self.strings, mode: .list(compact: true), accountPeerId: context.account.peerId, - postbox: context.account.postbox, - network: context.account.network, + stateManager: context.account.stateManager, energyUsageSettings: context.sharedContext.energyUsageSettings, contentSettings: context.currentContentSettings.with { $0 }, animationCache: context.animationCache, diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift index 4a60f7bc7f..631c325844 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import SwiftSignalKit import TelegramCore import TelegramPresentationData 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/Language Selection/TranslatonSettingsController.swift b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift index 5da4449a77..1c5976d031 100644 --- a/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/TranslatonSettingsController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift index 4caf2e68c7..555deef539 100644 --- a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift +++ b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionControllerNode.swift @@ -791,10 +791,10 @@ final class NotificationExceptionsControllerNode: ViewControllerTracingNode { self?.view.endEditing(true) } - let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) - + let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.globalNotifications)) + let previousEntriesHolder = Atomic<([NotificationExceptionEntry], PresentationTheme, PresentationStrings)?>(value: nil) - + self.listDisposable = (combineLatest(context.sharedContext.presentationData, statePromise.get(), preferences, context.engine.peers.notificationSoundList()) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, prefs, notificationSoundList in let entries = notificationsExceptionEntries(presentationData: presentationData, notificationSoundList: notificationSoundList, state: state) let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) @@ -1045,10 +1045,10 @@ private final class NotificationExceptionsSearchContainerNode: SearchDisplayCont } - let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) - + let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.globalNotifications)) + let previousEntriesHolder = Atomic<([NotificationExceptionEntry], PresentationTheme, PresentationStrings)?>(value: nil) - + let stateQuery = stateAndPeers |> map { stateAndPeers -> String? in return stateAndPeers.1 @@ -1056,7 +1056,7 @@ private final class NotificationExceptionsSearchContainerNode: SearchDisplayCont |> distinctUntilChanged let searchSignal = stateQuery - |> mapToSignal { query -> Signal<(PresentationData, NotificationSoundList?, (NotificationExceptionState, String?), PreferencesView, [EngineRenderedPeer]), NoError> in + |> mapToSignal { query -> Signal<(PresentationData, NotificationSoundList?, (NotificationExceptionState, String?), PreferencesEntry?, [EngineRenderedPeer]), NoError> in var contactsSignal: Signal<[EngineRenderedPeer], NoError> = .single([]) if let query = query { contactsSignal = context.account.postbox.searchPeers(query: query) diff --git a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift index c2ff00d359..2759bfb669 100644 --- a/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift +++ b/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptions.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import AccountContext diff --git a/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift b/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift index 0109d7090e..3dc9a053c2 100644 --- a/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift +++ b/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift @@ -779,7 +779,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions }) let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings]) - let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) + let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.globalNotifications)) let exceptionsSignal = Signal.single(exceptionsList) |> then(context.engine.peers.notificationExceptionsList() |> map(Optional.init)) @@ -794,7 +794,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions for (key, value) in list.settings { if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId { if value.storySettings != defaultStorySettings { - stories[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer)) + stories[key] = NotificationExceptionWrapper(settings: value, peer: peer) } switch value.muteState { @@ -805,24 +805,24 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions default: switch key.namespace { case Namespaces.Peer.CloudUser: - users[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer)) + users[key] = NotificationExceptionWrapper(settings: value, peer: peer) default: - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(peer) = peer, case .broadcast = peer.info { channels[key] = NotificationExceptionWrapper(settings: value, peer: .channel(peer)) } else { - groups[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer)) + groups[key] = NotificationExceptionWrapper(settings: value, peer: peer) } } } default: switch key.namespace { case Namespaces.Peer.CloudUser: - users[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer)) + users[key] = NotificationExceptionWrapper(settings: value, peer: peer) default: - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(peer) = peer, case .broadcast = peer.info { channels[key] = NotificationExceptionWrapper(settings: value, peer: .channel(peer)) } else { - groups[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer)) + groups[key] = NotificationExceptionWrapper(settings: value, peer: peer) } } } @@ -858,7 +858,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions |> map { presentationData, sharedData, view, exceptions, authorizationStatus, warningSuppressed, hasMoreThanOneAccount -> (ItemListControllerState, (ItemListNodeState, Any)) in let viewSettings: GlobalNotificationSettingsSet - if let settings = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) { + if let settings = view?.get(GlobalNotificationSettings.self) { viewSettings = settings.effective } else { viewSettings = GlobalNotificationSettingsSet.defaultSettings diff --git a/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift b/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift index 984938fda7..2456dbe02e 100644 --- a/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift +++ b/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift @@ -1007,7 +1007,7 @@ public func notificationsPeerCategoryController(context: AccountContext, categor }) let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings]) - let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) + let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.globalNotifications)) var automaticData: Signal<([EnginePeer], [EnginePeer.Id: EnginePeer.NotificationSettings]), NoError> = .single(([], [:])) if case .stories = category { @@ -1039,7 +1039,7 @@ public func notificationsPeerCategoryController(context: AccountContext, categor let signal = combineLatest(context.sharedContext.presentationData, context.engine.peers.notificationSoundList(), sharedData, preferences, statePromise.get(), automaticData) |> map { presentationData, notificationSoundList, sharedData, view, state, automaticData -> (ItemListControllerState, (ItemListNodeState, Any)) in let viewSettings: GlobalNotificationSettingsSet - if let settings = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) { + if let settings = view?.get(GlobalNotificationSettings.self) { viewSettings = settings.effective } else { viewSettings = GlobalNotificationSettingsSet.defaultSettings diff --git a/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift index 0e01ee3e27..bcde257640 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift @@ -18,9 +18,9 @@ private final class BlockedPeersControllerArguments { let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let addPeer: () -> Void let removePeer: (PeerId) -> Void - let openPeer: (Peer) -> Void - - init(context: AccountContext, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (Peer) -> Void) { + let openPeer: (EnginePeer) -> Void + + init(context: AccountContext, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (EnginePeer) -> Void) { self.context = context self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.addPeer = addPeer @@ -41,7 +41,7 @@ private enum BlockedPeersEntryStableId: Hashable { private enum BlockedPeersEntry: ItemListNodeEntry { case add(PresentationTheme, String) - case peerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, ItemListPeerItemEditing, Bool) + case peerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, EnginePeer, ItemListPeerItemEditing, Bool) var section: ItemListSectionId { switch self { @@ -86,7 +86,7 @@ private enum BlockedPeersEntry: ItemListNodeEntry { if lhsNameOrder != rhsNameOrder { return false } - if !lhsPeer.isEqual(rhsPeer) { + if lhsPeer != rhsPeer { return false } if lhsEditing != rhsEditing { @@ -132,7 +132,7 @@ private enum BlockedPeersEntry: ItemListNodeEntry { arguments.removePeer(peer.id) })]) - return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer), presence: nil, text: .none, label: .none, editing: editing, revealOptions: revealOptions, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .none, label: .none, editing: editing, revealOptions: revealOptions, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { arguments.openPeer(peer) }, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) @@ -195,7 +195,7 @@ private func blockedPeersControllerEntries(presentationData: PresentationData, s var index: Int32 = 0 for peer in blockedPeersState.peers { - entries.append(.peerItem(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, peer.peer!, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.peerId == state.peerIdWithRevealedOptions), state.removingPeerId != peer.peerId)) + entries.append(.peerItem(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, EnginePeer(peer.peer!), ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.peerId == state.peerIdWithRevealedOptions), state.removingPeerId != peer.peerId)) index += 1 } } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift index d0b5427be0..e96125f1e7 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift @@ -313,7 +313,7 @@ func createPasswordController(context: AccountContext, createPasswordContext: Cr } if emailAlert { - presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: presentationData.strings.TwoStepAuth_EmailSkip, action: { + presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: presentationData.strings.TwoStepAuth_EmailSkip, action: { saveImpl() })]), nil) } else { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift index 9d290f5328..3a217663c8 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences @@ -547,11 +546,11 @@ public func dataPrivacyController(context: AccountContext, focusOnItemTag: DataP } |> distinctUntilChanged - let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), context.account.postbox.preferencesView(keys: [PreferencesKeys.contactsSettings]), context.engine.peers.recentPeers(), hasBotSettings) + let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), context.sharedContext.accountManager.noticeEntry(key: ApplicationSpecificNotice.secretChatLinkPreviewsKey()), context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.contactSynchronizationSettings]), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.contactsSettings)), context.engine.peers.recentPeers(), hasBotSettings) |> map { presentationData, state, noticeView, sharedData, preferences, recentPeers, hasBotSettings -> (ItemListControllerState, (ItemListNodeState, Any)) in let secretChatLinkPreviews = noticeView.value.flatMap({ ApplicationSpecificNotice.getSecretChatLinkPreviews($0) }) - - let settings: ContactsSettings = preferences.values[PreferencesKeys.contactsSettings]?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings + + let settings: ContactsSettings = preferences?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings let synchronizeDeviceContacts: Bool = settings.synchronizeContacts diff --git a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift index 7ccb1d9560..66aaae7efe 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift @@ -303,7 +303,7 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32, }, openCustomValue: { let currentValue = stateValue.with({ $0 }).updatedValue - let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .autoremove, currentTime: currentValue == 0 ? nil : currentValue, dismissByTapOutside: true, completion: { value in + let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .autoremove, currentTime: currentValue == 0 ? nil : currentValue, completion: { value in updateValue(value) }) presentControllerImpl?(controller, nil) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift index 0d423438f9..f390f231b6 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift @@ -340,7 +340,7 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP } } - updatedPeers[peer.id] = SelectivePrivacyPeer(peer: peer._asPeer(), participantCount: participantCount) + updatedPeers[peer.id] = SelectivePrivacyPeer(peer: peer, participantCount: participantCount) } } return updatedPeers diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index e024a7d320..a835c6f668 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -1021,12 +1021,12 @@ public func privacyAndSecurityController( let privacySignal = privacySettingsPromise.get() |> take(1) - let callsSignal = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.voiceCallSettings]), context.account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration])) + let callsSignal = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.voiceCallSettings]), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.voipConfiguration))) |> take(1) |> map { sharedData, view -> (VoiceCallSettings, VoipConfiguration) in let voiceCallSettings: VoiceCallSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.voiceCallSettings]?.get(VoiceCallSettings.self) ?? .defaultSettings - let voipConfiguration = view.values[PreferencesKeys.voipConfiguration]?.get(VoipConfiguration.self) ?? .defaultValue - + let voipConfiguration = view?.get(VoipConfiguration.self) ?? .defaultValue + return (voiceCallSettings, voipConfiguration) } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift index b6c12d70df..7f309d72a7 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyIntroController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import SwiftSignalKit import TelegramCore import TelegramPresentationData diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift index 1dc5be3814..39380763fc 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/ItemListWebsiteItem.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences @@ -36,7 +35,7 @@ final class ItemListWebsiteItem: ListViewItem, ItemListItem { let dateTimeFormat: PresentationDateTimeFormat let nameDisplayOrder: PresentationPersonNameOrder let website: WebAuthorization - let peer: Peer? + let peer: EnginePeer? let enabled: Bool let editing: Bool let revealed: Bool @@ -45,7 +44,7 @@ final class ItemListWebsiteItem: ListViewItem, ItemListItem { let removeSession: (Int64) -> Void let action: (() -> Void)? - init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, action: (() -> Void)?) { + init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: EnginePeer?, enabled: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, action: (() -> Void)?) { self.context = context self.presentationData = presentationData self.systemStyle = systemStyle @@ -229,8 +228,8 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { let rightInset: CGFloat = params.rightInset - if let user = item.peer as? TelegramUser { - titleAttributedString = NSAttributedString(string: EnginePeer(user).displayTitle(strings: item.presentationData.strings, displayOrder: item.nameDisplayOrder), font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) + if let peer = item.peer, case .user = peer { + titleAttributedString = NSAttributedString(string: peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.nameDisplayOrder), font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) } var appString = "" @@ -332,7 +331,7 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode { } if let peer = item.peer { - strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: EnginePeer(peer), authorOfMessage: nil, overrideImage: nil, emptyColor: nil, clipStyle: .none, synchronousLoad: false) + strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, authorOfMessage: nil, overrideImage: nil, emptyColor: nil, clipStyle: .none, synchronousLoad: false) } let revealOffset = strongSelf.revealOffset diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift index 0730050b3b..a2cd1fcee2 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift @@ -21,7 +21,7 @@ private final class RecentSessionsControllerArguments { let terminateOtherSessions: () -> Void let openSession: (RecentAccountSession) -> Void - let openWebSession: (WebAuthorization, Peer?) -> Void + let openWebSession: (WebAuthorization, EnginePeer?) -> Void let removeWebSession: (Int64) -> Void let terminateAllWebSessions: () -> Void @@ -34,7 +34,7 @@ private final class RecentSessionsControllerArguments { let openDesktopLink: () -> Void let openWebLink: () -> Void - init(context: AccountContext, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, terminateOtherSessions: @escaping () -> Void, openSession: @escaping (RecentAccountSession) -> Void, openWebSession: @escaping (WebAuthorization, Peer?) -> Void, removeWebSession: @escaping (Int64) -> Void, terminateAllWebSessions: @escaping () -> Void, addDevice: @escaping () -> Void, openOtherAppsUrl: @escaping () -> Void, setupAuthorizationTTL: @escaping () -> Void, openDesktopLink: @escaping () -> Void, openWebLink: @escaping () -> Void) { + init(context: AccountContext, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, terminateOtherSessions: @escaping () -> Void, openSession: @escaping (RecentAccountSession) -> Void, openWebSession: @escaping (WebAuthorization, EnginePeer?) -> Void, removeWebSession: @escaping (Int64) -> Void, terminateAllWebSessions: @escaping () -> Void, addDevice: @escaping () -> Void, openOtherAppsUrl: @escaping () -> Void, setupAuthorizationTTL: @escaping () -> Void, openDesktopLink: @escaping () -> Void, openWebLink: @escaping () -> Void) { self.context = context self.setSessionIdWithRevealedOptions = setSessionIdWithRevealedOptions self.removeSession = removeSession @@ -117,7 +117,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry { case otherSessionsHeader(SortIndex, String) case addDevice(SortIndex, String) case session(index: Int32, sortIndex: SortIndex, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool) - case website(index: Int32, sortIndex: SortIndex, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool) + case website(index: Int32, sortIndex: SortIndex, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: EnginePeer?, enabled: Bool, editing: Bool, revealed: Bool) case devicesInfo(SortIndex, String) case ttlHeader(SortIndex, String) case ttlTimeout(SortIndex, String, String) @@ -296,7 +296,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry { return false } case let .website(lhsIndex, lhsSortIndex, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsWebsite, lhsPeer, lhsEnabled, lhsEditing, lhsRevealed): - if case let .website(rhsIndex, rhsSortIndex, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsWebsite, rhsPeer, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsSortIndex == rhsSortIndex, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameOrder == rhsNameOrder, lhsWebsite == rhsWebsite, arePeersEqual(lhsPeer, rhsPeer), lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { + if case let .website(rhsIndex, rhsSortIndex, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsWebsite, rhsPeer, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsSortIndex == rhsSortIndex, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameOrder == rhsNameOrder, lhsWebsite == rhsWebsite, lhsPeer == rhsPeer, lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { return true } else { return false @@ -562,7 +562,7 @@ private func recentSessionsControllerEntries(presentationData: PresentationData, let website = websites[i] if !existingSessionIds.contains(website.hash) { existingSessionIds.insert(website.hash) - entries.append(.website(index: Int32(i), sortIndex: SortIndex(section: 1, item: i), strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, website: website, peer: peers[website.botId], enabled: state.removingSessionId != website.hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == website.hash)) + entries.append(.website(index: Int32(i), sortIndex: SortIndex(section: 1, item: i), strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, website: website, peer: peers[website.botId].flatMap(EnginePeer.init), enabled: state.removingSessionId != website.hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == website.hash)) } } } @@ -727,7 +727,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont }) presentControllerImpl?(controller, nil) }, openWebSession: { session, peer in - let controller = RecentSessionScreen(context: context, subject: .website(session, peer.flatMap(EnginePeer.init)), updateAcceptSecretChats: { _ in }, updateAcceptIncomingCalls: { _ in }, remove: { completion in + let controller = RecentSessionScreen(context: context, subject: .website(session, peer), updateAcceptSecretChats: { _ in }, updateAcceptIncomingCalls: { _ in }, remove: { completion in removeWebSessionImpl(session.hash) completion() }) @@ -811,9 +811,9 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont let previousMode = Atomic(value: .sessions) - let enableQRLogin = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + let enableQRLogin = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) |> map { view -> Bool in - guard let appConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) else { + guard let appConfiguration = view?.get(AppConfiguration.self) else { return false } guard let data = appConfiguration.data, let enableQR = data["qr_login_camera"] as? Bool, enableQR else { diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift index 8c9b007e92..cce4170d0c 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift @@ -1478,7 +1478,7 @@ public func selectivePrivacySettingsController( } } - updatedPeers[peer.id] = SelectivePrivacyPeer(peer: peer._asPeer(), participantCount: participantCount) + updatedPeers[peer.id] = SelectivePrivacyPeer(peer: peer, participantCount: participantCount) } } return updatedPeers diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift index 6a758582ad..6395e90a2f 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsPeersController.swift @@ -475,7 +475,7 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri } } - updatedPeers.append(SelectivePrivacyPeer(peer: peer._asPeer(), participantCount: participantCount)) + updatedPeers.append(SelectivePrivacyPeer(peer: peer, participantCount: participantCount)) } } return updatedPeers @@ -506,7 +506,7 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri })) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, openPeer: { peer in - guard let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + guard let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { return } pushControllerImpl?(controller) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift index 97c2ab8bca..3a3f2e8053 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import ItemListUI diff --git a/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift b/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift index 0463ea0ad6..116728780b 100644 --- a/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift +++ b/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift @@ -384,7 +384,7 @@ public func reactionNotificationSettingsController( } ) - let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) + let preferences = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.globalNotifications)) let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, @@ -394,7 +394,7 @@ public func reactionNotificationSettingsController( ) |> map { presentationData, notificationSoundList, preferencesView, state -> (ItemListControllerState, (ItemListNodeState, Any)) in let viewSettings: GlobalNotificationSettingsSet - if let settings = preferencesView.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) { + if let settings = preferencesView?.get(GlobalNotificationSettings.self) { viewSettings = settings.effective } else { viewSettings = GlobalNotificationSettingsSet.defaultSettings diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift index 8c3e310f23..8d1bd13a19 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift index 65d87903ed..49c5dee31d 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import AsyncDisplayKit -import Postbox import Display import SwiftSignalKit import TelegramCore diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index 92cf70cf2f..560caa33be 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -968,7 +968,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc present: { context, _, present in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer?._asPeer() else { + guard let peer = peer else { return } let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false) @@ -977,7 +977,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc } ) ) - + //TODO:fix items.append( SettingsSearchableItem( @@ -986,7 +986,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc present: { context, _, present in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer?._asPeer() else { + guard let peer = peer else { return } let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false) @@ -1017,7 +1017,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc present: { context, _, present in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer?._asPeer() else { + guard let peer = peer else { return } let controller = context.sharedContext.makePeerInfoController( @@ -1034,7 +1034,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc } ) ) - + items.append( SettingsSearchableItem( id: "my-profile/edit", @@ -1043,7 +1043,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc present: { context, _, present in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer?._asPeer() else { + guard let peer = peer else { return } let controller = context.sharedContext.makePeerInfoController( @@ -1056,7 +1056,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc requestsContext: nil ) present(.push, controller) - + Queue.mainQueue().justDispatch { if let controller = controller as? PeerInfoScreen { controller.activateEdit() @@ -1077,7 +1077,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc present: { context, _, present in let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { peer in - guard let peer = peer?._asPeer() else { + guard let peer = peer else { return } let controller = context.sharedContext.makePeerInfoController( @@ -1551,7 +1551,7 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob for (key, value) in list.settings { if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId { if value.storySettings != defaultStorySettings { - stories[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer)) + stories[key] = NotificationExceptionWrapper(settings: value, peer: peer) } switch value.muteState { @@ -1562,24 +1562,24 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob default: switch key.namespace { case Namespaces.Peer.CloudUser: - users[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer)) + users[key] = NotificationExceptionWrapper(settings: value, peer: peer) default: - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(peer) = peer, case .broadcast = peer.info { channels[key] = NotificationExceptionWrapper(settings: value, peer: .channel(peer)) } else { - groups[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer)) + groups[key] = NotificationExceptionWrapper(settings: value, peer: peer) } } } default: switch key.namespace { case Namespaces.Peer.CloudUser: - users[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer)) + users[key] = NotificationExceptionWrapper(settings: value, peer: peer) default: - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(peer) = peer, case .broadcast = peer.info { channels[key] = NotificationExceptionWrapper(settings: value, peer: .channel(peer)) } else { - groups[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer)) + groups[key] = NotificationExceptionWrapper(settings: value, peer: peer) } } } @@ -2073,11 +2073,11 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac } let callsSignal: Signal<(VoiceCallSettings, VoipConfiguration)?, NoError> if case .voiceCalls = kind { - callsSignal = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.voiceCallSettings]), context.account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration])) + callsSignal = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.voiceCallSettings]), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.voipConfiguration))) |> take(1) |> map { sharedData, view -> (VoiceCallSettings, VoipConfiguration)? in let voiceCallSettings: VoiceCallSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.voiceCallSettings]?.get(VoiceCallSettings.self) ?? .defaultSettings - let voipConfiguration = view.values[PreferencesKeys.voipConfiguration]?.get(VoipConfiguration.self) ?? .defaultValue + let voipConfiguration = view?.get(VoipConfiguration.self) ?? .defaultValue return (voiceCallSettings, voipConfiguration) } } else { @@ -4317,11 +4317,11 @@ func settingsSearchableItems( return accountsAndPeers.1.count + 1 < maximumNumberOfAccounts } - let notificationSettings = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) + let notificationSettings = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.globalNotifications)) |> take(1) |> map { view -> GlobalNotificationSettingsSet in let viewSettings: GlobalNotificationSettingsSet - if let settings = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) { + if let settings = view?.get(GlobalNotificationSettings.self) { viewSettings = settings.effective } else { viewSettings = GlobalNotificationSettingsSet.defaultSettings diff --git a/submodules/SettingsUI/Sources/SettingsController.swift b/submodules/SettingsUI/Sources/SettingsController.swift index 2acd061193..1b27fbd354 100644 --- a/submodules/SettingsUI/Sources/SettingsController.swift +++ b/submodules/SettingsUI/Sources/SettingsController.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import SwiftSignalKit -import Postbox import TelegramCore import AccountContext import PasswordSetupUI diff --git a/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift index c028dbaacf..91570aadf6 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 @@ -258,7 +257,7 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv if forceEdit { updateState { - $0.withUpdatedEditing(true) + $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(Set()) } } @@ -429,7 +428,8 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv } }, togglePackSelected: { packId in updateState { state in - if var selectedPackIds = state.selectedPackIds { + if state.editing { + var selectedPackIds = state.selectedPackIds ?? Set() if selectedPackIds.contains(packId) { selectedPackIds.remove(packId) } else { @@ -555,7 +555,7 @@ public func archivedStickerPacksController(context: AccountContext, mode: Archiv presentControllerImpl?(actionSheet, nil) }), .init(title: presentationData.strings.StickerPacks_ActionShare, isEnabled: selectedCount > 0, action: { updateState { - $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(nil) + $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(Set()) } var packNames: [String] = [] @@ -565,7 +565,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/FeaturedStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/FeaturedStickerPacksController.swift index e0baafe727..c62b4f08c5 100644 --- a/submodules/SettingsUI/Sources/Stickers/FeaturedStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/FeaturedStickerPacksController.swift @@ -172,7 +172,7 @@ public func featuredStickerPacksController(context: AccountContext) -> ViewContr presentStickerPackController?(info) }, addPack: { info in let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal in switch result { case let .result(info, items, installed): if installed { @@ -186,9 +186,18 @@ public func featuredStickerPacksController(context: AccountContext) -> ViewContr break } return .complete() - } |> deliverOnMainQueue).start() + } |> deliverOnMainQueue).startStandalone() }) + actionsDisposable.add((context.account.stateManager.installedStickerPacksArchivedEvents + |> deliverOnMainQueue).startStandalone(next: { count in + if count == 0 { + return + } + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + presentControllerImpl?(textAlertController(context: context, updatedPresentationData: nil, title: nil, text: presentationData.strings.ArchivedPacksAlert_Title, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + })) + let stickerPacks = Promise() stickerPacks.set(context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index c8ae43fa25..59dda715e0 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 @@ -657,7 +657,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta if focusOnItemTag == InstalledStickerPacksEntryTag.edit { updateState { - $0.withUpdatedEditing(true) + $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(Set()) } } @@ -836,7 +836,8 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta }).start() }, togglePackSelected: { packId in updateState { state in - if var selectedPackIds = state.selectedPackIds { + if state.editing { + var selectedPackIds = state.selectedPackIds ?? Set() if selectedPackIds.contains(packId) { selectedPackIds.remove(packId) } else { @@ -863,7 +864,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta if installed { return .complete() } else { - return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) + return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) |> map { _ in return Void() } } case .fetching: break @@ -887,11 +888,11 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta archivedPromise.set(.single(archivedPacks) |> then(context.engine.stickers.archivedStickerPacks() |> map(Optional.init))) quickReaction = combineLatest( context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), - context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings]) + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.reactionSettings)) ) |> map { peer, preferencesView -> MessageReaction.Reaction? in let reactionSettings: ReactionSettings - if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) { + if let entry = preferencesView, let value = entry.get(ReactionSettings.self) { reactionSettings = value } else { reactionSettings = .default @@ -989,7 +990,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta if case .modal = mode { updateState { - $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(nil) + $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(Set()) } } else { updateState { @@ -1020,7 +1021,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta if case .modal = mode { updateState { - $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(nil) + $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(Set()) } } else { updateState { @@ -1046,7 +1047,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta }), .init(title: presentationData.strings.StickerPacks_ActionShare, isEnabled: selectedCount > 0, action: { if case .modal = mode { updateState { - $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(nil) + $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(Set()) } } else { updateState { @@ -1063,7 +1064,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/Terms of Service/TermsOfServiceController.swift b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift index 2317be18fb..b85aaeea8c 100644 --- a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift +++ b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift @@ -118,7 +118,7 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont } strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.PrivacyPolicy_Decline, text: text, actions: [TextAlertAction(type: .destructiveAction, title: declineTitle, action: { self?.decline() - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { })], actionLayout: .vertical), in: .window(.root)) }, rightAction: { [weak self] in guard let strongSelf = self else { diff --git a/submodules/SettingsUI/Sources/ThemePickerController.swift b/submodules/SettingsUI/Sources/ThemePickerController.swift index 24c0d62467..8be71db7d0 100644 --- a/submodules/SettingsUI/Sources/ThemePickerController.swift +++ b/submodules/SettingsUI/Sources/ThemePickerController.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences @@ -13,7 +12,7 @@ import AlertUI import PresentationDataUtils import MediaResources import WallpaperResources -import ShareController + import AccountContext import ContextUI import UndoUI @@ -623,11 +622,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 +874,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) }) }))) @@ -1232,7 +1229,7 @@ public func themePickerController(context: AccountContext, focusOnItemTag: Theme wallpaperSignal = cachedWallpaper(account: context.account, slug: file.slug, settings: colorWallpaper.settings) |> mapToSignal { cachedWallpaper in if let wallpaper = cachedWallpaper?.wallpaper, case let .file(file) = wallpaper { - let _ = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start() + let _ = context.engine.resources.fetch(reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource), userLocation: .other, userContentType: .other).start() return .single(wallpaper) diff --git a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift index aae20daead..ad9cc43121 100644 --- a/submodules/SettingsUI/Sources/ThemePickerGridItem.swift +++ b/submodules/SettingsUI/Sources/ThemePickerGridItem.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import MergeLists @@ -269,7 +268,7 @@ private final class ThemeGridThemeItemIconNode : ASDisplayNode { } self.animatedStickerNode = animatedStickerNode self.emojiContainerNode.insertSubnode(animatedStickerNode, belowSubnode: self.placeholderNode) - let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let pathPrefix = item.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.resource.id)) animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix)) animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) @@ -277,7 +276,7 @@ private final class ThemeGridThemeItemIconNode : ASDisplayNode { animatedStickerNode.autoplay = true animatedStickerNode.visibility = true - self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + self.stickerFetchedDisposable.set(item.context.engine.resources.fetch(reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource), userLocation: .other, userContentType: .other).start()) let thumbnailDimensions = PixelDimensions(width: 512, height: 512) self.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: thumbnailDimensions.cgSize) diff --git a/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift b/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift index d4ac7c9af5..062d165781 100644 --- a/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift +++ b/submodules/SettingsUI/Sources/Themes/CustomWallpaperPicker.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import SwiftSignalKit -import Postbox import TelegramCore import LegacyComponents import TelegramUIPreferences diff --git a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift index e00bdb0fee..33a60120ef 100644 --- a/submodules/SettingsUI/Sources/Themes/EditThemeController.swift +++ b/submodules/SettingsUI/Sources/Themes/EditThemeController.swift @@ -334,7 +334,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll case let .edit(info): hasSettings = info.theme.settings != nil settingsPromise.set(.single(info.theme.settings?.first)) - if let file = info.theme.file, let path = context.sharedContext.accountManager.mediaBox.completedResourcePath(file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let theme = makePresentationTheme(data: data, resolvedWallpaper: info.resolvedWallpaper) { + if let file = info.theme.file, let path = context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(file.resource)), let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let theme = makePresentationTheme(data: data, resolvedWallpaper: info.resolvedWallpaper) { if case let .file(file) = theme.chat.defaultWallpaper, file.id == 0 { previewThemePromise.set(cachedWallpaper(account: context.account, slug: file.slug, settings: file.settings) |> map ({ wallpaper -> PresentationTheme in @@ -415,7 +415,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll guard complete, let fullSizeData = fullSizeData else { return .complete() } - context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true) + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: fullSizeData, synchronous: true) return .single(wallpaper.wallpaper) } } else { @@ -575,8 +575,8 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll let themeThumbnailData: Data? if let theme = theme, let themeString = encodePresentationTheme(theme), let data = themeString.data(using: .utf8) { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) themeResource = resource themeData = data @@ -612,14 +612,14 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll let resource = file.file.resource var data: Data? - if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + if let path = context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { data = maybeData - } else if let path = context.sharedContext.accountManager.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + } else if let path = context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { data = maybeData } if let data = data { - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) prepare = .complete() } else { prepare = .complete() @@ -637,7 +637,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start() let _ = (updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in if let resource = resultTheme.file?.resource, let data = themeData { - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) } let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: context.account.id)) @@ -671,7 +671,7 @@ public func editThemeController(context: AccountContext, mode: EditThemeControll let _ = applyTheme(accountManager: context.sharedContext.accountManager, account: context.account, theme: resultTheme).start() let _ = (updatePresentationThemeSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in if let resource = resultTheme.file?.resource, let data = themeData { - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) } let themeReference: PresentationThemeReference = .cloud(PresentationCloudTheme(theme: resultTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: context.account.id)) diff --git a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift index 7e5f7fded1..9d92ab1692 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeAutoNightSettingsController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewController.swift index 26504db15a..c9f21d9af2 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 @@ -268,8 +267,8 @@ public final class ThemePreviewController: ViewController { case .media: if let strings = encodePresentationTheme(previewTheme), let data = strings.data(using: .utf8) { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) theme = .single(.local(PresentationLocalTheme(title: previewTheme.name.string, resource: resource, resolvedWallpaper: nil))) } else { theme = .single(.builtin(.dayClassic)) @@ -335,7 +334,7 @@ public final class ThemePreviewController: ViewController { return .single((.local(updatedTheme), true)) } if case let .result(theme) = result, let file = theme.file { - context.sharedContext.accountManager.mediaBox.moveResourceData(from: info.resource.id, to: file.resource.id) + context.sharedContext.accountManager.resources.moveResourceData(from: EngineMediaResource.Id(info.resource.id), to: EngineMediaResource.Id(file.resource.id)) return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: theme.isCreator ? context.account.id : nil)), true)) } else { return .complete() @@ -354,7 +353,7 @@ public final class ThemePreviewController: ViewController { return .single((.local(updatedTheme), true)) } if case let .result(updatedTheme) = result, let file = updatedTheme.file { - context.sharedContext.accountManager.mediaBox.moveResourceData(from: info.resource.id, to: file.resource.id) + context.sharedContext.accountManager.resources.moveResourceData(from: EngineMediaResource.Id(info.resource.id), to: EngineMediaResource.Id(file.resource.id)) return .single((.cloud(PresentationCloudTheme(theme: updatedTheme, resolvedWallpaper: resolvedWallpaper, creatorAccountId: updatedTheme.isCreator ? context.account.id : nil)), true)) } else { return .complete() @@ -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/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index c3bd534963..bd2b74b03d 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -265,16 +265,16 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { } strongSelf.remoteChatBackgroundNode.setSignal(signal) - strongSelf.fetchDisposable.set(fetchedMediaResource(mediaBox: context.sharedContext.accountManager.mediaBox, userLocation: .other, userContentType: .other, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start()) - - let account = strongSelf.context.account - let statusSignal = strongSelf.context.sharedContext.accountManager.mediaBox.resourceStatus(file.file.resource) + strongSelf.fetchDisposable.set(context.sharedContext.accountManager.resources.fetch(reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource), userLocation: .other, userContentType: .other).start()) + + let engine = strongSelf.context.engine + let statusSignal = strongSelf.context.sharedContext.accountManager.resources.status(resource: EngineMediaResource(file.file.resource)) |> take(1) - |> mapToSignal { status -> Signal in + |> mapToSignal { status -> Signal in if case .Local = status { return .single(status) } else { - return account.postbox.mediaBox.resourceStatus(file.file.resource) + return engine.resources.status(resource: EngineMediaResource(file.file.resource)) } } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 5e35e60c0d..60575fb189 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences @@ -13,7 +12,7 @@ import AlertUI import PresentationDataUtils import MediaResources import WallpaperResources -import ShareController + import AccountContext import ContextUI import UndoUI @@ -425,7 +424,7 @@ private func themeSettingsControllerEntries( var authorName = presentationData.strings.Appearance_PreviewReplyAuthor if let accountPeer { nameColor = accountPeer.nameColor ?? .preset(.blue) - if accountPeer._asPeer().hasCustomNameColor { + if accountPeer.hasCustomNameColor { authorName = accountPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder) } profileColor = accountPeer.effectiveProfileColor @@ -776,11 +775,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 +1025,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) }) }))) @@ -1325,7 +1322,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The wallpaperSignal = cachedWallpaper(account: context.account, slug: file.slug, settings: colorWallpaper.settings) |> mapToSignal { cachedWallpaper in if let wallpaper = cachedWallpaper?.wallpaper, case let .file(file) = wallpaper { - let _ = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start() + let _ = context.engine.resources.fetch(reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource), userLocation: .other, userContentType: .other).start() return .single(wallpaper) diff --git a/submodules/SettingsUI/Sources/UsernameSetupController.swift b/submodules/SettingsUI/Sources/UsernameSetupController.swift index 9af8ac6442..24f5af80ab 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 @@ -469,10 +468,17 @@ public func usernameSetupController(context: AccountContext, mode: UsernameSetup })) } }, shareLink: { - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> take(1) |> deliverOnMainQueue).start(next: { peer in - if let user = peer as? TelegramUser, user.botInfo != nil { + if case let .user(user) = peer, user.botInfo != nil { context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://fragment.com/", forceExternal: true, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) } else { var currentAddressName: String = peer.addressName ?? "" @@ -484,11 +490,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..d45899e01c 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,57 +297,20 @@ 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 - - public init(account: ShareControllerAccountContext, peer: Peer) { + public let peer: EnginePeer + + public init(account: ShareControllerAccountContext, peer: EnginePeer) { self.account = account self.peer = peer } - + public static func ==(lhs: ShareControllerSwitchableAccount, rhs: ShareControllerSwitchableAccount) -> Bool { if lhs.account !== rhs.account { return false } - if !arePeersEqual(lhs.peer, rhs.peer) { + if lhs.peer != rhs.peer { return false } return true @@ -1212,11 +1145,10 @@ public final class ShareController: ViewController { for info in strongSelf.switchableAccounts { items.append(ActionSheetPeerItem( accountPeerId: info.account.accountPeerId, - postbox: info.account.stateManager.postbox, - network: info.account.stateManager.network, + stateManager: info.account.stateManager, contentSettings: info.account.contentSettings, - peer: EnginePeer(info.peer), - title: EnginePeer(info.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), + peer: info.peer, + title: info.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), isSelected: info.account.accountId == strongSelf.currentContext.accountId, strings: presentationData.strings, theme: presentationData.theme, @@ -2467,10 +2399,9 @@ public final class ShareController: ViewController { } let context = accountContext.context - let postbox = self.currentContext.stateManager.postbox let signals: [Signal] = messages.compactMap { message -> Signal? in if let media = message.media.first { - return SaveToCameraRoll.saveToCameraRoll(context: context, postbox: postbox, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: media)) + return SaveToCameraRoll.saveToCameraRoll(context: context, userLocation: .peer(message.id.peerId), mediaReference: .message(message: MessageReference(message), media: media)) } else { return nil } @@ -2496,7 +2427,7 @@ public final class ShareController: ViewController { let context = accountContext.context let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations.map({ $0.representation }), immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) - self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: .standalone(media: media)) |> map(Optional.init), dismissImmediately: true, completion: {}) + self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, userLocation: .other, mediaReference: .standalone(media: media)) |> map(Optional.init), dismissImmediately: true, completion: {}) } private func saveToCameraRoll(mediaReference: AnyMediaReference, completion: (() -> Void)?) { @@ -2505,7 +2436,7 @@ public final class ShareController: ViewController { } let context = accountContext.context - self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: mediaReference) |> map(Optional.init), dismissImmediately: completion == nil, completion: completion ?? {}) + self.controllerNode.transitionToProgressWithValue(signal: SaveToCameraRoll.saveToCameraRoll(context: context, userLocation: .other, mediaReference: mediaReference) |> map(Optional.init), dismissImmediately: completion == nil, completion: completion ?? {}) } private func switchToAccount(account: ShareControllerAccountContext, animateIn: Bool) { @@ -2563,7 +2494,7 @@ public final class ShareController: ViewController { for entry in view.0.entries.reversed() { switch entry { case let .MessageEntry(entryData): - if let peer = entryData.renderedPeer.peers[entryData.renderedPeer.peerId], peer.id != accountPeer.id, canSendMessagesToPeer(peer) { + if let peer = entryData.renderedPeer.peers[entryData.renderedPeer.peerId], peer.id != accountPeer.id, canSendMessagesToPeer(EnginePeer(peer)) { peers.append(EngineRenderedPeer(entryData.renderedPeer)) if let user = peer as? TelegramUser, user.flags.contains(.requirePremium) || user.flags.contains(.requireStars) { possiblePremiumRequiredPeers.insert(user.id) diff --git a/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift b/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift index f9ee2a4670..1389f89037 100644 --- a/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift +++ b/submodules/ShareController/Sources/ShareControllerPeerGridItem.swift @@ -236,8 +236,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { let resolveInlineStickers = context.resolveInlineStickers self.peerNode.setup( accountPeerId: context.accountPeerId, - postbox: context.stateManager.postbox, - network: context.stateManager.network, + stateManager: context.stateManager, energyUsageSettings: environment.energyUsageSettings, contentSettings: context.contentSettings, animationCache: context.animationCache, @@ -272,8 +271,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode { } self.peerNode.setupStoryRepost( accountPeerId: context.accountPeerId, - postbox: context.stateManager.postbox, - network: context.stateManager.network, + stateManager: context.stateManager, theme: theme, strings: strings, synchronousLoad: synchronousLoad, diff --git a/submodules/ShareController/Sources/ShareControllerRecentPeersGridItem.swift b/submodules/ShareController/Sources/ShareControllerRecentPeersGridItem.swift index 074eb85846..a0c5f9a07c 100644 --- a/submodules/ShareController/Sources/ShareControllerRecentPeersGridItem.swift +++ b/submodules/ShareController/Sources/ShareControllerRecentPeersGridItem.swift @@ -63,8 +63,7 @@ final class ShareControllerRecentPeersGridItemNode: GridItemNode { } else { peersNode = ChatListSearchRecentPeersNode( accountPeerId: context.accountPeerId, - postbox: context.stateManager.postbox, - network: context.stateManager.network, + stateManager: context.stateManager, energyUsageSettings: environment.energyUsageSettings, contentSettings: context.contentSettings, animationCache: context.animationCache, diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 63caa2459c..1bc44a39f9 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -232,7 +232,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { network: context.stateManager.network, contentSettings: context.contentSettings, theme: theme, - peer: EnginePeer(info.peer), + peer: info.peer, emptyColor: nil, synchronousLoad: false ) @@ -373,7 +373,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.contentOffsetUpdated = f } - private func calculateMetrics(size: CGSize, additionalBottomInset: CGFloat) -> (topInset: CGFloat, itemWidth: CGFloat) { + private func calculateMetrics(size: CGSize, additionalBottomInset: CGFloat, isEmbedded: Bool) -> (topInset: CGFloat, itemWidth: CGFloat) { let itemCount = self.entries.count let itemInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0) @@ -394,7 +394,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { } let initiallyRevealedRowCount = min(minimallyRevealedRowCount, CGFloat(rowCount)) - let gridTopInset = max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0 - additionalBottomInset) + let gridTopInset = isEmbedded ? 136.0 : max(0.0, size.height - floor(initiallyRevealedRowCount * itemWidth) - 14.0 - additionalBottomInset) return (gridTopInset, itemWidth) } @@ -569,10 +569,15 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { } } + var isEmbedded = false func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { let firstLayout = self.validLayout == nil self.validLayout = (size, bottomInset) + self.contentTitleNode.isHidden = self.isEmbedded + self.contentSubtitleNode.isHidden = self.isEmbedded + self.searchButtonNode.isHidden = self.isEmbedded + let gridLayoutTransition: ContainedViewLayoutTransition if firstLayout { gridLayoutTransition = .immediate @@ -582,7 +587,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.overrideGridOffsetTransition = nil } - let (gridTopInset, itemWidth) = self.calculateMetrics(size: size, additionalBottomInset: bottomInset) + let (gridTopInset, itemWidth) = self.calculateMetrics(size: size, additionalBottomInset: bottomInset, isEmbedded: self.isEmbedded) var scrollToItem: GridNodeScrollToItem? if let ensurePeerVisibleOnLayout = self.ensurePeerVisibleOnLayout { @@ -680,8 +685,8 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.contentTitleNode.isHidden = true self.contentSubtitleNode.isHidden = true } else { - self.contentTitleNode.isHidden = false - self.contentSubtitleNode.isHidden = false + self.contentTitleNode.isHidden = self.isEmbedded + self.contentSubtitleNode.isHidden = self.isEmbedded var subtitleText = self.strings.ShareMenu_SelectChats if !self.controllerInteraction.selectedPeers.isEmpty { diff --git a/submodules/ShareController/Sources/ShareSearchContainerNode.swift b/submodules/ShareController/Sources/ShareSearchContainerNode.swift index ee3b966d15..faef506e64 100644 --- a/submodules/ShareController/Sources/ShareSearchContainerNode.swift +++ b/submodules/ShareController/Sources/ShareSearchContainerNode.swift @@ -286,7 +286,16 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { let foundItems = combineLatest(self.searchQuery.get(), self.themePromise.get()) |> mapToSignal { query, theme -> Signal<([ShareSearchPeerEntry]?, Bool), NoError> in if !query.isEmpty { - let accountPeer = context.stateManager.postbox.loadedPeerWithId(context.accountPeerId) |> take(1) + let accountPeer = context.engineData.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.accountPeerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + |> map { $0._asPeer() } + |> take(1) let foundLocalPeers = context.stateManager.postbox.searchPeers(query: query.lowercased()) let foundRemotePeers: Signal<([FoundPeer], [FoundPeer], Bool), NoError> = .single(([], [], true)) |> then( @@ -326,12 +335,12 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { } for peer in foundPeers.foundRemotePeers.0 { - if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) { + if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) { result.insert(user.id) } } for peer in foundPeers.foundRemotePeers.1 { - if let user = peer.peer as? TelegramUser, user.flags.contains(.requirePremium) { + if case let .user(user) = peer.peer, user.flags.contains(.requirePremium) { result.insert(user.id) } } @@ -372,7 +381,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { for renderedPeer in foundLocalPeers { if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != accountPeer.id { - if !existingPeerIds.contains(renderedPeer.peerId) && canSendMessagesToPeer(peer) { + if !existingPeerIds.contains(renderedPeer.peerId) && canSendMessagesToPeer(EnginePeer(peer)) { existingPeerIds.insert(renderedPeer.peerId) entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(renderedPeer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false)) index += 1 @@ -392,16 +401,16 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) { existingPeerIds.insert(peer.id) - entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(foundPeer.peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false)) + entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: foundPeer.peer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false)) index += 1 } } - + for foundPeer in foundRemotePeers.1 { let peer = foundPeer.peer if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) { existingPeerIds.insert(peer.id) - entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: EnginePeer(peer)), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: true)) + entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: peer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: true)) index += 1 } } @@ -465,8 +474,8 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { } var index = 0 for (peer, requiresPremiumForMessaging) in recentPeerList { - if let mainPeer = peer.peers[peer.peerId], canSendMessagesToPeer(mainPeer._asPeer()) { - recentItemList.append(.peer(index: index, theme: theme, peer: mainPeer, associatedPeer: mainPeer._asPeer().associatedPeerId.flatMap { peer.peers[$0] }, presence: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: nil, strings: strings)) + if let mainPeer = peer.peers[peer.peerId], canSendMessagesToPeer(mainPeer) { + recentItemList.append(.peer(index: index, theme: theme, peer: mainPeer, associatedPeer: mainPeer.associatedPeerId.flatMap { peer.peers[$0] }, presence: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: nil, strings: strings)) index += 1 } } diff --git a/submodules/ShareItems/Impl/Sources/TGItemProviderSignals.m b/submodules/ShareItems/Impl/Sources/TGItemProviderSignals.m index 92aeb2c25e..7989e9583e 100644 --- a/submodules/ShareItems/Impl/Sources/TGItemProviderSignals.m +++ b/submodules/ShareItems/Impl/Sources/TGItemProviderSignals.m @@ -184,13 +184,13 @@ __unused static CGSize TGFitSize(CGSize size, CGSize maxSize) { if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) { [itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:imageOptions completionHandler:^(id _Nullable item, NSError * _Null_unspecified error) { if (error != nil && ![(NSObject *)item respondsToSelector:@selector(CGImage)] && ![(NSObject *)item respondsToSelector:@selector(absoluteString)]) { - [itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeData options:nil completionHandler:^(UIImage *image, NSError *error) + [itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeData options:nil completionHandler:^(NSData *data, NSError *error) { if (error != nil) [subscriber putError:nil]; else { - [subscriber putNext:@{@"image": image}]; + [subscriber putNext:@{@"data": data}]; [subscriber putCompletion]; } }]; @@ -263,13 +263,13 @@ __unused static CGSize TGFitSize(CGSize size, CGSize maxSize) { } }]; } else { - [itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeData options:nil completionHandler:^(UIImage *image, NSError *error) + [itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeData options:nil completionHandler:^(NSData *data, NSError *error) { if (error != nil) [subscriber putError:nil]; else { - [subscriber putNext:@{@"image": image}]; + [subscriber putNext:@{@"data": data}]; [subscriber putCompletion]; } }]; diff --git a/submodules/ShareItems/Sources/ShareItems.swift b/submodules/ShareItems/Sources/ShareItems.swift index 0b8208a0d2..689da92243 100644 --- a/submodules/ShareItems/Sources/ShareItems.swift +++ b/submodules/ShareItems/Sources/ShareItems.swift @@ -478,6 +478,7 @@ public func sentShareItems(accountPeerId: PeerId, postbox: Postbox, network: Net var mediaMessageCount = 0 var consumedText = false + var captionAssigned = false for item in items { switch item { case let .text(text): @@ -493,11 +494,22 @@ public func sentShareItems(accountPeerId: PeerId, postbox: Postbox, network: Net case let .media(media): switch media { case let .media(reference): + let captionText: String + if !captionAssigned { + if let file = reference.media as? TelegramMediaFile, file.isInstantVideo { + captionText = "" + } else { + captionText = additionalText + captionAssigned = true + } + } else { + captionText = "" + } var message = StandaloneSendEnqueueMessage( content: .arbitraryMedia( media: reference, text: StandaloneSendEnqueueMessage.Text( - string: additionalText, + string: captionText, entities: [] ) ), diff --git a/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift index 7353213b80..9dc246a920 100644 --- a/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift +++ b/submodules/SoftwareVideo/Sources/SoftwareVideoLayerFrameManager.swift @@ -1,246 +1,5 @@ import Foundation -import UIKit -import Postbox -import TelegramCore import SwiftSignalKit -import CoreMedia -import UniversalMediaPlayer -import AVFoundation public let softwareVideoApplyQueue = Queue() public let softwareVideoWorkers = ThreadPool(threadCount: 3, threadPriority: 0.2) -private var nextWorker = 0 - -public final class SoftwareVideoLayerFrameManager { - private let fetchDisposable: Disposable - private var dataDisposable = MetaDisposable() - private let source = Atomic(value: nil) - private let hintVP9: Bool - - private var baseTimestamp: Double? - private var frames: [MediaTrackFrame] = [] - private var minPts: CMTime? - private var maxPts: CMTime? - - private let account: Account - private let resource: MediaResource - private let secondaryResource: MediaResource? - private let queue: ThreadPoolQueue - private let layerHolder: SampleBufferLayer? - private weak var layer: AVSampleBufferDisplayLayer? - - private var rotationAngle: CGFloat = 0.0 - private var aspect: CGFloat = 1.0 - - private var layerRotationAngleAndAspect: (CGFloat, CGFloat)? - - private var didStart = false - public var started: () -> Void = { } - - public init(account: Account, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference, layerHolder: SampleBufferLayer?, layer: AVSampleBufferDisplayLayer? = nil, hintVP9: Bool = false) { - var resource = fileReference.media.resource - var secondaryResource: MediaResource? - for attribute in fileReference.media.attributes { - if case .Video = attribute { - if let thumbnail = fileReference.media.videoThumbnails.first { - resource = thumbnail.resource - secondaryResource = fileReference.media.resource - } - } - } - - nextWorker += 1 - self.account = account - self.resource = resource - self.hintVP9 = hintVP9 - self.secondaryResource = secondaryResource - self.queue = ThreadPoolQueue(threadPool: softwareVideoWorkers) - self.layerHolder = layerHolder - self.layer = layer ?? layerHolder?.layer - self.layer?.videoGravity = .resizeAspectFill - self.layer?.masksToBounds = true - self.fetchDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: fileReference.resourceReference(resource)).start() - } - - deinit { - self.fetchDisposable.dispose() - self.dataDisposable.dispose() - } - - public func start() { - func stringForResource(_ resource: MediaResource?) -> String { - guard let resource = resource else { - return "" - } - if let resource = resource as? WebFileReferenceMediaResource { - return resource.url - } else { - return resource.id.stringRepresentation - } - } - Logger.shared.log("SoftwareVideo", "load video from \(stringForResource(self.resource)) or \(stringForResource(self.secondaryResource))") - let secondarySignal: Signal<(String, MediaResource)?, NoError> - if let secondaryResource = self.secondaryResource { - secondarySignal = self.account.postbox.mediaBox.resourceData(secondaryResource, option: .complete(waitUntilFetchStatus: false)) - |> map { data -> (String, MediaResource)? in - if data.complete { - return (data.path, secondaryResource) - } else { - return nil - } - } - } else { - secondarySignal = .single(nil) - } - - let firstResource = self.resource - - let firstReady: Signal<(String, MediaResource), NoError> = combineLatest( - self.account.postbox.mediaBox.resourceData(self.resource, option: .complete(waitUntilFetchStatus: false)), - secondarySignal - ) - |> mapToSignal { first, second -> Signal<(String, MediaResource), NoError> in - if first.complete { - return .single((first.path, firstResource)) - } else if let second = second { - return .single(second) - } else { - return .complete() - } - } - |> take(1) - - self.dataDisposable.set((firstReady - |> deliverOn(softwareVideoApplyQueue)).start(next: { [weak self] path, resource in - if let strongSelf = self { - let size = fileSize(path) - Logger.shared.log("SoftwareVideo", "loaded video from \(stringForResource(resource)) (file size: \(String(describing: size))") - - let _ = strongSelf.source.swap(SoftwareVideoSource(path: path, hintVP9: strongSelf.hintVP9, unpremultiplyAlpha: true)) - } - })) - } - - public func tick(timestamp: Double) { - softwareVideoApplyQueue.async { - if self.baseTimestamp == nil && !self.frames.isEmpty { - self.baseTimestamp = timestamp - } - - if let baseTimestamp = self.baseTimestamp { - var index = 0 - var latestFrameIndex: Int? - while index < self.frames.count { - if baseTimestamp + self.frames[index].position.seconds + self.frames[index].duration.seconds <= timestamp { - latestFrameIndex = index - //print("latestFrameIndex = \(index)") - } - index += 1 - } - if let latestFrameIndex = latestFrameIndex { - let frame = self.frames[latestFrameIndex] - for i in (0 ... latestFrameIndex).reversed() { - self.frames.remove(at: i) - } - if self.layer?.status == .failed { - self.layer?.flush() - } - /*if self.layerRotationAngleAndAspect?.0 != self.rotationAngle || self.layerRotationAngleAndAspect?.1 != self.aspect { - self.layerRotationAngleAndAspect = (self.rotationAngle, self.aspect) - var transform = CGAffineTransform(rotationAngle: CGFloat(self.rotationAngle)) - if !self.rotationAngle.isZero { - transform = transform.scaledBy(x: CGFloat(self.aspect), y: CGFloat(1.0 / self.aspect)) - } - self.layerHolder.layer.setAffineTransform(transform) - }*/ - self.layer?.enqueue(frame.sampleBuffer) - - if !self.didStart { - self.didStart = true - Queue.mainQueue().async { - self.started() - } - } - } - } - - self.poll() - } - } - - private var polling = false - - private func poll() { - if self.frames.count < 2 && !self.polling { - self.polling = true - let minPts = self.minPts - let maxPts = self.maxPts - self.queue.addTask(ThreadPoolTask { [weak self] state in - if state.cancelled.with({ $0 }) { - return - } - if let strongSelf = self { - var frameAndLoop: (MediaTrackFrame?, CGFloat, CGFloat, Bool)? - - var hadLoop = false - for _ in 0 ..< 1 { - frameAndLoop = (strongSelf.source.with { $0 })?.readFrame(maxPts: maxPts) - if let frameAndLoop = frameAndLoop { - if frameAndLoop.0 != nil || minPts != nil { - break - } else { - if frameAndLoop.3 { - hadLoop = true - } - //print("skip nil frame loop: \(frameAndLoop.3)") - } - } else { - break - } - } - if let loop = frameAndLoop?.3, loop { - hadLoop = true - } - - softwareVideoApplyQueue.async { - if let strongSelf = self { - strongSelf.polling = false - if let (_, rotationAngle, aspect, _) = frameAndLoop { - strongSelf.rotationAngle = rotationAngle - strongSelf.aspect = aspect - } - if let frame = frameAndLoop?.0 { - if strongSelf.minPts == nil || CMTimeCompare(strongSelf.minPts!, frame.position) < 0 { - var position = CMTimeAdd(frame.position, frame.duration) - for _ in 0 ..< 1 { - position = CMTimeAdd(position, frame.duration) - } - strongSelf.minPts = position - } - strongSelf.frames.append(frame) - strongSelf.frames.sort(by: { lhs, rhs in - if CMTimeCompare(lhs.position, rhs.position) < 0 { - return true - } else { - return false - } - }) - //print("add frame at \(CMTimeGetSeconds(frame.position))") - //let positions = strongSelf.frames.map { CMTimeGetSeconds($0.position) } - //print("frames: \(positions)") - } else { - //print("not adding frames") - } - if hadLoop { - strongSelf.maxPts = strongSelf.minPts - strongSelf.minPts = nil - //print("loop at \(strongSelf.minPts)") - } - strongSelf.poll() - } - } - } - }) - } - } -} diff --git a/submodules/SparseItemGrid/BUILD b/submodules/SparseItemGrid/BUILD index 302566dbbb..75ad547e78 100644 --- a/submodules/SparseItemGrid/BUILD +++ b/submodules/SparseItemGrid/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/AnimationUI:AnimationUI", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/SparseItemGrid/Sources/SparseDiscreteScrollingArea.swift b/submodules/SparseItemGrid/Sources/SparseDiscreteScrollingArea.swift index 311fdf4d1d..b40d79b9b1 100644 --- a/submodules/SparseItemGrid/Sources/SparseDiscreteScrollingArea.swift +++ b/submodules/SparseItemGrid/Sources/SparseDiscreteScrollingArea.swift @@ -351,6 +351,8 @@ public final class SparseDiscreteScrollingArea: ASDisplayNode { let indicatorSize = self.dateIndicator.update( transition: .immediate, component: AnyComponent(SparseItemGridScrollingIndicatorComponent( + isDark: theme.overallDarkAppearance, + isVisible: true, backgroundColor: theme.list.itemBlocksBackgroundColor, shadowColor: .black, foregroundColor: theme.list.itemPrimaryTextColor, diff --git a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift index 4b5eba4437..af2fc0010a 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGridScrollingArea.swift @@ -6,6 +6,7 @@ import ComponentFlow import SwiftSignalKit import AnimationUI import TelegramPresentationData +import GlassBackgroundComponent public final class MultilineText: Component { public let text: String @@ -709,6 +710,8 @@ public final class RollingText: Component { final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { + let isDark: Bool + let isVisible: Bool let backgroundColor: UIColor let shadowColor: UIColor let foregroundColor: UIColor @@ -716,12 +719,16 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { let previousDate: (String, Int32)? init( + isDark: Bool, + isVisible: Bool, backgroundColor: UIColor, shadowColor: UIColor, foregroundColor: UIColor, date: (String, Int32), previousDate: (String, Int32)? ) { + self.isDark = isDark + self.isVisible = isVisible self.backgroundColor = backgroundColor self.shadowColor = shadowColor self.foregroundColor = foregroundColor @@ -730,6 +737,12 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { } static func ==(lhs: SparseItemGridScrollingIndicatorComponent, rhs: SparseItemGridScrollingIndicatorComponent) -> Bool { + if lhs.isDark != rhs.isDark { + return false + } + if lhs.isVisible != rhs.isVisible { + return false + } if lhs.backgroundColor != rhs.backgroundColor { return false } @@ -749,13 +762,11 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { } static var body: Body { - let rect = Child(ShadowRoundedRectangle.self) + let rect = Child(GlassBackgroundComponent.self) let textMonth = Child(RollingText.self) let textYear = Child(RollingText.self) return { context in - context.view.clipsToBounds = true - let date = context.component.date let components = date.0.components(separatedBy: " ") @@ -804,11 +815,17 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { transition: .immediate ) + let rectSize = CGSize(width: textMonth.size.width + 3.0 + textYear.size.width + 26.0, height: 32.0) let rect = rect.update( - component: ShadowRoundedRectangle( - color: context.component.backgroundColor + component: GlassBackgroundComponent( + size: rectSize, + cornerRadius: rectSize.height * 0.5, + isDark: context.component.isDark, + tintColor: .init(kind: .panel), + isInteractive: false, + isVisible: context.component.isVisible ), - availableSize: CGSize(width: textMonth.size.width + 3.0 + textYear.size.width + 26.0, height: 32.0), + availableSize: rectSize, transition: .easeInOut(duration: 0.2) ) @@ -818,7 +835,7 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { )) context.add(rect - .position(CGPoint(x: rectFrame.midX + shadowInset, y: rectFrame.midY + shadowInset)) + .position(CGPoint(x: rectFrame.midX, y: rectFrame.midY)) ) let offset = CGSize(width: textMonth.size.width + 3.0 + textYear.size.width, height: textMonth.size.height).centered(in: rectFrame) @@ -827,9 +844,11 @@ final class SparseItemGridScrollingIndicatorComponent: CombinedComponent { let yearTextFrame = textYear.size.leftCentered(in: rectFrame).offsetBy(dx: offset.minX + monthTextFrame.width + 3.0, dy: 0.0) context.add(textMonth .position(CGPoint(x: monthTextFrame.midX, y: monthTextFrame.midY)) + .opacity(context.component.isVisible ? 1.0 : 0.0) ) context.add(textYear .position(CGPoint(x: yearTextFrame.midX, y: yearTextFrame.midY)) + .opacity(context.component.isVisible ? 1.0 : 0.0) ) return rect.size @@ -1029,7 +1048,6 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { self.dateIndicator = ComponentHostView() self.lineIndicator = ComponentHostView() - self.dateIndicator.alpha = 0.0 self.lineIndicator.alpha = 0.0 super.init() @@ -1159,6 +1177,36 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { private var currentDate: (String, Int32)? private var isScrolling: Bool = false + private var isDateIndicatorVisible = false + + private func requestUpdate(animated: Bool) { + guard let containerSize = self.containerSize else { + return + } + let _ = self.updateIndicator(containerSize: containerSize, animated: animated) + } + + private func updateIndicator(containerSize: CGSize, previousDate: (String, Int32)? = nil, animated: Bool) -> CGSize { + guard let theme = self.theme, let date = self.currentDate else { + return .zero + } + let indicatorSize = self.dateIndicator.update( + transition: animated ? .easeInOut(duration: 0.2) : .immediate, + component: AnyComponent(SparseItemGridScrollingIndicatorComponent( + isDark: theme.overallDarkAppearance, + isVisible: self.isDateIndicatorVisible, + backgroundColor: theme.list.itemBlocksBackgroundColor, + shadowColor: .black, + foregroundColor: theme.list.itemPrimaryTextColor, + date: date, + previousDate: previousDate + )), + environment: {}, + containerSize: containerSize + ) + return indicatorSize + } + public func update( containerSize: CGSize, containerInsets: UIEdgeInsets, @@ -1186,18 +1234,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { } let animateIndicatorFrame = previousDate != nil && previousDate?.1 != date.1 - let indicatorSize = self.dateIndicator.update( - transition: animateIndicatorFrame ? .easeInOut(duration: 0.2) : .immediate, - component: AnyComponent(SparseItemGridScrollingIndicatorComponent( - backgroundColor: theme.list.itemBlocksBackgroundColor, - shadowColor: .black, - foregroundColor: theme.list.itemPrimaryTextColor, - date: date, - previousDate: previousDate - )), - environment: {}, - containerSize: containerSize - ) + let indicatorSize = self.updateIndicator(containerSize: containerSize, previousDate: previousDate, animated: animateIndicatorFrame) let scrollIndicatorHeightFraction = min(1.0, max(0.0, (containerSize.height - containerInsets.top - containerInsets.bottom) / contentHeight)) if scrollIndicatorHeightFraction >= 0.55 - .ulpOfOne { @@ -1244,7 +1281,6 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { if isScrolling { let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) - transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 1.0) transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 1.0) } @@ -1284,10 +1320,13 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { private func updateActivityTimer(isScrolling: Bool) { self.activityTimer?.invalidate() + + if isScrolling || self.isDragging { + self.isDateIndicatorVisible = true + } if self.isDragging { let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) - transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 1.0) transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 1.0) } else { self.activityTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: false, completion: { [weak self] in @@ -1295,9 +1334,11 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { return } let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) - transition.updateAlpha(layer: strongSelf.dateIndicator.layer, alpha: 0.0) transition.updateAlpha(layer: strongSelf.lineIndicator.layer, alpha: 0.0) + strongSelf.isDateIndicatorVisible = false + strongSelf.requestUpdate(animated: true) + strongSelf.dismissLineTooltip() }, queue: .mainQueue()) self.activityTimer?.start() @@ -1372,7 +1413,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.dateIndicator.alpha <= 0.01 { + if self.dateIndicator.alpha <= 0.01 || !self.isDateIndicatorVisible { return nil } if self.dateIndicator.frame.offsetBy(dx: self.dateIndicatorContainer.frame.minX, dy: self.dateIndicatorContainer.frame.minY).contains(point) { @@ -1391,9 +1432,11 @@ public final class SparseItemGridScrollingArea: ASDisplayNode { public func hideScroller() { let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) - transition.updateAlpha(layer: self.dateIndicator.layer, alpha: 0.0) transition.updateAlpha(layer: self.lineIndicator.layer, alpha: 0.0) + self.isDateIndicatorVisible = false + self.requestUpdate(animated: true) + self.dismissLineTooltip() } } diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index a0f5a6df6b..0fa30c747c 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 @@ -205,7 +205,7 @@ private enum StatsEntry: ItemListNodeEntry { case instantPageInteractionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType) case postsTitle(PresentationTheme, String) - case post(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, StatsPostItem, ChannelStatsPostInteractions) + case post(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, EnginePeer, StatsPostItem, ChannelStatsPostInteractions) case boostLevel(PresentationTheme, Int32, Int32, CGFloat) @@ -636,7 +636,7 @@ private enum StatsEntry: ItemListNodeEntry { return false } case let .post(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsPost, lhsInteractions): - if case let .post(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsPost, rhsInteractions) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsPost == rhsPost, lhsInteractions == rhsInteractions { + if case let .post(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsPost, rhsInteractions) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsPost == rhsPost, lhsInteractions == rhsInteractions { return true } else { return false @@ -960,7 +960,7 @@ private enum StatsEntry: ItemListNodeEntry { }, sectionId: self.section, style: .blocks) case let .post(_, _, _, _, peer, post, interactions): return StatsMessageItem(context: arguments.context, presentationData: presentationData, peer: peer, item: post, views: interactions.views, reactions: interactions.reactions, forwards: interactions.forwards, sectionId: self.section, style: .blocks, action: { - arguments.openPostStats(EnginePeer(peer), post) + arguments.openPostStats(peer, post) }, openStory: { sourceView in if case let .story(_, story) = post { arguments.openStory(story, sourceView) @@ -1426,11 +1426,11 @@ private func statsEntries( switch post { case let .message(message): if let interactions = interactions[.message(id: message.id)] { - entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer._asPeer(), post, interactions)) + entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, post, interactions)) } case let .story(_, story): if let interactions = interactions[.story(peerId: peer.id, id: story.id)] { - entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer._asPeer(), post, interactions)) + entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, post, interactions)) } } index += 1 @@ -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/StatisticsUI/Sources/GroupStatsController.swift b/submodules/StatisticsUI/Sources/GroupStatsController.swift index 7109e9d382..a868f71de3 100644 --- a/submodules/StatisticsUI/Sources/GroupStatsController.swift +++ b/submodules/StatisticsUI/Sources/GroupStatsController.swift @@ -826,7 +826,15 @@ public func groupStatsController(context: AccountContext, updatedPresentationDat let previousData = Atomic(value: nil) let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData - let signal = combineLatest(statePromise.get(), presentationData, dataPromise.get(), context.account.postbox.loadedPeerWithId(peerId), peersPromise.get(), longLoadingSignal) + let loadedChannelPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + let signal = combineLatest(statePromise.get(), presentationData, dataPromise.get(), loadedChannelPeer, peersPromise.get(), longLoadingSignal) |> deliverOnMainQueue |> map { state, presentationData, data, channelPeer, peers, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in let previous = previousData.swap(data) @@ -838,9 +846,9 @@ public func groupStatsController(context: AccountContext, updatedPresentationDat emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } } - + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChannelInfo_Stats), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupStatsControllerEntries(accountPeerId: context.account.peerId, state: state, data: data, channelPeer: EnginePeer(channelPeer), peers: peers, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: groupStatsControllerEntries(accountPeerId: context.account.peerId, state: state, data: data, channelPeer: channelPeer, peers: peers, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -862,7 +870,14 @@ public func groupStatsController(context: AccountContext, updatedPresentationDat } openPeerImpl = { [weak controller] peer in if let navigationController = controller?.navigationController as? NavigationController { - let _ = (context.account.postbox.loadedPeerWithId(peer.id) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> take(1) |> deliverOnMainQueue).start(next: { peer in if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { @@ -887,7 +902,14 @@ public func groupStatsController(context: AccountContext, updatedPresentationDat } openPeerAdminActionsImpl = { [weak controller] participantPeerId in if let navigationController = controller?.navigationController as? NavigationController { - let _ = (context.account.postbox.loadedPeerWithId(peerId) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> take(1) |> deliverOnMainQueue).start(next: { peer in let controller = context.sharedContext.makeChatRecentActionsController(context: context, peer: peer, adminPeerId: participantPeerId, starsState: nil) diff --git a/submodules/StatisticsUI/Sources/MessageStatsController.swift b/submodules/StatisticsUI/Sources/MessageStatsController.swift index a1d3cf316b..bbe3db555e 100644 --- a/submodules/StatisticsUI/Sources/MessageStatsController.swift +++ b/submodules/StatisticsUI/Sources/MessageStatsController.swift @@ -36,6 +36,7 @@ private final class MessageStatsControllerArguments { private enum StatsSection: Int32 { case overview + case votes case interactions case reactions case publicForwards @@ -45,6 +46,9 @@ private enum StatsEntry: ItemListNodeEntry { case overviewTitle(PresentationTheme, String) case overview(PresentationTheme, PostStats, EngineStoryItem.Views?, Int32?) + case votesTitle(PresentationTheme, String) + case votesGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Bool) + case interactionsTitle(PresentationTheme, String) case interactionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType, Bool) @@ -58,6 +62,8 @@ private enum StatsEntry: ItemListNodeEntry { switch self { case .overviewTitle, .overview: return StatsSection.overview.rawValue + case .votesTitle, .votesGraph: + return StatsSection.votes.rawValue case .interactionsTitle, .interactionsGraph: return StatsSection.interactions.rawValue case .reactionsTitle, .reactionsGraph: @@ -73,18 +79,22 @@ private enum StatsEntry: ItemListNodeEntry { return 0 case .overview: return 1 - case .interactionsTitle: + case .votesTitle: return 2 - case .interactionsGraph: + case .votesGraph: return 3 - case .reactionsTitle: + case .interactionsTitle: return 4 - case .reactionsGraph: + case .interactionsGraph: return 5 - case .publicForwardsTitle: + case .reactionsTitle: return 6 + case .reactionsGraph: + return 7 + case .publicForwardsTitle: + return 8 case let .publicForward(index, _, _, _, _): - return 7 + index + return 9 + index } } @@ -108,6 +118,18 @@ private enum StatsEntry: ItemListNodeEntry { } else { return false } + case let .votesTitle(lhsTheme, lhsText): + if case let .votesTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .votesGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType, lhsNoInitialZoom): + if case let .votesGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType, rhsNoInitialZoom) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType, lhsNoInitialZoom == rhsNoInitialZoom { + return true + } else { + return false + } case let .interactionsTitle(lhsTheme, lhsText): if case let .interactionsTitle(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -155,13 +177,14 @@ private enum StatsEntry: ItemListNodeEntry { let arguments = arguments as! MessageStatsControllerArguments switch self { case let .overviewTitle(_, text), + let .votesTitle(_, text), let .interactionsTitle(_, text), let .reactionsTitle(_, text), let .publicForwardsTitle(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .overview(_, stats, storyViews, publicShares): return StatsOverviewItem(context: arguments.context, presentationData: presentationData, systemStyle: .glass, isGroup: false, stats: stats as? Stats, storyViews: storyViews, publicShares: publicShares, sectionId: self.section, style: .blocks) - case let .interactionsGraph(_, _, _, graph, type, noInitialZoom), let .reactionsGraph(_, _, _, graph, type, noInitialZoom): + case let .votesGraph(_, _, _, graph, type, noInitialZoom), let .interactionsGraph(_, _, _, graph, type, noInitialZoom), let .reactionsGraph(_, _, _, graph, type, noInitialZoom): return StatsGraphItem(presentationData: presentationData, systemStyle: .glass, graph: graph, type: type, noInitialZoom: noInitialZoom, getDetailsData: { date, completion in let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in if let graph = graph, case let .Loaded(_, data) = graph { @@ -175,10 +198,10 @@ private enum StatsEntry: ItemListNodeEntry { var reactions: Int32 = 0 var isStory = false - let peer: Peer + let peer: EnginePeer switch item { case let .message(message): - peer = message.peers[message.id.peerId]! + peer = EnginePeer(message.peers[message.id.peerId]!) for attribute in message.attributes { if let viewsAttribute = attribute as? ViewCountMessageAttribute { views = Int32(viewsAttribute.count) @@ -191,7 +214,7 @@ private enum StatsEntry: ItemListNodeEntry { } } case let .story(peerValue, story): - peer = peerValue._asPeer() + peer = peerValue views = Int32(story.views?.seenCount ?? 0) forwards = Int32(story.views?.forwardCount ?? 0) reactions = Int32(story.views?.reactedCount ?? 0) @@ -215,7 +238,7 @@ private enum StatsEntry: ItemListNodeEntry { } } -private func messageStatsControllerEntries(data: PostStats?, storyViews: EngineStoryItem.Views?, forwards: StoryStatsPublicForwardsContext.State?, presentationData: PresentationData) -> [StatsEntry] { +private func messageStatsControllerEntries(data: PostStats?, pollData: PollStats?, storyViews: EngineStoryItem.Views?, forwards: StoryStatsPublicForwardsContext.State?, presentationData: PresentationData) -> [StatsEntry] { var entries: [StatsEntry] = [] if let data = data { @@ -227,6 +250,11 @@ private func messageStatsControllerEntries(data: PostStats?, storyViews: EngineS } entries.append(.overview(presentationData.theme, data, storyViews, publicShares)) + if let pollData, !pollData.votesGraph.isEmpty { + entries.append(.votesTitle(presentationData.theme, presentationData.strings.PollStats_GraphHeader.uppercased())) + entries.append(.votesGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, pollData.votesGraph, .lines5Min, false)) + } + var isStories = false if let _ = data as? StoryStats { isStories = true @@ -295,8 +323,10 @@ public func messageStatsController(context: AccountContext, updatedPresentationD let actionsDisposable = DisposableSet() let dataPromise = Promise(nil) let forwardsPromise = Promise(nil) + let pollDataPromise = Promise(nil) let anyStatsContext: Any + var pollStatsContext: Any? let dataSignal: Signal var loadDetailedGraphImpl: ((StatsGraph, Int64) -> Signal)? var openStoryImpl: ((EnginePeer.Id, EngineStoryItem, UIView) -> Void)? @@ -320,6 +350,31 @@ public func messageStatsController(context: AccountContext, updatedPresentationD anyStatsContext = statsContext forwardsContext = StoryStatsPublicForwardsContext(account: context.account, subject: .message(messageId: id)) + + pollDataPromise.set(context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: id)) + |> map { message -> PollStatsContext? in + guard let message else { + return nil + } + for media in message.media { + if let poll = media as? TelegramMediaPoll, poll.results.canViewStats { + return PollStatsContext(account: context.account, messageId: message.id) + } + } + return nil + } + |> afterNext { statsContext in + pollStatsContext = statsContext + } + |> mapToSignal { pollStatsContext in + guard let pollStatsContext else { + return .single(nil) + } + return pollStatsContext.state + |> map { state in + return state.stats + } + }) case let .story(peerIdValue, id, item, _): peerId = peerIdValue storyItem = item @@ -338,13 +393,11 @@ public func messageStatsController(context: AccountContext, updatedPresentationD forwardsContext = StoryStatsPublicForwardsContext(account: context.account, subject: .story(peerId: peerId, id: id)) } - forwardsPromise.set( - .single(nil) - |> then( - forwardsContext.state - |> map(Optional.init) - ) - ) + forwardsPromise.set(.single(nil) + |> then( + forwardsContext.state + |> map(Optional.init) + )) let arguments = MessageStatsControllerArguments(context: context, loadDetailedGraph: { graph, x -> Signal in return loadDetailedGraphImpl?(graph, x) ?? .single(nil) @@ -384,12 +437,13 @@ public func messageStatsController(context: AccountContext, updatedPresentationD let signal = combineLatest( presentationData, dataPromise.get(), + pollDataPromise.get(), forwardsPromise.get(), longLoadingSignal, iconNodePromise.get() ) |> deliverOnMainQueue - |> map { presentationData, data, forwards, longLoading, iconNode -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, data, pollData, forwards, longLoading, iconNode -> (ItemListControllerState, (ItemListNodeState, Any)) in let previous = previousData.swap(data) var emptyStateItem: ItemListControllerEmptyStateItem? if data == nil { @@ -415,13 +469,14 @@ public func messageStatsController(context: AccountContext, updatedPresentationD openStoryImpl?(peerId, storyItem, iconNode.view) } }) }, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: messageStatsControllerEntries(data: data, storyViews: storyViews, forwards: forwards, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: messageStatsControllerEntries(data: data, pollData: pollData, storyViews: storyViews, forwards: forwards, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() let _ = anyStatsContext + let _ = pollStatsContext let _ = forwardsContext } @@ -546,7 +601,7 @@ public func messageStatsController(context: AccountContext, updatedPresentationD return } if case .user = peer { - if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) { navigationController.pushViewController(controller) } } else { diff --git a/submodules/StatisticsUI/Sources/StatsGraphItem.swift b/submodules/StatisticsUI/Sources/StatsGraphItem.swift index 8fb221d919..ce10ccaeea 100644 --- a/submodules/StatisticsUI/Sources/StatsGraphItem.swift +++ b/submodules/StatisticsUI/Sources/StatsGraphItem.swift @@ -109,6 +109,7 @@ public final class StatsGraphItemNode: ListViewItemNode { let chartNode: ChartNode private let activityIndicator: ActivityIndicator + private let errorTextNode: TextNode private var item: StatsGraphItem? private var visibilityHeight: CGFloat? @@ -134,11 +135,15 @@ public final class StatsGraphItemNode: ListViewItemNode { self.chartNode = ChartNode() self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(.black, 16.0, 2.0, false)) self.activityIndicator.isHidden = true + self.errorTextNode = TextNode() + self.errorTextNode.isUserInteractionEnabled = false + self.errorTextNode.isHidden = true super.init(layerBacked: false) self.chartContainerNode.addSubnode(self.chartNode) self.chartContainerNode.addSubnode(self.activityIndicator) + self.chartContainerNode.addSubnode(self.errorTextNode) } public override func didLoad() { @@ -154,12 +159,14 @@ public final class StatsGraphItemNode: ListViewItemNode { } func asyncLayout() -> (_ item: StatsGraphItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { + let makeErrorTextLayout = TextNode.asyncLayout(self.errorTextNode) let currentItem = self.item let currentVisibilityHeight = self.visibilityHeight return { item, params, neighbors in let leftInset = params.leftInset let rightInset: CGFloat = params.rightInset + let errorTextFont = Font.regular(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 16.0) var updatedTheme: PresentationTheme? var updatedGraph: StatsGraph? var updatedController: BaseChartController? @@ -197,6 +204,27 @@ public final class StatsGraphItemNode: ListViewItemNode { contentSize = CGSize(width: params.width, height: 361.0) insets = itemListNeighborsGroupedInsets(neighbors, params) } + + let errorText: String + if case let .Failed(text) = item.graph { + errorText = text + } else { + errorText = "" + } + let (errorTextLayout, errorTextApply) = makeErrorTextLayout(TextNodeLayoutArguments( + attributedString: NSAttributedString( + string: errorText, + font: errorTextFont, + textColor: item.presentationData.theme.list.itemSecondaryTextColor + ), + backgroundColor: nil, + maximumNumberOfLines: 0, + truncationType: .end, + constrainedSize: CGSize(width: max(1.0, params.width - leftInset - rightInset - 32.0), height: contentSize.height), + alignment: .center, + cutout: nil, + insets: UIEdgeInsets() + )) var visibilityHeight = currentVisibilityHeight if let updatedController = updatedController { @@ -293,8 +321,16 @@ public final class StatsGraphItemNode: ListViewItemNode { strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) strongSelf.activityIndicator.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - 16.0) / 2.0), y: floor((layout.size.height - 16.0) / 2.0)), size: CGSize(width: 16.0, height: 16.0)) + strongSelf.errorTextNode.frame = CGRect( + origin: CGPoint( + x: floorToScreenPixels((strongSelf.chartContainerNode.bounds.width - errorTextLayout.size.width) / 2.0), + y: floorToScreenPixels((strongSelf.chartContainerNode.bounds.height - errorTextLayout.size.height) / 2.0) + ), + size: errorTextLayout.size + ) } + let _ = errorTextApply() strongSelf.activityIndicator.type = .custom(item.presentationData.theme.list.itemSecondaryTextColor, 16.0, 2.0, false) if let updatedTheme = updatedTheme { @@ -310,15 +346,27 @@ public final class StatsGraphItemNode: ListViewItemNode { ) } - if let updatedGraph = updatedGraph { - if case .Loaded = updatedGraph, let updatedController = updatedController { - strongSelf.chartNode.setup(controller: updatedController, noInitialZoom: item.noInitialZoom) - strongSelf.activityIndicator.isHidden = true - strongSelf.chartNode.isHidden = false - } else if case .OnDemand = updatedGraph { - strongSelf.activityIndicator.isHidden = false - strongSelf.chartNode.isHidden = true - } + if let updatedGraph = updatedGraph, case .Loaded = updatedGraph, let updatedController = updatedController { + strongSelf.chartNode.setup(controller: updatedController, noInitialZoom: item.noInitialZoom) + } + + switch item.graph { + case .Loaded: + strongSelf.activityIndicator.isHidden = true + strongSelf.chartNode.isHidden = false + strongSelf.errorTextNode.isHidden = true + case .OnDemand: + strongSelf.activityIndicator.isHidden = false + strongSelf.chartNode.isHidden = true + strongSelf.errorTextNode.isHidden = true + case let .Failed(error): + strongSelf.activityIndicator.isHidden = true + strongSelf.chartNode.isHidden = true + strongSelf.errorTextNode.isHidden = error.isEmpty + case .Empty: + strongSelf.activityIndicator.isHidden = true + strongSelf.chartNode.isHidden = true + strongSelf.errorTextNode.isHidden = true } } }) @@ -337,4 +385,3 @@ public final class StatsGraphItemNode: ListViewItemNode { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } } - diff --git a/submodules/StatisticsUI/Sources/StatsMessageItem.swift b/submodules/StatisticsUI/Sources/StatsMessageItem.swift index b7aa9dc8fb..2751c95efd 100644 --- a/submodules/StatisticsUI/Sources/StatsMessageItem.swift +++ b/submodules/StatisticsUI/Sources/StatsMessageItem.swift @@ -19,7 +19,7 @@ public class StatsMessageItem: ListViewItem, ItemListItem { let context: AccountContext let presentationData: ItemListPresentationData let systemStyle: ItemListSystemStyle - let peer: Peer + let peer: EnginePeer let item: StatsPostItem let views: Int32 let reactions: Int32 @@ -31,7 +31,7 @@ public class StatsMessageItem: ListViewItem, ItemListItem { let openStory: (UIView) -> Void let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? - init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .glass, peer: Peer, item: StatsPostItem, views: Int32, reactions: Int32, forwards: Int32, isPeer: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, openStory: @escaping (UIView) -> Void, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?) { + init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .glass, peer: EnginePeer, item: StatsPostItem, views: Int32, reactions: Int32, forwards: Int32, isPeer: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, openStory: @escaping (UIView) -> Void, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?) { self.context = context self.presentationData = presentationData self.systemStyle = systemStyle @@ -351,7 +351,7 @@ final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode { } if item.isPeer { - text = EnginePeer(item.peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + text = item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) } else { text = foldLineBreaks(text) } @@ -476,7 +476,7 @@ final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode { strongSelf.offsetContainerNode.addSubnode(avatarNode) strongSelf.avatarNode = avatarNode } - avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: EnginePeer(item.peer)) + avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer) if case .story = item.item { contentImageInset += 3.0 diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift index 62b02b164a..514091663d 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackEmojisItem.swift @@ -4,7 +4,6 @@ import Display import TelegramCore import SwiftSignalKit import AsyncDisplayKit -import Postbox import StickerResources import AccountContext import AnimatedStickerNode diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 2621570cd5..538b72044c 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)) } @@ -1517,13 +1517,20 @@ private final class StickerPackContainer: ASDisplayNode { if let (info, items, installed) = self.currentStickerPack { var dismissed = false switch self.decideNextAction(self, installed ? .remove : .add) { - case .dismiss: - self.requestDismiss() - dismissed = true - case .navigatedNext, .ignored: - self.updateStickerPackContents([.result(info: StickerPackCollectionInfo.Accessor(info), items: items, installed: !installed)], hasPremium: false) + case .dismiss: + self.requestDismiss() + dismissed = true + case .navigatedNext, .ignored: + self.updateStickerPackContents([.result(info: StickerPackCollectionInfo.Accessor(info), items: items, installed: !installed)], hasPremium: false) } + guard let controller = self.controller else { + return + } + let navigationController = controller.parentNavigationController ?? (controller.navigationController as? NavigationController) + let context = self.context + let strings = self.presentationData.strings + let actionPerformed = self.controller?.actionPerformed if installed { let _ = (self.context.engine.stickers.removeStickerPackInteractively(id: info.id, option: .delete) @@ -1536,7 +1543,19 @@ private final class StickerPackContainer: ASDisplayNode { } }) } else { - let _ = self.context.engine.stickers.addStickerPackInteractively(info: info, items: items).start() + let _ = self.context.engine.stickers.addStickerPackInteractively(info: info, items: items, noDelay: true).startStandalone() + let _ = (self.context.account.stateManager.installedStickerPacksArchivedEvents + |> deliverOnMainQueue + |> take(1) + |> timeout(3.0, queue: .mainQueue(), alternate: .single(0))).startStandalone(next: { [weak navigationController] count in + if count == 0 { + return + } + guard let navigationController else { + return + } + navigationController.pushViewController(textAlertController(context: context, updatedPresentationData: controller.updatedPresentationData, title: nil, text: strings.ArchivedPacksAlert_Title, actions: [TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})])) + }) if dismissed { actionPerformed?([(info, items, .add)]) } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewController.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewController.swift index 9d39bd02fa..b4c6928544 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewController.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import AccountContext diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewControllerNode.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewControllerNode.swift index 526ea1281c..f0cda8dd1c 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewControllerNode.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewControllerNode.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import AccountContext diff --git a/submodules/StickerPeekUI/BUILD b/submodules/StickerPeekUI/BUILD index 33f9618a5b..de35047057 100644 --- a/submodules/StickerPeekUI/BUILD +++ b/submodules/StickerPeekUI/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AccountContext:AccountContext", diff --git a/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift index 749f133e9b..0d8028d905 100644 --- a/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import StickerResources diff --git a/submodules/Svg/BUILD b/submodules/Svg/BUILD index eba76650f4..b559ec3b70 100644 --- a/submodules/Svg/BUILD +++ b/submodules/Svg/BUILD @@ -1,21 +1,16 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") -objc_library( +swift_library( name = "Svg", - enable_modules = True, module_name = "Svg", srcs = glob([ - "Sources/**/*.m", - "Sources/**/*.c", - "Sources/**/*.h", + "Sources/**/*.swift", ]), - hdrs = glob([ - "PublicHeaders/**/*.h", - ]), - includes = [ - "PublicHeaders", + copts = [ + "-warnings-as-errors", ], - sdk_frameworks = [ - "Foundation", + deps = [ + "//submodules/Svg/LegacyImpl", ], visibility = [ "//visibility:public", diff --git a/submodules/Svg/LegacyImpl/BUILD b/submodules/Svg/LegacyImpl/BUILD new file mode 100644 index 0000000000..6808c17505 --- /dev/null +++ b/submodules/Svg/LegacyImpl/BUILD @@ -0,0 +1,23 @@ + +objc_library( + name = "LegacyImpl", + enable_modules = True, + module_name = "LegacyImpl", + srcs = glob([ + "Sources/**/*.m", + "Sources/**/*.c", + "Sources/**/*.h", + ]), + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + sdk_frameworks = [ + "Foundation", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Svg/PublicHeaders/Svg/Svg.h b/submodules/Svg/LegacyImpl/PublicHeaders/Svg/Svg.h similarity index 84% rename from submodules/Svg/PublicHeaders/Svg/Svg.h rename to submodules/Svg/LegacyImpl/PublicHeaders/Svg/Svg.h index 39080185f3..acb4abd69f 100755 --- a/submodules/Svg/PublicHeaders/Svg/Svg.h +++ b/submodules/Svg/LegacyImpl/PublicHeaders/Svg/Svg.h @@ -27,6 +27,6 @@ GiftPatternData * _Nullable getGiftPatternData(NSData * _Nonnull data); UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit); UIImage * _Nullable renderPreparedImageWithSymbol(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit, UIImage * _Nullable symbolImage, int32_t modelRectIndex); -UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, CGFloat scale, bool opaque); +UIImage * _Nullable drawSvgImageImpl(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, CGFloat scale, bool opaque); #endif /* Lottie_h */ diff --git a/submodules/Svg/Sources/Svg.m b/submodules/Svg/LegacyImpl/Sources/Svg.m similarity index 99% rename from submodules/Svg/Sources/Svg.m rename to submodules/Svg/LegacyImpl/Sources/Svg.m index d3c4edee41..a7c73827b1 100755 --- a/submodules/Svg/Sources/Svg.m +++ b/submodules/Svg/LegacyImpl/Sources/Svg.m @@ -302,7 +302,7 @@ void renderShape(NSVGshape *shape, CGContextRef context, UIColor *foregroundColo } } -UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, UIColor *foregroundColor, CGFloat canvasScale, bool opaque) { +UIImage * _Nullable drawSvgImageImpl(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, UIColor *foregroundColor, CGFloat canvasScale, bool opaque) { if (!data || data.length == 0) return nil; NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; diff --git a/submodules/Svg/Sources/nanosvg.c b/submodules/Svg/LegacyImpl/Sources/nanosvg.c similarity index 100% rename from submodules/Svg/Sources/nanosvg.c rename to submodules/Svg/LegacyImpl/Sources/nanosvg.c diff --git a/submodules/Svg/Sources/nanosvg.h b/submodules/Svg/LegacyImpl/Sources/nanosvg.h similarity index 100% rename from submodules/Svg/Sources/nanosvg.h rename to submodules/Svg/LegacyImpl/Sources/nanosvg.h diff --git a/submodules/Svg/Sources/Svg.swift b/submodules/Svg/Sources/Svg.swift new file mode 100644 index 0000000000..3d70dbddd7 --- /dev/null +++ b/submodules/Svg/Sources/Svg.swift @@ -0,0 +1,7 @@ +import Foundation +import UIKit +import LegacyImpl + +public func drawSvgImage(data: Data, size: CGSize, backgroundColor: UIColor?, foregroundColor: UIColor?, scale: CGFloat, opaque: Bool) -> UIImage? { + return drawSvgImageImpl(data, size, backgroundColor, foregroundColor, scale, opaque) +} diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 6a56740d86..fd0a5e29ee 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -1,6 +1,7 @@ public enum Api { public enum account {} + public enum aicompose {} public enum auth {} public enum bots {} public enum channels {} @@ -23,6 +24,7 @@ public enum Api { public enum users {} public enum functions { public enum account {} + public enum aicompose {} public enum auth {} public enum bots {} public enum channels {} @@ -55,6 +57,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[0x0929C32F] = { return parseInt256($0) } dict[-1255641564] = { return parseString($0) } dict[-1194283041] = { return Api.AccountDaysTTL.parse_accountDaysTTL($0) } + dict[-805945687] = { return Api.AiComposeTone.parse_aiComposeTone($0) } + dict[-1683135468] = { return Api.AiComposeTone.parse_aiComposeToneDefault($0) } + dict[-237623060] = { return Api.AiComposeToneExample.parse_aiComposeToneExample($0) } dict[-653423106] = { return Api.AttachMenuBot.parse_attachMenuBot($0) } dict[-1297663893] = { return Api.AttachMenuBotIcon.parse_attachMenuBotIcon($0) } dict[1165423600] = { return Api.AttachMenuBotIconColor.parse_attachMenuBotIconColor($0) } @@ -321,6 +326,9 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1589952067] = { return Api.InlineQueryPeerType.parse_inlineQueryPeerTypeMegagroup($0) } dict[-2093215828] = { return Api.InlineQueryPeerType.parse_inlineQueryPeerTypePM($0) } dict[813821341] = { return Api.InlineQueryPeerType.parse_inlineQueryPeerTypeSameBotPM($0) } + dict[535407039] = { return Api.InputAiComposeTone.parse_inputAiComposeToneDefault($0) } + dict[125026432] = { return Api.InputAiComposeTone.parse_inputAiComposeToneID($0) } + dict[530584407] = { return Api.InputAiComposeTone.parse_inputAiComposeToneSlug($0) } dict[488313413] = { return Api.InputAppEvent.parse_inputAppEvent($0) } dict[-1457472134] = { return Api.InputBotApp.parse_inputBotAppID($0) } dict[-1869872121] = { return Api.InputBotApp.parse_inputBotAppShortName($0) } @@ -510,7 +518,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[853188252] = { return Api.InputStickerSetItem.parse_inputStickerSetItem($0) } dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) } dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) } - dict[-1682807955] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentAuthCode($0) } + dict[1069645911] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentAuthCode($0) } dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) } dict[-75955309] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiftCode($0) } dict[369444042] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentPremiumGiveaway($0) } @@ -577,7 +585,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) } dict[1235637404] = { return Api.MediaArea.parse_mediaAreaWeather($0) } dict[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) } - dict[988112002] = { return Api.Message.parse_message($0) } + dict[-1779470549] = { return Api.Message.parse_message($0) } dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) } dict[2055212554] = { return Api.Message.parse_messageService($0) } dict[-872240531] = { return Api.MessageAction.parse_messageActionBoostApply($0) } @@ -820,7 +828,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[236446268] = { return Api.PhotoSize.parse_photoSizeEmpty($0) } dict[-96535659] = { return Api.PhotoSize.parse_photoSizeProgressive($0) } dict[-525288402] = { return Api.PhotoSize.parse_photoStrippedSize($0) } - dict[-1203610647] = { return Api.Poll.parse_poll($0) } + dict[-1771164225] = { return Api.Poll.parse_poll($0) } dict[429911446] = { return Api.PollAnswer.parse_inputPollAnswer($0) } dict[1266514026] = { return Api.PollAnswer.parse_pollAnswer($0) } dict[910500618] = { return Api.PollAnswerVoters.parse_pollAnswerVoters($0) } @@ -1085,6 +1093,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1236871718] = { return Api.TodoList.parse_todoList($0) } dict[-305282981] = { return Api.TopPeer.parse_topPeer($0) } dict[-39945236] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsApp($0) } + dict[1814361053] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsGuestChat($0) } dict[344356834] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsInline($0) } dict[-1419371685] = { return Api.TopPeerCategory.parse_topPeerCategoryBotsPM($0) } dict[371037736] = { return Api.TopPeerCategory.parse_topPeerCategoryChannels($0) } @@ -1094,6 +1103,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1122524854] = { return Api.TopPeerCategory.parse_topPeerCategoryGroups($0) } dict[511092620] = { return Api.TopPeerCategory.parse_topPeerCategoryPhoneCalls($0) } dict[-75283823] = { return Api.TopPeerCategoryPeers.parse_topPeerCategoryPeers($0) } + dict[-1945136645] = { return Api.Update.parse_updateAiComposeTones($0) } dict[397910539] = { return Api.Update.parse_updateAttachMenuBots($0) } dict[-335171433] = { return Api.Update.parse_updateAutoSaveSettings($0) } dict[-1964652166] = { return Api.Update.parse_updateBotBusinessConnect($0) } @@ -1103,6 +1113,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1299263278] = { return Api.Update.parse_updateBotCommands($0) } dict[-1607821266] = { return Api.Update.parse_updateBotDeleteBusinessMessage($0) } dict[132077692] = { return Api.Update.parse_updateBotEditBusinessMessage($0) } + dict[-841742019] = { return Api.Update.parse_updateBotGuestChatQuery($0) } dict[1232025500] = { return Api.Update.parse_updateBotInlineQuery($0) } dict[317794823] = { return Api.Update.parse_updateBotInlineSend($0) } dict[347625491] = { return Api.Update.parse_updateBotMenuButton($0) } @@ -1283,6 +1294,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[555358088] = { return Api.WebPage.parse_webPageEmpty($0) } dict[1930545681] = { return Api.WebPage.parse_webPageNotModified($0) } dict[-1328464313] = { return Api.WebPage.parse_webPagePending($0) } + dict[2005007896] = { return Api.WebPageAttribute.parse_webPageAttributeAiComposeTone($0) } dict[29770178] = { return Api.WebPageAttribute.parse_webPageAttributeStarGiftAuction($0) } dict[835375875] = { return Api.WebPageAttribute.parse_webPageAttributeStarGiftCollection($0) } dict[1355547603] = { return Api.WebPageAttribute.parse_webPageAttributeStickerSet($0) } @@ -1329,6 +1341,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-842824308] = { return Api.account.WallPapers.parse_wallPapers($0) } dict[471437699] = { return Api.account.WallPapers.parse_wallPapersNotModified($0) } dict[-313079300] = { return Api.account.WebAuthorizations.parse_webAuthorizations($0) } + dict[1822232318] = { return Api.aicompose.Tones.parse_tones($0) } + dict[-1040948989] = { return Api.aicompose.Tones.parse_tonesNotModified($0) } dict[782418132] = { return Api.auth.Authorization.parse_authorization($0) } dict[1148485274] = { return Api.auth.Authorization.parse_authorizationSignUpRequired($0) } dict[1948046307] = { return Api.auth.CodeType.parse_codeTypeCall($0) } @@ -1344,7 +1358,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-503089271] = { return Api.auth.PasskeyLoginOptions.parse_passkeyLoginOptions($0) } dict[326715557] = { return Api.auth.PasswordRecovery.parse_passwordRecovery($0) } dict[1577067778] = { return Api.auth.SentCode.parse_sentCode($0) } - dict[-527082948] = { return Api.auth.SentCode.parse_sentCodePaymentRequired($0) } + dict[-125665601] = { return Api.auth.SentCode.parse_sentCodePaymentRequired($0) } dict[596704836] = { return Api.auth.SentCode.parse_sentCodeSuccess($0) } dict[1035688326] = { return Api.auth.SentCodeType.parse_sentCodeTypeApp($0) } dict[1398007207] = { return Api.auth.SentCodeType.parse_sentCodeTypeCall($0) } @@ -1357,6 +1371,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1073693790] = { return Api.auth.SentCodeType.parse_sentCodeTypeSms($0) } dict[-1284008785] = { return Api.auth.SentCodeType.parse_sentCodeTypeSmsPhrase($0) } dict[-1542017919] = { return Api.auth.SentCodeType.parse_sentCodeTypeSmsWord($0) } + dict[-585121901] = { return Api.bots.AccessSettings.parse_accessSettings($0) } dict[-391678544] = { return Api.bots.BotInfo.parse_botInfo($0) } dict[1012971041] = { return Api.bots.ExportedBotToken.parse_exportedBotToken($0) } dict[428978491] = { return Api.bots.PopularAppBots.parse_popularAppBots($0) } @@ -1568,6 +1583,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[963421692] = { return Api.stats.BroadcastStats.parse_broadcastStats($0) } dict[-276825834] = { return Api.stats.MegagroupStats.parse_megagroupStats($0) } dict[2145983508] = { return Api.stats.MessageStats.parse_messageStats($0) } + dict[697941741] = { return Api.stats.PollStats.parse_pollStats($0) } dict[-1828487648] = { return Api.stats.PublicForwards.parse_publicForwards($0) } dict[1355613820] = { return Api.stats.StoryStats.parse_storyStats($0) } dict[-2046910401] = { return Api.stickers.SuggestedShortName.parse_suggestedShortName($0) } @@ -1668,6 +1684,10 @@ public extension Api { switch object { case let _1 as Api.AccountDaysTTL: _1.serialize(buffer, boxed) + case let _1 as Api.AiComposeTone: + _1.serialize(buffer, boxed) + case let _1 as Api.AiComposeToneExample: + _1.serialize(buffer, boxed) case let _1 as Api.AttachMenuBot: _1.serialize(buffer, boxed) case let _1 as Api.AttachMenuBotIcon: @@ -1908,6 +1928,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.InlineQueryPeerType: _1.serialize(buffer, boxed) + case let _1 as Api.InputAiComposeTone: + _1.serialize(buffer, boxed) case let _1 as Api.InputAppEvent: _1.serialize(buffer, boxed) case let _1 as Api.InputBotApp: @@ -2470,6 +2492,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.account.WebAuthorizations: _1.serialize(buffer, boxed) + case let _1 as Api.aicompose.Tones: + _1.serialize(buffer, boxed) case let _1 as Api.auth.Authorization: _1.serialize(buffer, boxed) case let _1 as Api.auth.CodeType: @@ -2488,6 +2512,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.auth.SentCodeType: _1.serialize(buffer, boxed) + case let _1 as Api.bots.AccessSettings: + _1.serialize(buffer, boxed) case let _1 as Api.bots.BotInfo: _1.serialize(buffer, boxed) case let _1 as Api.bots.ExportedBotToken: @@ -2792,6 +2818,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.stats.MessageStats: _1.serialize(buffer, boxed) + case let _1 as Api.stats.PollStats: + _1.serialize(buffer, boxed) case let _1 as Api.stats.PublicForwards: _1.serialize(buffer, boxed) case let _1 as Api.stats.StoryStats: diff --git a/submodules/TelegramApi/Sources/Api1.swift b/submodules/TelegramApi/Sources/Api1.swift index b7c6682f33..aac4ebc52e 100644 --- a/submodules/TelegramApi/Sources/Api1.swift +++ b/submodules/TelegramApi/Sources/Api1.swift @@ -42,6 +42,221 @@ public extension Api { } } } +public extension Api { + enum AiComposeTone: TypeConstructorDescription { + public class Cons_aiComposeTone: TypeConstructorDescription { + public var flags: Int32 + public var id: Int64 + public var accessHash: Int64 + public var slug: String + public var title: String + public var emojiId: Int64? + public var prompt: String? + public var installsCount: Int32? + public var authorId: Int64? + public var exampleEnglish: Api.AiComposeToneExample? + public init(flags: Int32, id: Int64, accessHash: Int64, slug: String, title: String, emojiId: Int64?, prompt: String?, installsCount: Int32?, authorId: Int64?, exampleEnglish: Api.AiComposeToneExample?) { + self.flags = flags + self.id = id + self.accessHash = accessHash + self.slug = slug + self.title = title + self.emojiId = emojiId + self.prompt = prompt + self.installsCount = installsCount + self.authorId = authorId + self.exampleEnglish = exampleEnglish + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("aiComposeTone", [("flags", ConstructorParameterDescription(self.flags)), ("id", ConstructorParameterDescription(self.id)), ("accessHash", ConstructorParameterDescription(self.accessHash)), ("slug", ConstructorParameterDescription(self.slug)), ("title", ConstructorParameterDescription(self.title)), ("emojiId", ConstructorParameterDescription(self.emojiId)), ("prompt", ConstructorParameterDescription(self.prompt)), ("installsCount", ConstructorParameterDescription(self.installsCount)), ("authorId", ConstructorParameterDescription(self.authorId)), ("exampleEnglish", ConstructorParameterDescription(self.exampleEnglish))]) + } + } + public class Cons_aiComposeToneDefault: TypeConstructorDescription { + public var tone: String + public var emojiId: Int64 + public var title: String + public init(tone: String, emojiId: Int64, title: String) { + self.tone = tone + self.emojiId = emojiId + self.title = title + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("aiComposeToneDefault", [("tone", ConstructorParameterDescription(self.tone)), ("emojiId", ConstructorParameterDescription(self.emojiId)), ("title", ConstructorParameterDescription(self.title))]) + } + } + case aiComposeTone(Cons_aiComposeTone) + case aiComposeToneDefault(Cons_aiComposeToneDefault) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .aiComposeTone(let _data): + if boxed { + buffer.appendInt32(-805945687) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeInt64(_data.id, buffer: buffer, boxed: false) + serializeInt64(_data.accessHash, buffer: buffer, boxed: false) + serializeString(_data.slug, buffer: buffer, boxed: false) + serializeString(_data.title, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 1) != 0 { + serializeInt64(_data.emojiId!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 4) != 0 { + serializeString(_data.prompt!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 2) != 0 { + serializeInt32(_data.installsCount!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 3) != 0 { + serializeInt64(_data.authorId!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 5) != 0 { + _data.exampleEnglish!.serialize(buffer, true) + } + break + case .aiComposeToneDefault(let _data): + if boxed { + buffer.appendInt32(-1683135468) + } + serializeString(_data.tone, buffer: buffer, boxed: false) + serializeInt64(_data.emojiId, buffer: buffer, boxed: false) + serializeString(_data.title, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .aiComposeTone(let _data): + return ("aiComposeTone", [("flags", ConstructorParameterDescription(_data.flags)), ("id", ConstructorParameterDescription(_data.id)), ("accessHash", ConstructorParameterDescription(_data.accessHash)), ("slug", ConstructorParameterDescription(_data.slug)), ("title", ConstructorParameterDescription(_data.title)), ("emojiId", ConstructorParameterDescription(_data.emojiId)), ("prompt", ConstructorParameterDescription(_data.prompt)), ("installsCount", ConstructorParameterDescription(_data.installsCount)), ("authorId", ConstructorParameterDescription(_data.authorId)), ("exampleEnglish", ConstructorParameterDescription(_data.exampleEnglish))]) + case .aiComposeToneDefault(let _data): + return ("aiComposeToneDefault", [("tone", ConstructorParameterDescription(_data.tone)), ("emojiId", ConstructorParameterDescription(_data.emojiId)), ("title", ConstructorParameterDescription(_data.title))]) + } + } + + public static func parse_aiComposeTone(_ reader: BufferReader) -> AiComposeTone? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int64? + _3 = reader.readInt64() + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: Int64? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _6 = reader.readInt64() + } + var _7: String? + if Int(_1 ?? 0) & Int(1 << 4) != 0 { + _7 = parseString(reader) + } + var _8: Int32? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _8 = reader.readInt32() + } + var _9: Int64? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _9 = reader.readInt64() + } + var _10: Api.AiComposeToneExample? + if Int(_1 ?? 0) & Int(1 << 5) != 0 { + if let signature = reader.readInt32() { + _10 = Api.parse(reader, signature: signature) as? Api.AiComposeToneExample + } + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _10 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { + return Api.AiComposeTone.aiComposeTone(Cons_aiComposeTone(flags: _1!, id: _2!, accessHash: _3!, slug: _4!, title: _5!, emojiId: _6, prompt: _7, installsCount: _8, authorId: _9, exampleEnglish: _10)) + } + else { + return nil + } + } + public static func parse_aiComposeToneDefault(_ reader: BufferReader) -> AiComposeTone? { + var _1: String? + _1 = parseString(reader) + var _2: Int64? + _2 = reader.readInt64() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.AiComposeTone.aiComposeToneDefault(Cons_aiComposeToneDefault(tone: _1!, emojiId: _2!, title: _3!)) + } + else { + return nil + } + } + } +} +public extension Api { + enum AiComposeToneExample: TypeConstructorDescription { + public class Cons_aiComposeToneExample: TypeConstructorDescription { + public var from: Api.TextWithEntities + public var to: Api.TextWithEntities + public init(from: Api.TextWithEntities, to: Api.TextWithEntities) { + self.from = from + self.to = to + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("aiComposeToneExample", [("from", ConstructorParameterDescription(self.from)), ("to", ConstructorParameterDescription(self.to))]) + } + } + case aiComposeToneExample(Cons_aiComposeToneExample) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .aiComposeToneExample(let _data): + if boxed { + buffer.appendInt32(-237623060) + } + _data.from.serialize(buffer, true) + _data.to.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .aiComposeToneExample(let _data): + return ("aiComposeToneExample", [("from", ConstructorParameterDescription(_data.from)), ("to", ConstructorParameterDescription(_data.to))]) + } + } + + public static func parse_aiComposeToneExample(_ reader: BufferReader) -> AiComposeToneExample? { + var _1: Api.TextWithEntities? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } + var _2: Api.TextWithEntities? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.AiComposeToneExample.aiComposeToneExample(Cons_aiComposeToneExample(from: _1!, to: _2!)) + } + else { + return nil + } + } + } +} public extension Api { enum AttachMenuBot: TypeConstructorDescription { public class Cons_attachMenuBot: TypeConstructorDescription { @@ -103,7 +318,7 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: [Api.AttachMenuPeerType]? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.AttachMenuPeerType.self) } @@ -115,7 +330,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.AttachMenuBot.attachMenuBot(Cons_attachMenuBot(flags: _1!, botId: _2!, shortName: _3!, peerTypes: _4, icons: _5!)) @@ -182,7 +397,7 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.Document } var _4: [Api.AttachMenuBotIconColor]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.AttachMenuBotIconColor.self) } @@ -190,7 +405,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.AttachMenuBotIcon.attachMenuBotIcon(Cons_attachMenuBotIcon(flags: _1!, name: _2!, icon: _3!, colors: _4)) } @@ -803,11 +1018,11 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int64? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _2 = reader.readInt64() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _2 != nil if _c1 && _c2 { return Api.AutoSaveSettings.autoSaveSettings(Cons_autoSaveSettings(flags: _1!, videoMaxSize: _2)) } @@ -875,21 +1090,21 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = reader.readInt64() } var _5: Int64? _5 = reader.readInt64() var _6: Int64? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _6 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.AvailableEffect.availableEffect(Cons_availableEffect(flags: _1!, id: _2!, emoticon: _3!, staticIconId: _4, effectStickerId: _5!, effectAnimationId: _6)) } @@ -989,13 +1204,13 @@ public extension Api { _8 = Api.parse(reader, signature: signature) as? Api.Document } var _9: Api.Document? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.Document } } var _10: Api.Document? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.Document } @@ -1008,8 +1223,8 @@ public extension Api { let _c6 = _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _10 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { return Api.AvailableReaction.availableReaction(Cons_availableReaction(flags: _1!, reaction: _2!, title: _3!, staticIcon: _4!, appearAnimation: _5!, selectAnimation: _6!, activateAnimation: _7!, effectAnimation: _8!, aroundAnimation: _9, centerIcon: _10)) } @@ -1189,13 +1404,13 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.Birthday.birthday(Cons_birthday(flags: _1!, day: _2!, month: _3!, year: _4)) } @@ -1313,11 +1528,11 @@ public extension Api { var _2: String? _2 = parseString(reader) var _3: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt64() } var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = reader.readInt32() } var _5: Int32? @@ -1325,26 +1540,26 @@ public extension Api { var _6: Int32? _6 = reader.readInt32() var _7: String? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _7 = parseString(reader) } var _8: Int32? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _8 = reader.readInt32() } var _9: Int64? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _9 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 5) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.Boost.boost(Cons_boost(flags: _1!, id: _2!, userId: _3, giveawayMsgId: _4, date: _5!, expires: _6!, usedGiftSlug: _7, multiplier: _8, stars: _9)) } @@ -1354,208 +1569,3 @@ public extension Api { } } } -public extension Api { - enum BotApp: TypeConstructorDescription { - public class Cons_botApp: TypeConstructorDescription { - public var flags: Int32 - public var id: Int64 - public var accessHash: Int64 - public var shortName: String - public var title: String - public var description: String - public var photo: Api.Photo - public var document: Api.Document? - public var hash: Int64 - public init(flags: Int32, id: Int64, accessHash: Int64, shortName: String, title: String, description: String, photo: Api.Photo, document: Api.Document?, hash: Int64) { - self.flags = flags - self.id = id - self.accessHash = accessHash - self.shortName = shortName - self.title = title - self.description = description - self.photo = photo - self.document = document - self.hash = hash - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("botApp", [("flags", ConstructorParameterDescription(self.flags)), ("id", ConstructorParameterDescription(self.id)), ("accessHash", ConstructorParameterDescription(self.accessHash)), ("shortName", ConstructorParameterDescription(self.shortName)), ("title", ConstructorParameterDescription(self.title)), ("description", ConstructorParameterDescription(self.description)), ("photo", ConstructorParameterDescription(self.photo)), ("document", ConstructorParameterDescription(self.document)), ("hash", ConstructorParameterDescription(self.hash))]) - } - } - case botApp(Cons_botApp) - case botAppNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .botApp(let _data): - if boxed { - buffer.appendInt32(-1778593322) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeInt64(_data.id, buffer: buffer, boxed: false) - serializeInt64(_data.accessHash, buffer: buffer, boxed: false) - serializeString(_data.shortName, buffer: buffer, boxed: false) - serializeString(_data.title, buffer: buffer, boxed: false) - serializeString(_data.description, buffer: buffer, boxed: false) - _data.photo.serialize(buffer, true) - if Int(_data.flags) & Int(1 << 0) != 0 { - _data.document!.serialize(buffer, true) - } - serializeInt64(_data.hash, buffer: buffer, boxed: false) - break - case .botAppNotModified: - if boxed { - buffer.appendInt32(1571189943) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .botApp(let _data): - return ("botApp", [("flags", ConstructorParameterDescription(_data.flags)), ("id", ConstructorParameterDescription(_data.id)), ("accessHash", ConstructorParameterDescription(_data.accessHash)), ("shortName", ConstructorParameterDescription(_data.shortName)), ("title", ConstructorParameterDescription(_data.title)), ("description", ConstructorParameterDescription(_data.description)), ("photo", ConstructorParameterDescription(_data.photo)), ("document", ConstructorParameterDescription(_data.document)), ("hash", ConstructorParameterDescription(_data.hash))]) - case .botAppNotModified: - return ("botAppNotModified", []) - } - } - - public static func parse_botApp(_ reader: BufferReader) -> BotApp? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int64? - _3 = reader.readInt64() - var _4: String? - _4 = parseString(reader) - var _5: String? - _5 = parseString(reader) - var _6: String? - _6 = parseString(reader) - var _7: Api.Photo? - if let signature = reader.readInt32() { - _7 = Api.parse(reader, signature: signature) as? Api.Photo - } - var _8: Api.Document? - if Int(_1!) & Int(1 << 0) != 0 { - if let signature = reader.readInt32() { - _8 = Api.parse(reader, signature: signature) as? Api.Document - } - } - var _9: Int64? - _9 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 0) == 0) || _8 != nil - let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.BotApp.botApp(Cons_botApp(flags: _1!, id: _2!, accessHash: _3!, shortName: _4!, title: _5!, description: _6!, photo: _7!, document: _8, hash: _9!)) - } - else { - return nil - } - } - public static func parse_botAppNotModified(_ reader: BufferReader) -> BotApp? { - return Api.BotApp.botAppNotModified - } - } -} -public extension Api { - enum BotAppSettings: TypeConstructorDescription { - public class Cons_botAppSettings: TypeConstructorDescription { - public var flags: Int32 - public var placeholderPath: Buffer? - public var backgroundColor: Int32? - public var backgroundDarkColor: Int32? - public var headerColor: Int32? - public var headerDarkColor: Int32? - public init(flags: Int32, placeholderPath: Buffer?, backgroundColor: Int32?, backgroundDarkColor: Int32?, headerColor: Int32?, headerDarkColor: Int32?) { - self.flags = flags - self.placeholderPath = placeholderPath - self.backgroundColor = backgroundColor - self.backgroundDarkColor = backgroundDarkColor - self.headerColor = headerColor - self.headerDarkColor = headerDarkColor - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("botAppSettings", [("flags", ConstructorParameterDescription(self.flags)), ("placeholderPath", ConstructorParameterDescription(self.placeholderPath)), ("backgroundColor", ConstructorParameterDescription(self.backgroundColor)), ("backgroundDarkColor", ConstructorParameterDescription(self.backgroundDarkColor)), ("headerColor", ConstructorParameterDescription(self.headerColor)), ("headerDarkColor", ConstructorParameterDescription(self.headerDarkColor))]) - } - } - case botAppSettings(Cons_botAppSettings) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .botAppSettings(let _data): - if boxed { - buffer.appendInt32(-912582320) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeBytes(_data.placeholderPath!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 1) != 0 { - serializeInt32(_data.backgroundColor!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 2) != 0 { - serializeInt32(_data.backgroundDarkColor!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 3) != 0 { - serializeInt32(_data.headerColor!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 4) != 0 { - serializeInt32(_data.headerDarkColor!, buffer: buffer, boxed: false) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .botAppSettings(let _data): - return ("botAppSettings", [("flags", ConstructorParameterDescription(_data.flags)), ("placeholderPath", ConstructorParameterDescription(_data.placeholderPath)), ("backgroundColor", ConstructorParameterDescription(_data.backgroundColor)), ("backgroundDarkColor", ConstructorParameterDescription(_data.backgroundDarkColor)), ("headerColor", ConstructorParameterDescription(_data.headerColor)), ("headerDarkColor", ConstructorParameterDescription(_data.headerDarkColor))]) - } - } - - public static func parse_botAppSettings(_ reader: BufferReader) -> BotAppSettings? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Buffer? - if Int(_1!) & Int(1 << 0) != 0 { - _2 = parseBytes(reader) - } - var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 { - _3 = reader.readInt32() - } - var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 { - _4 = reader.readInt32() - } - var _5: Int32? - if Int(_1!) & Int(1 << 3) != 0 { - _5 = reader.readInt32() - } - var _6: Int32? - if Int(_1!) & Int(1 << 4) != 0 { - _6 = reader.readInt32() - } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.BotAppSettings.botAppSettings(Cons_botAppSettings(flags: _1!, placeholderPath: _2, backgroundColor: _3, backgroundDarkColor: _4, headerColor: _5, headerDarkColor: _6)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api10.swift b/submodules/TelegramApi/Sources/Api10.swift index 1aa0f1f987..f9dbfbe0e0 100644 --- a/submodules/TelegramApi/Sources/Api10.swift +++ b/submodules/TelegramApi/Sources/Api10.swift @@ -323,19 +323,19 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() var _5: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = reader.readInt32() } var _6: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.InputFileLocation.inputGroupCallStream(Cons_inputGroupCallStream(flags: _1!, call: _2!, timeMs: _3!, scale: _4!, videoChannel: _5, videoQuality: _6)) } @@ -640,13 +640,13 @@ public extension Api { var _3: Double? _3 = reader.readDouble() var _4: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.InputGeoPoint.inputGeoPoint(Cons_inputGeoPoint(flags: _1!, lat: _2!, long: _3!, accuracyRadius: _4)) } @@ -1196,7 +1196,7 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Api.TextWithEntities? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } @@ -1204,7 +1204,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.InputInvoice.inputInvoicePremiumGiftStars(Cons_inputInvoicePremiumGiftStars(flags: _1!, userId: _2!, months: _3!, message: _4)) } @@ -1233,7 +1233,7 @@ public extension Api { var _3: Int64? _3 = reader.readInt64() var _4: Api.TextWithEntities? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } @@ -1241,7 +1241,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.InputInvoice.inputInvoiceStarGift(Cons_inputInvoiceStarGift(flags: _1!, peer: _2!, giftId: _3!, message: _4)) } @@ -1253,7 +1253,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.InputPeer? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.InputPeer } @@ -1263,16 +1263,16 @@ public extension Api { var _4: Int64? _4 = reader.readInt64() var _5: Api.TextWithEntities? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 3) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.InputInvoice.inputInvoiceStarGiftAuctionBid(Cons_inputInvoiceStarGiftAuctionBid(flags: _1!, peer: _2, giftId: _3!, bidAmount: _4!, message: _5)) } diff --git a/submodules/TelegramApi/Sources/Api11.swift b/submodules/TelegramApi/Sources/Api11.swift index 5c9e6d7e3d..dc425a13f2 100644 --- a/submodules/TelegramApi/Sources/Api11.swift +++ b/submodules/TelegramApi/Sources/Api11.swift @@ -666,29 +666,29 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.InputDocument } var _3: Api.InputPhoto? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.InputPhoto } } var _4: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _4 = reader.readInt32() } var _5: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = reader.readInt32() } var _6: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _6 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.InputMedia.inputMediaDocument(Cons_inputMediaDocument(flags: _1!, id: _2!, videoCover: _3, videoTimestamp: _4, ttlSeconds: _5, query: _6)) } @@ -702,24 +702,24 @@ public extension Api { var _2: String? _2 = parseString(reader) var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Api.InputPhoto? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.InputPhoto } } var _5: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.InputMedia.inputMediaDocumentExternal(Cons_inputMediaDocumentExternal(flags: _1!, url: _2!, ttlSeconds: _3, videoCover: _4, videoTimestamp: _5)) } @@ -751,22 +751,22 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.InputGeoPoint } var _3: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _3 = reader.readInt32() } var _4: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = reader.readInt32() } var _5: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.InputMedia.inputMediaGeoLive(Cons_inputMediaGeoLive(flags: _1!, geoPoint: _2!, heading: _3, period: _4, proximityNotificationRadius: _5)) } @@ -795,7 +795,7 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Api.InputWebDocument? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.InputWebDocument } @@ -807,7 +807,7 @@ public extension Api { var _6: Buffer? _6 = parseBytes(reader) var _7: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _7 = parseString(reader) } var _8: Api.DataJSON? @@ -815,11 +815,11 @@ public extension Api { _8 = Api.parse(reader, signature: signature) as? Api.DataJSON } var _9: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _9 = parseString(reader) } var _10: Api.InputMedia? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.InputMedia } @@ -827,13 +827,13 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _7 != nil let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 2) == 0) || _10 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _10 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { return Api.InputMedia.inputMediaInvoice(Cons_inputMediaInvoice(flags: _1!, title: _2!, description: _3!, photo: _4, invoice: _5!, payload: _6!, provider: _7, providerData: _8!, startParam: _9, extendedMedia: _10)) } @@ -851,13 +851,13 @@ public extension Api { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputMedia.self) } var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.InputMedia.inputMediaPaidMedia(Cons_inputMediaPaidMedia(flags: _1!, starsAmount: _2!, extendedMedia: _3!, payload: _4)) } @@ -873,19 +873,19 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.InputPhoto } var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Api.InputDocument? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.InputDocument } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.InputMedia.inputMediaPhoto(Cons_inputMediaPhoto(flags: _1!, id: _2!, ttlSeconds: _3, video: _4)) } @@ -899,12 +899,12 @@ public extension Api { var _2: String? _2 = parseString(reader) var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.InputMedia.inputMediaPhotoExternal(Cons_inputMediaPhotoExternal(flags: _1!, url: _2!, ttlSeconds: _3)) } @@ -920,40 +920,40 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.Poll } var _3: [Int32]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } } var _4: Api.InputMedia? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.InputMedia } } var _5: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = parseString(reader) } var _6: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _7: Api.InputMedia? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.InputMedia } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.InputMedia.inputMediaPoll(Cons_inputMediaPoll(flags: _1!, poll: _2!, correctAnswers: _3, attachedMedia: _4, solution: _5, solutionEntities: _6, solutionMedia: _7)) } @@ -1015,7 +1015,7 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.InputFile } var _3: Api.InputFile? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.InputFile } @@ -1027,34 +1027,34 @@ public extension Api { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DocumentAttribute.self) } var _6: [Api.InputDocument]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputDocument.self) } } var _7: Api.InputPhoto? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.InputPhoto } } var _8: Int32? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _8 = reader.readInt32() } var _9: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _9 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 6) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 7) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.InputMedia.inputMediaUploadedDocument(Cons_inputMediaUploadedDocument(flags: _1!, file: _2!, thumb: _3, mimeType: _4!, attributes: _5!, stickers: _6, videoCover: _7, videoTimestamp: _8, ttlSeconds: _9)) } @@ -1070,26 +1070,26 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.InputFile } var _3: [Api.InputDocument]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputDocument.self) } } var _4: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = reader.readInt32() } var _5: Api.InputDocument? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.InputDocument } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.InputMedia.inputMediaUploadedPhoto(Cons_inputMediaUploadedPhoto(flags: _1!, file: _2!, stickers: _3, ttlSeconds: _4, video: _5)) } diff --git a/submodules/TelegramApi/Sources/Api12.swift b/submodules/TelegramApi/Sources/Api12.swift index 0fd5371719..89a9c99847 100644 --- a/submodules/TelegramApi/Sources/Api12.swift +++ b/submodules/TelegramApi/Sources/Api12.swift @@ -540,53 +540,53 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Bool? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Bool } } var _3: Api.Bool? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Bool } } var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = reader.readInt32() } var _5: Api.NotificationSound? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.NotificationSound } } var _6: Api.Bool? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Bool } } var _7: Api.Bool? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.Bool } } var _8: Api.NotificationSound? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.NotificationSound } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 6) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 7) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 8) == 0) || _8 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.InputPeerNotifySettings.inputPeerNotifySettings(Cons_inputPeerNotifySettings(flags: _1!, showPreviews: _2, silent: _3, muteUntil: _4, sound: _5, storiesMuted: _6, storiesHideSender: _7, storiesSound: _8)) } diff --git a/submodules/TelegramApi/Sources/Api13.swift b/submodules/TelegramApi/Sources/Api13.swift index 1501d492d3..cd4f464e19 100644 --- a/submodules/TelegramApi/Sources/Api13.swift +++ b/submodules/TelegramApi/Sources/Api13.swift @@ -194,53 +194,53 @@ public extension Api { var _2: Int32? _2 = reader.readInt32() var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Api.InputPeer? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.InputPeer } } var _5: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = parseString(reader) } var _6: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let _ = reader.readInt32() { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _7: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _7 = reader.readInt32() } var _8: Api.InputPeer? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.InputPeer } } var _9: Int32? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _9 = reader.readInt32() } var _10: Buffer? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _10 = parseBytes(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 5) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 7) == 0) || _10 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _10 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { return Api.InputReplyTo.inputReplyToMessage(Cons_inputReplyToMessage(flags: _1!, replyToMsgId: _2!, topMsgId: _3, replyToPeerId: _4, quoteText: _5, quoteEntities: _6, quoteOffset: _7, monoforumPeerId: _8, todoItemId: _9, pollOption: _10)) } @@ -577,56 +577,56 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.SecureValueType } var _3: Api.SecureData? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.SecureData } } var _4: Api.InputSecureFile? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.InputSecureFile } } var _5: Api.InputSecureFile? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.InputSecureFile } } var _6: Api.InputSecureFile? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.InputSecureFile } } var _7: [Api.InputSecureFile]? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let _ = reader.readInt32() { _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputSecureFile.self) } } var _8: [Api.InputSecureFile]? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let _ = reader.readInt32() { _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputSecureFile.self) } } var _9: Api.SecurePlainData? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.SecurePlainData } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 6) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 5) == 0) || _9 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.InputSecureValue.inputSecureValue(Cons_inputSecureValue(flags: _1!, type: _2!, data: _3, frontSide: _4, reverseSide: _5, selfie: _6, translation: _7, files: _8, plainData: _9)) } @@ -697,7 +697,7 @@ public extension Api { var _4: String? _4 = parseString(reader) var _5: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } @@ -706,7 +706,7 @@ public extension Api { let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.InputSingleMedia.inputSingleMedia(Cons_inputSingleMedia(flags: _1!, media: _2!, randomId: _3!, message: _4!, entities: _5)) } @@ -1103,20 +1103,20 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Api.MaskCoords? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.MaskCoords } } var _5: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.InputStickerSetItem.inputStickerSetItem(Cons_inputStickerSetItem(flags: _1!, document: _2!, emoji: _3!, maskCoords: _4, keywords: _5)) } @@ -1209,17 +1209,19 @@ public extension Api { public var flags: Int32 public var phoneNumber: String public var phoneCodeHash: String + public var premiumDays: Int32 public var currency: String public var amount: Int64 - public init(flags: Int32, phoneNumber: String, phoneCodeHash: String, currency: String, amount: Int64) { + public init(flags: Int32, phoneNumber: String, phoneCodeHash: String, premiumDays: Int32, currency: String, amount: Int64) { self.flags = flags self.phoneNumber = phoneNumber self.phoneCodeHash = phoneCodeHash + self.premiumDays = premiumDays self.currency = currency self.amount = amount } public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputStorePaymentAuthCode", [("flags", ConstructorParameterDescription(self.flags)), ("phoneNumber", ConstructorParameterDescription(self.phoneNumber)), ("phoneCodeHash", ConstructorParameterDescription(self.phoneCodeHash)), ("currency", ConstructorParameterDescription(self.currency)), ("amount", ConstructorParameterDescription(self.amount))]) + return ("inputStorePaymentAuthCode", [("flags", ConstructorParameterDescription(self.flags)), ("phoneNumber", ConstructorParameterDescription(self.phoneNumber)), ("phoneCodeHash", ConstructorParameterDescription(self.phoneCodeHash)), ("premiumDays", ConstructorParameterDescription(self.premiumDays)), ("currency", ConstructorParameterDescription(self.currency)), ("amount", ConstructorParameterDescription(self.amount))]) } } public class Cons_inputStorePaymentGiftPremium: TypeConstructorDescription { @@ -1362,11 +1364,12 @@ public extension Api { switch self { case .inputStorePaymentAuthCode(let _data): if boxed { - buffer.appendInt32(-1682807955) + buffer.appendInt32(1069645911) } serializeInt32(_data.flags, buffer: buffer, boxed: false) serializeString(_data.phoneNumber, buffer: buffer, boxed: false) serializeString(_data.phoneCodeHash, buffer: buffer, boxed: false) + serializeInt32(_data.premiumDays, buffer: buffer, boxed: false) serializeString(_data.currency, buffer: buffer, boxed: false) serializeInt64(_data.amount, buffer: buffer, boxed: false) break @@ -1488,7 +1491,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { switch self { case .inputStorePaymentAuthCode(let _data): - return ("inputStorePaymentAuthCode", [("flags", ConstructorParameterDescription(_data.flags)), ("phoneNumber", ConstructorParameterDescription(_data.phoneNumber)), ("phoneCodeHash", ConstructorParameterDescription(_data.phoneCodeHash)), ("currency", ConstructorParameterDescription(_data.currency)), ("amount", ConstructorParameterDescription(_data.amount))]) + return ("inputStorePaymentAuthCode", [("flags", ConstructorParameterDescription(_data.flags)), ("phoneNumber", ConstructorParameterDescription(_data.phoneNumber)), ("phoneCodeHash", ConstructorParameterDescription(_data.phoneCodeHash)), ("premiumDays", ConstructorParameterDescription(_data.premiumDays)), ("currency", ConstructorParameterDescription(_data.currency)), ("amount", ConstructorParameterDescription(_data.amount))]) case .inputStorePaymentGiftPremium(let _data): return ("inputStorePaymentGiftPremium", [("userId", ConstructorParameterDescription(_data.userId)), ("currency", ConstructorParameterDescription(_data.currency)), ("amount", ConstructorParameterDescription(_data.amount))]) case .inputStorePaymentPremiumGiftCode(let _data): @@ -1513,17 +1516,20 @@ public extension Api { _2 = parseString(reader) var _3: String? _3 = parseString(reader) - var _4: String? - _4 = parseString(reader) - var _5: Int64? - _5 = reader.readInt64() + var _4: Int32? + _4 = reader.readInt32() + var _5: String? + _5 = parseString(reader) + var _6: Int64? + _6 = reader.readInt64() let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.InputStorePaymentPurpose.inputStorePaymentAuthCode(Cons_inputStorePaymentAuthCode(flags: _1!, phoneNumber: _2!, phoneCodeHash: _3!, currency: _4!, amount: _5!)) + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.InputStorePaymentPurpose.inputStorePaymentAuthCode(Cons_inputStorePaymentAuthCode(flags: _1!, phoneNumber: _2!, phoneCodeHash: _3!, premiumDays: _4!, currency: _5!, amount: _6!)) } else { return nil @@ -1556,7 +1562,7 @@ public extension Api { _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) } var _3: Api.InputPeer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.InputPeer } @@ -1566,17 +1572,17 @@ public extension Api { var _5: Int64? _5 = reader.readInt64() var _6: Api.TextWithEntities? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.InputStorePaymentPurpose.inputStorePaymentPremiumGiftCode(Cons_inputStorePaymentPremiumGiftCode(flags: _1!, users: _2!, boostPeer: _3, currency: _4!, amount: _5!, message: _6)) } @@ -1592,19 +1598,19 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.InputPeer } var _3: [Api.InputPeer]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self) } } var _4: [String]? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) } } var _5: String? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _5 = parseString(reader) } var _6: Int64? @@ -1617,9 +1623,9 @@ public extension Api { _9 = reader.readInt64() let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil @@ -1674,19 +1680,19 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.InputPeer } var _4: [Api.InputPeer]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputPeer.self) } } var _5: [String]? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let _ = reader.readInt32() { _5 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) } } var _6: String? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _6 = parseString(reader) } var _7: Int64? @@ -1702,9 +1708,9 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil let _c9 = _9 != nil @@ -1727,7 +1733,7 @@ public extension Api { var _4: Int64? _4 = reader.readInt64() var _5: Api.InputPeer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.InputPeer } @@ -1736,7 +1742,7 @@ public extension Api { let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.InputStorePaymentPurpose.inputStorePaymentStarsTopup(Cons_inputStorePaymentStarsTopup(flags: _1!, stars: _2!, currency: _3!, amount: _4!, spendPurposePeer: _5)) } diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index 3d3e270c25..732969a330 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -148,23 +148,23 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _4 = reader.readInt32() } var _5: [Int32]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _5 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } } var _6: Api.InputWallPaper? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.InputWallPaper } } var _7: Api.WallPaperSettings? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.WallPaperSettings } @@ -172,10 +172,10 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.InputThemeSettings.inputThemeSettings(Cons_inputThemeSettings(flags: _1!, baseTheme: _2!, accentColor: _3!, outboxAccentColor: _4, messageColors: _5, wallpaper: _6, wallpaperSettings: _7)) } @@ -580,23 +580,23 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.InputDocument? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.InputDocument } } var _3: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = parseString(reader) } var _4: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = parseString(reader) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.InputWebFileLocation.inputWebFileAudioAlbumThumbLocation(Cons_inputWebFileAudioAlbumThumbLocation(flags: _1!, document: _2, title: _3, performer: _4)) } @@ -723,30 +723,30 @@ public extension Api { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.LabeledPrice.self) } var _4: Int64? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _4 = reader.readInt64() } var _5: [Int64]? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { if let _ = reader.readInt32() { _5 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } } var _6: String? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { _6 = parseString(reader) } var _7: Int32? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _7 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 8) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 8) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 10) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.Invoice.invoice(Cons_invoice(flags: _1!, currency: _2!, prices: _3!, maxTipAmount: _4, suggestedTipAmounts: _5, termsUrl: _6, subscriptionPeriod: _7)) } @@ -1556,7 +1556,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1572,7 +1572,7 @@ public extension Api { var _6: Int32? _6 = reader.readInt32() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil @@ -1588,7 +1588,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1596,7 +1596,7 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = parseString(reader) } var _5: String? @@ -1606,9 +1606,9 @@ public extension Api { _6 = Api.parse(reader, signature: signature) as? Api.InputUser } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { @@ -1622,7 +1622,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1634,7 +1634,7 @@ public extension Api { _4 = Api.parse(reader, signature: signature) as? Api.InputUser } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { @@ -1648,7 +1648,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1656,7 +1656,7 @@ public extension Api { var _3: String? _3 = parseString(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { return Api.KeyboardButton.keyboardButton(Cons_keyboardButton(flags: _1!, style: _2, text: _3!)) @@ -1669,7 +1669,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1677,7 +1677,7 @@ public extension Api { var _3: String? _3 = parseString(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { return Api.KeyboardButton.keyboardButtonBuy(Cons_keyboardButtonBuy(flags: _1!, style: _2, text: _3!)) @@ -1690,7 +1690,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1700,7 +1700,7 @@ public extension Api { var _4: Buffer? _4 = parseBytes(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { @@ -1714,7 +1714,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1724,7 +1724,7 @@ public extension Api { var _4: String? _4 = parseString(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { @@ -1738,7 +1738,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1746,7 +1746,7 @@ public extension Api { var _3: String? _3 = parseString(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { return Api.KeyboardButton.keyboardButtonGame(Cons_keyboardButtonGame(flags: _1!, style: _2, text: _3!)) @@ -1759,7 +1759,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1767,7 +1767,7 @@ public extension Api { var _3: String? _3 = parseString(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { return Api.KeyboardButton.keyboardButtonRequestGeoLocation(Cons_keyboardButtonRequestGeoLocation(flags: _1!, style: _2, text: _3!)) @@ -1780,7 +1780,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1796,7 +1796,7 @@ public extension Api { var _6: Int32? _6 = reader.readInt32() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil @@ -1812,7 +1812,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1820,7 +1820,7 @@ public extension Api { var _3: String? _3 = parseString(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { return Api.KeyboardButton.keyboardButtonRequestPhone(Cons_keyboardButtonRequestPhone(flags: _1!, style: _2, text: _3!)) @@ -1833,13 +1833,13 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } } var _3: Api.Bool? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Bool } @@ -1847,8 +1847,8 @@ public extension Api { var _4: String? _4 = parseString(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.KeyboardButton.keyboardButtonRequestPoll(Cons_keyboardButtonRequestPoll(flags: _1!, style: _2, quiz: _3, text: _4!)) @@ -1861,7 +1861,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1871,7 +1871,7 @@ public extension Api { var _4: String? _4 = parseString(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { @@ -1885,7 +1885,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1895,16 +1895,16 @@ public extension Api { var _4: String? _4 = parseString(reader) var _5: [Api.InlineQueryPeerType]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InlineQueryPeerType.self) } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.KeyboardButton.keyboardButtonSwitchInline(Cons_keyboardButtonSwitchInline(flags: _1!, style: _2, text: _3!, query: _4!, peerTypes: _5)) } @@ -1916,7 +1916,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1926,7 +1926,7 @@ public extension Api { var _4: String? _4 = parseString(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { @@ -1940,7 +1940,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1948,7 +1948,7 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } var _5: String? @@ -1956,9 +1956,9 @@ public extension Api { var _6: Int32? _6 = reader.readInt32() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { @@ -1972,7 +1972,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -1982,7 +1982,7 @@ public extension Api { var _4: Int64? _4 = reader.readInt64() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { @@ -1996,7 +1996,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.KeyboardButtonStyle? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.KeyboardButtonStyle } @@ -2006,7 +2006,7 @@ public extension Api { var _4: String? _4 = parseString(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 10) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index 2fb0d8d59e..6e05447f93 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -88,11 +88,11 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int64? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _2 = reader.readInt64() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 3) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _2 != nil if _c1 && _c2 { return Api.KeyboardButtonStyle.keyboardButtonStyle(Cons_keyboardButtonStyle(flags: _1!, icon: _2)) } @@ -287,7 +287,7 @@ public extension Api { var _4: String? _4 = parseString(reader) var _5: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = parseString(reader) } var _6: String? @@ -302,7 +302,7 @@ public extension Api { let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil @@ -448,34 +448,34 @@ public extension Api { var _2: String? _2 = parseString(reader) var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } var _4: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = parseString(reader) } var _5: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = parseString(reader) } var _6: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _6 = parseString(reader) } var _7: String? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _7 = parseString(reader) } var _8: String? _8 = parseString(reader) let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _7 != nil let _c8 = _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.LangPackString.langPackStringPluralized(Cons_langPackStringPluralized(flags: _1!, key: _2!, zeroValue: _3, oneValue: _4, twoValue: _5, fewValue: _6, manyValue: _7, otherValue: _8!)) @@ -862,7 +862,7 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.GeoPoint } var _4: Api.GeoPointAddress? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.GeoPointAddress } @@ -870,7 +870,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.MediaArea.mediaAreaGeoPoint(Cons_mediaAreaGeoPoint(flags: _1!, coordinates: _2!, geo: _3!, address: _4)) } @@ -1053,7 +1053,7 @@ public extension Api { var _6: Double? _6 = reader.readDouble() var _7: Double? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _7 = reader.readDouble() } let _c1 = _1 != nil @@ -1062,7 +1062,7 @@ public extension Api { let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.MediaAreaCoordinates.mediaAreaCoordinates(Cons_mediaAreaCoordinates(flags: _1!, x: _2!, y: _3!, w: _4!, h: _5!, rotation: _6!, radius: _7)) } @@ -1086,6 +1086,7 @@ public extension Api { public var fwdFrom: Api.MessageFwdHeader? public var viaBotId: Int64? public var viaBusinessBotId: Int64? + public var guestchatViaFrom: Api.Peer? public var replyTo: Api.MessageReplyHeader? public var date: Int32 public var message: String @@ -1109,7 +1110,7 @@ public extension Api { public var suggestedPost: Api.SuggestedPost? public var scheduleRepeatPeriod: Int32? public var summaryFromLanguage: String? - public init(flags: Int32, flags2: Int32, id: Int32, fromId: Api.Peer?, fromBoostsApplied: Int32?, fromRank: String?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, viaBusinessBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?, quickReplyShortcutId: Int32?, effect: Int64?, factcheck: Api.FactCheck?, reportDeliveryUntilDate: Int32?, paidMessageStars: Int64?, suggestedPost: Api.SuggestedPost?, scheduleRepeatPeriod: Int32?, summaryFromLanguage: String?) { + public init(flags: Int32, flags2: Int32, id: Int32, fromId: Api.Peer?, fromBoostsApplied: Int32?, fromRank: String?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, viaBusinessBotId: Int64?, guestchatViaFrom: Api.Peer?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?, quickReplyShortcutId: Int32?, effect: Int64?, factcheck: Api.FactCheck?, reportDeliveryUntilDate: Int32?, paidMessageStars: Int64?, suggestedPost: Api.SuggestedPost?, scheduleRepeatPeriod: Int32?, summaryFromLanguage: String?) { self.flags = flags self.flags2 = flags2 self.id = id @@ -1121,6 +1122,7 @@ public extension Api { self.fwdFrom = fwdFrom self.viaBotId = viaBotId self.viaBusinessBotId = viaBusinessBotId + self.guestchatViaFrom = guestchatViaFrom self.replyTo = replyTo self.date = date self.message = message @@ -1146,7 +1148,7 @@ public extension Api { self.summaryFromLanguage = summaryFromLanguage } public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("message", [("flags", ConstructorParameterDescription(self.flags)), ("flags2", ConstructorParameterDescription(self.flags2)), ("id", ConstructorParameterDescription(self.id)), ("fromId", ConstructorParameterDescription(self.fromId)), ("fromBoostsApplied", ConstructorParameterDescription(self.fromBoostsApplied)), ("fromRank", ConstructorParameterDescription(self.fromRank)), ("peerId", ConstructorParameterDescription(self.peerId)), ("savedPeerId", ConstructorParameterDescription(self.savedPeerId)), ("fwdFrom", ConstructorParameterDescription(self.fwdFrom)), ("viaBotId", ConstructorParameterDescription(self.viaBotId)), ("viaBusinessBotId", ConstructorParameterDescription(self.viaBusinessBotId)), ("replyTo", ConstructorParameterDescription(self.replyTo)), ("date", ConstructorParameterDescription(self.date)), ("message", ConstructorParameterDescription(self.message)), ("media", ConstructorParameterDescription(self.media)), ("replyMarkup", ConstructorParameterDescription(self.replyMarkup)), ("entities", ConstructorParameterDescription(self.entities)), ("views", ConstructorParameterDescription(self.views)), ("forwards", ConstructorParameterDescription(self.forwards)), ("replies", ConstructorParameterDescription(self.replies)), ("editDate", ConstructorParameterDescription(self.editDate)), ("postAuthor", ConstructorParameterDescription(self.postAuthor)), ("groupedId", ConstructorParameterDescription(self.groupedId)), ("reactions", ConstructorParameterDescription(self.reactions)), ("restrictionReason", ConstructorParameterDescription(self.restrictionReason)), ("ttlPeriod", ConstructorParameterDescription(self.ttlPeriod)), ("quickReplyShortcutId", ConstructorParameterDescription(self.quickReplyShortcutId)), ("effect", ConstructorParameterDescription(self.effect)), ("factcheck", ConstructorParameterDescription(self.factcheck)), ("reportDeliveryUntilDate", ConstructorParameterDescription(self.reportDeliveryUntilDate)), ("paidMessageStars", ConstructorParameterDescription(self.paidMessageStars)), ("suggestedPost", ConstructorParameterDescription(self.suggestedPost)), ("scheduleRepeatPeriod", ConstructorParameterDescription(self.scheduleRepeatPeriod)), ("summaryFromLanguage", ConstructorParameterDescription(self.summaryFromLanguage))]) + return ("message", [("flags", ConstructorParameterDescription(self.flags)), ("flags2", ConstructorParameterDescription(self.flags2)), ("id", ConstructorParameterDescription(self.id)), ("fromId", ConstructorParameterDescription(self.fromId)), ("fromBoostsApplied", ConstructorParameterDescription(self.fromBoostsApplied)), ("fromRank", ConstructorParameterDescription(self.fromRank)), ("peerId", ConstructorParameterDescription(self.peerId)), ("savedPeerId", ConstructorParameterDescription(self.savedPeerId)), ("fwdFrom", ConstructorParameterDescription(self.fwdFrom)), ("viaBotId", ConstructorParameterDescription(self.viaBotId)), ("viaBusinessBotId", ConstructorParameterDescription(self.viaBusinessBotId)), ("guestchatViaFrom", ConstructorParameterDescription(self.guestchatViaFrom)), ("replyTo", ConstructorParameterDescription(self.replyTo)), ("date", ConstructorParameterDescription(self.date)), ("message", ConstructorParameterDescription(self.message)), ("media", ConstructorParameterDescription(self.media)), ("replyMarkup", ConstructorParameterDescription(self.replyMarkup)), ("entities", ConstructorParameterDescription(self.entities)), ("views", ConstructorParameterDescription(self.views)), ("forwards", ConstructorParameterDescription(self.forwards)), ("replies", ConstructorParameterDescription(self.replies)), ("editDate", ConstructorParameterDescription(self.editDate)), ("postAuthor", ConstructorParameterDescription(self.postAuthor)), ("groupedId", ConstructorParameterDescription(self.groupedId)), ("reactions", ConstructorParameterDescription(self.reactions)), ("restrictionReason", ConstructorParameterDescription(self.restrictionReason)), ("ttlPeriod", ConstructorParameterDescription(self.ttlPeriod)), ("quickReplyShortcutId", ConstructorParameterDescription(self.quickReplyShortcutId)), ("effect", ConstructorParameterDescription(self.effect)), ("factcheck", ConstructorParameterDescription(self.factcheck)), ("reportDeliveryUntilDate", ConstructorParameterDescription(self.reportDeliveryUntilDate)), ("paidMessageStars", ConstructorParameterDescription(self.paidMessageStars)), ("suggestedPost", ConstructorParameterDescription(self.suggestedPost)), ("scheduleRepeatPeriod", ConstructorParameterDescription(self.scheduleRepeatPeriod)), ("summaryFromLanguage", ConstructorParameterDescription(self.summaryFromLanguage))]) } } public class Cons_messageEmpty: TypeConstructorDescription { @@ -1197,7 +1199,7 @@ public extension Api { switch self { case .message(let _data): if boxed { - buffer.appendInt32(988112002) + buffer.appendInt32(-1779470549) } serializeInt32(_data.flags, buffer: buffer, boxed: false) serializeInt32(_data.flags2, buffer: buffer, boxed: false) @@ -1224,6 +1226,9 @@ public extension Api { if Int(_data.flags2) & Int(1 << 0) != 0 { serializeInt64(_data.viaBusinessBotId!, buffer: buffer, boxed: false) } + if Int(_data.flags2) & Int(1 << 19) != 0 { + _data.guestchatViaFrom!.serialize(buffer, true) + } if Int(_data.flags) & Int(1 << 3) != 0 { _data.replyTo!.serialize(buffer, true) } @@ -1339,7 +1344,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { switch self { case .message(let _data): - return ("message", [("flags", ConstructorParameterDescription(_data.flags)), ("flags2", ConstructorParameterDescription(_data.flags2)), ("id", ConstructorParameterDescription(_data.id)), ("fromId", ConstructorParameterDescription(_data.fromId)), ("fromBoostsApplied", ConstructorParameterDescription(_data.fromBoostsApplied)), ("fromRank", ConstructorParameterDescription(_data.fromRank)), ("peerId", ConstructorParameterDescription(_data.peerId)), ("savedPeerId", ConstructorParameterDescription(_data.savedPeerId)), ("fwdFrom", ConstructorParameterDescription(_data.fwdFrom)), ("viaBotId", ConstructorParameterDescription(_data.viaBotId)), ("viaBusinessBotId", ConstructorParameterDescription(_data.viaBusinessBotId)), ("replyTo", ConstructorParameterDescription(_data.replyTo)), ("date", ConstructorParameterDescription(_data.date)), ("message", ConstructorParameterDescription(_data.message)), ("media", ConstructorParameterDescription(_data.media)), ("replyMarkup", ConstructorParameterDescription(_data.replyMarkup)), ("entities", ConstructorParameterDescription(_data.entities)), ("views", ConstructorParameterDescription(_data.views)), ("forwards", ConstructorParameterDescription(_data.forwards)), ("replies", ConstructorParameterDescription(_data.replies)), ("editDate", ConstructorParameterDescription(_data.editDate)), ("postAuthor", ConstructorParameterDescription(_data.postAuthor)), ("groupedId", ConstructorParameterDescription(_data.groupedId)), ("reactions", ConstructorParameterDescription(_data.reactions)), ("restrictionReason", ConstructorParameterDescription(_data.restrictionReason)), ("ttlPeriod", ConstructorParameterDescription(_data.ttlPeriod)), ("quickReplyShortcutId", ConstructorParameterDescription(_data.quickReplyShortcutId)), ("effect", ConstructorParameterDescription(_data.effect)), ("factcheck", ConstructorParameterDescription(_data.factcheck)), ("reportDeliveryUntilDate", ConstructorParameterDescription(_data.reportDeliveryUntilDate)), ("paidMessageStars", ConstructorParameterDescription(_data.paidMessageStars)), ("suggestedPost", ConstructorParameterDescription(_data.suggestedPost)), ("scheduleRepeatPeriod", ConstructorParameterDescription(_data.scheduleRepeatPeriod)), ("summaryFromLanguage", ConstructorParameterDescription(_data.summaryFromLanguage))]) + return ("message", [("flags", ConstructorParameterDescription(_data.flags)), ("flags2", ConstructorParameterDescription(_data.flags2)), ("id", ConstructorParameterDescription(_data.id)), ("fromId", ConstructorParameterDescription(_data.fromId)), ("fromBoostsApplied", ConstructorParameterDescription(_data.fromBoostsApplied)), ("fromRank", ConstructorParameterDescription(_data.fromRank)), ("peerId", ConstructorParameterDescription(_data.peerId)), ("savedPeerId", ConstructorParameterDescription(_data.savedPeerId)), ("fwdFrom", ConstructorParameterDescription(_data.fwdFrom)), ("viaBotId", ConstructorParameterDescription(_data.viaBotId)), ("viaBusinessBotId", ConstructorParameterDescription(_data.viaBusinessBotId)), ("guestchatViaFrom", ConstructorParameterDescription(_data.guestchatViaFrom)), ("replyTo", ConstructorParameterDescription(_data.replyTo)), ("date", ConstructorParameterDescription(_data.date)), ("message", ConstructorParameterDescription(_data.message)), ("media", ConstructorParameterDescription(_data.media)), ("replyMarkup", ConstructorParameterDescription(_data.replyMarkup)), ("entities", ConstructorParameterDescription(_data.entities)), ("views", ConstructorParameterDescription(_data.views)), ("forwards", ConstructorParameterDescription(_data.forwards)), ("replies", ConstructorParameterDescription(_data.replies)), ("editDate", ConstructorParameterDescription(_data.editDate)), ("postAuthor", ConstructorParameterDescription(_data.postAuthor)), ("groupedId", ConstructorParameterDescription(_data.groupedId)), ("reactions", ConstructorParameterDescription(_data.reactions)), ("restrictionReason", ConstructorParameterDescription(_data.restrictionReason)), ("ttlPeriod", ConstructorParameterDescription(_data.ttlPeriod)), ("quickReplyShortcutId", ConstructorParameterDescription(_data.quickReplyShortcutId)), ("effect", ConstructorParameterDescription(_data.effect)), ("factcheck", ConstructorParameterDescription(_data.factcheck)), ("reportDeliveryUntilDate", ConstructorParameterDescription(_data.reportDeliveryUntilDate)), ("paidMessageStars", ConstructorParameterDescription(_data.paidMessageStars)), ("suggestedPost", ConstructorParameterDescription(_data.suggestedPost)), ("scheduleRepeatPeriod", ConstructorParameterDescription(_data.scheduleRepeatPeriod)), ("summaryFromLanguage", ConstructorParameterDescription(_data.summaryFromLanguage))]) case .messageEmpty(let _data): return ("messageEmpty", [("flags", ConstructorParameterDescription(_data.flags)), ("id", ConstructorParameterDescription(_data.id)), ("peerId", ConstructorParameterDescription(_data.peerId))]) case .messageService(let _data): @@ -1355,17 +1360,17 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Api.Peer? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Peer } } var _5: Int32? - if Int(_1!) & Int(1 << 29) != 0 { + if Int(_1 ?? 0) & Int(1 << 29) != 0 { _5 = reader.readInt32() } var _6: String? - if Int(_2!) & Int(1 << 12) != 0 { + if Int(_2 ?? 0) & Int(1 << 12) != 0 { _6 = parseString(reader) } var _7: Api.Peer? @@ -1373,167 +1378,174 @@ public extension Api { _7 = Api.parse(reader, signature: signature) as? Api.Peer } var _8: Api.Peer? - if Int(_1!) & Int(1 << 28) != 0 { + if Int(_1 ?? 0) & Int(1 << 28) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.Peer } } var _9: Api.MessageFwdHeader? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.MessageFwdHeader } } var _10: Int64? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _10 = reader.readInt64() } var _11: Int64? - if Int(_2!) & Int(1 << 0) != 0 { + if Int(_2 ?? 0) & Int(1 << 0) != 0 { _11 = reader.readInt64() } - var _12: Api.MessageReplyHeader? - if Int(_1!) & Int(1 << 3) != 0 { + var _12: Api.Peer? + if Int(_2 ?? 0) & Int(1 << 19) != 0 { if let signature = reader.readInt32() { - _12 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader + _12 = Api.parse(reader, signature: signature) as? Api.Peer } } - var _13: Int32? - _13 = reader.readInt32() - var _14: String? - _14 = parseString(reader) - var _15: Api.MessageMedia? - if Int(_1!) & Int(1 << 9) != 0 { + var _13: Api.MessageReplyHeader? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { - _15 = Api.parse(reader, signature: signature) as? Api.MessageMedia + _13 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader } } - var _16: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 6) != 0 { + var _14: Int32? + _14 = reader.readInt32() + var _15: String? + _15 = parseString(reader) + var _16: Api.MessageMedia? + if Int(_1 ?? 0) & Int(1 << 9) != 0 { if let signature = reader.readInt32() { - _16 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + _16 = Api.parse(reader, signature: signature) as? Api.MessageMedia } } - var _17: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 7) != 0 { + var _17: Api.ReplyMarkup? + if Int(_1 ?? 0) & Int(1 << 6) != 0 { + if let signature = reader.readInt32() { + _17 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup + } + } + var _18: [Api.MessageEntity]? + if Int(_1 ?? 0) & Int(1 << 7) != 0 { if let _ = reader.readInt32() { - _17 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + _18 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } - var _18: Int32? - if Int(_1!) & Int(1 << 10) != 0 { - _18 = reader.readInt32() - } var _19: Int32? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { _19 = reader.readInt32() } - var _20: Api.MessageReplies? - if Int(_1!) & Int(1 << 23) != 0 { + var _20: Int32? + if Int(_1 ?? 0) & Int(1 << 10) != 0 { + _20 = reader.readInt32() + } + var _21: Api.MessageReplies? + if Int(_1 ?? 0) & Int(1 << 23) != 0 { if let signature = reader.readInt32() { - _20 = Api.parse(reader, signature: signature) as? Api.MessageReplies + _21 = Api.parse(reader, signature: signature) as? Api.MessageReplies } } - var _21: Int32? - if Int(_1!) & Int(1 << 15) != 0 { - _21 = reader.readInt32() + var _22: Int32? + if Int(_1 ?? 0) & Int(1 << 15) != 0 { + _22 = reader.readInt32() } - var _22: String? - if Int(_1!) & Int(1 << 16) != 0 { - _22 = parseString(reader) + var _23: String? + if Int(_1 ?? 0) & Int(1 << 16) != 0 { + _23 = parseString(reader) } - var _23: Int64? - if Int(_1!) & Int(1 << 17) != 0 { - _23 = reader.readInt64() + var _24: Int64? + if Int(_1 ?? 0) & Int(1 << 17) != 0 { + _24 = reader.readInt64() } - var _24: Api.MessageReactions? - if Int(_1!) & Int(1 << 20) != 0 { + var _25: Api.MessageReactions? + if Int(_1 ?? 0) & Int(1 << 20) != 0 { if let signature = reader.readInt32() { - _24 = Api.parse(reader, signature: signature) as? Api.MessageReactions + _25 = Api.parse(reader, signature: signature) as? Api.MessageReactions } } - var _25: [Api.RestrictionReason]? - if Int(_1!) & Int(1 << 22) != 0 { + var _26: [Api.RestrictionReason]? + if Int(_1 ?? 0) & Int(1 << 22) != 0 { if let _ = reader.readInt32() { - _25 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RestrictionReason.self) + _26 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RestrictionReason.self) } } - var _26: Int32? - if Int(_1!) & Int(1 << 25) != 0 { - _26 = reader.readInt32() - } var _27: Int32? - if Int(_1!) & Int(1 << 30) != 0 { + if Int(_1 ?? 0) & Int(1 << 25) != 0 { _27 = reader.readInt32() } - var _28: Int64? - if Int(_2!) & Int(1 << 2) != 0 { - _28 = reader.readInt64() + var _28: Int32? + if Int(_1 ?? 0) & Int(1 << 30) != 0 { + _28 = reader.readInt32() } - var _29: Api.FactCheck? - if Int(_2!) & Int(1 << 3) != 0 { + var _29: Int64? + if Int(_2 ?? 0) & Int(1 << 2) != 0 { + _29 = reader.readInt64() + } + var _30: Api.FactCheck? + if Int(_2 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { - _29 = Api.parse(reader, signature: signature) as? Api.FactCheck + _30 = Api.parse(reader, signature: signature) as? Api.FactCheck } } - var _30: Int32? - if Int(_2!) & Int(1 << 5) != 0 { - _30 = reader.readInt32() + var _31: Int32? + if Int(_2 ?? 0) & Int(1 << 5) != 0 { + _31 = reader.readInt32() } - var _31: Int64? - if Int(_2!) & Int(1 << 6) != 0 { - _31 = reader.readInt64() + var _32: Int64? + if Int(_2 ?? 0) & Int(1 << 6) != 0 { + _32 = reader.readInt64() } - var _32: Api.SuggestedPost? - if Int(_2!) & Int(1 << 7) != 0 { + var _33: Api.SuggestedPost? + if Int(_2 ?? 0) & Int(1 << 7) != 0 { if let signature = reader.readInt32() { - _32 = Api.parse(reader, signature: signature) as? Api.SuggestedPost + _33 = Api.parse(reader, signature: signature) as? Api.SuggestedPost } } - var _33: Int32? - if Int(_2!) & Int(1 << 10) != 0 { - _33 = reader.readInt32() + var _34: Int32? + if Int(_2 ?? 0) & Int(1 << 10) != 0 { + _34 = reader.readInt32() } - var _34: String? - if Int(_2!) & Int(1 << 11) != 0 { - _34 = parseString(reader) + var _35: String? + if Int(_2 ?? 0) & Int(1 << 11) != 0 { + _35 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 8) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 29) == 0) || _5 != nil - let _c6 = (Int(_2!) & Int(1 << 12) == 0) || _6 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 29) == 0) || _5 != nil + let _c6 = (Int(_2 ?? 0) & Int(1 << 12) == 0) || _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 28) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 11) == 0) || _10 != nil - let _c11 = (Int(_2!) & Int(1 << 0) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 3) == 0) || _12 != nil - let _c13 = _13 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 28) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _10 != nil + let _c11 = (Int(_2 ?? 0) & Int(1 << 0) == 0) || _11 != nil + let _c12 = (Int(_2 ?? 0) & Int(1 << 19) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _13 != nil let _c14 = _14 != nil - let _c15 = (Int(_1!) & Int(1 << 9) == 0) || _15 != nil - let _c16 = (Int(_1!) & Int(1 << 6) == 0) || _16 != nil - let _c17 = (Int(_1!) & Int(1 << 7) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 10) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 10) == 0) || _19 != nil - let _c20 = (Int(_1!) & Int(1 << 23) == 0) || _20 != nil - let _c21 = (Int(_1!) & Int(1 << 15) == 0) || _21 != nil - let _c22 = (Int(_1!) & Int(1 << 16) == 0) || _22 != nil - let _c23 = (Int(_1!) & Int(1 << 17) == 0) || _23 != nil - let _c24 = (Int(_1!) & Int(1 << 20) == 0) || _24 != nil - let _c25 = (Int(_1!) & Int(1 << 22) == 0) || _25 != nil - let _c26 = (Int(_1!) & Int(1 << 25) == 0) || _26 != nil - let _c27 = (Int(_1!) & Int(1 << 30) == 0) || _27 != nil - let _c28 = (Int(_2!) & Int(1 << 2) == 0) || _28 != nil - let _c29 = (Int(_2!) & Int(1 << 3) == 0) || _29 != nil - let _c30 = (Int(_2!) & Int(1 << 5) == 0) || _30 != nil - let _c31 = (Int(_2!) & Int(1 << 6) == 0) || _31 != nil - let _c32 = (Int(_2!) & Int(1 << 7) == 0) || _32 != nil - let _c33 = (Int(_2!) & Int(1 << 10) == 0) || _33 != nil - let _c34 = (Int(_2!) & Int(1 << 11) == 0) || _34 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 && _c30 && _c31 && _c32 && _c33 && _c34 { - return Api.Message.message(Cons_message(flags: _1!, flags2: _2!, id: _3!, fromId: _4, fromBoostsApplied: _5, fromRank: _6, peerId: _7!, savedPeerId: _8, fwdFrom: _9, viaBotId: _10, viaBusinessBotId: _11, replyTo: _12, date: _13!, message: _14!, media: _15, replyMarkup: _16, entities: _17, views: _18, forwards: _19, replies: _20, editDate: _21, postAuthor: _22, groupedId: _23, reactions: _24, restrictionReason: _25, ttlPeriod: _26, quickReplyShortcutId: _27, effect: _28, factcheck: _29, reportDeliveryUntilDate: _30, paidMessageStars: _31, suggestedPost: _32, scheduleRepeatPeriod: _33, summaryFromLanguage: _34)) + let _c15 = _15 != nil + let _c16 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _16 != nil + let _c17 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _17 != nil + let _c18 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _18 != nil + let _c19 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _19 != nil + let _c20 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _20 != nil + let _c21 = (Int(_1 ?? 0) & Int(1 << 23) == 0) || _21 != nil + let _c22 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _22 != nil + let _c23 = (Int(_1 ?? 0) & Int(1 << 16) == 0) || _23 != nil + let _c24 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _24 != nil + let _c25 = (Int(_1 ?? 0) & Int(1 << 20) == 0) || _25 != nil + let _c26 = (Int(_1 ?? 0) & Int(1 << 22) == 0) || _26 != nil + let _c27 = (Int(_1 ?? 0) & Int(1 << 25) == 0) || _27 != nil + let _c28 = (Int(_1 ?? 0) & Int(1 << 30) == 0) || _28 != nil + let _c29 = (Int(_2 ?? 0) & Int(1 << 2) == 0) || _29 != nil + let _c30 = (Int(_2 ?? 0) & Int(1 << 3) == 0) || _30 != nil + let _c31 = (Int(_2 ?? 0) & Int(1 << 5) == 0) || _31 != nil + let _c32 = (Int(_2 ?? 0) & Int(1 << 6) == 0) || _32 != nil + let _c33 = (Int(_2 ?? 0) & Int(1 << 7) == 0) || _33 != nil + let _c34 = (Int(_2 ?? 0) & Int(1 << 10) == 0) || _34 != nil + let _c35 = (Int(_2 ?? 0) & Int(1 << 11) == 0) || _35 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 && _c30 && _c31 && _c32 && _c33 && _c34 && _c35 { + return Api.Message.message(Cons_message(flags: _1!, flags2: _2!, id: _3!, fromId: _4, fromBoostsApplied: _5, fromRank: _6, peerId: _7!, savedPeerId: _8, fwdFrom: _9, viaBotId: _10, viaBusinessBotId: _11, guestchatViaFrom: _12, replyTo: _13, date: _14!, message: _15!, media: _16, replyMarkup: _17, entities: _18, views: _19, forwards: _20, replies: _21, editDate: _22, postAuthor: _23, groupedId: _24, reactions: _25, restrictionReason: _26, ttlPeriod: _27, quickReplyShortcutId: _28, effect: _29, factcheck: _30, reportDeliveryUntilDate: _31, paidMessageStars: _32, suggestedPost: _33, scheduleRepeatPeriod: _34, summaryFromLanguage: _35)) } else { return nil @@ -1545,14 +1557,14 @@ public extension Api { var _2: Int32? _2 = reader.readInt32() var _3: Api.Peer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Peer } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.Message.messageEmpty(Cons_messageEmpty(flags: _1!, id: _2!, peerId: _3)) } @@ -1566,7 +1578,7 @@ public extension Api { var _2: Int32? _2 = reader.readInt32() var _3: Api.Peer? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Peer } @@ -1576,13 +1588,13 @@ public extension Api { _4 = Api.parse(reader, signature: signature) as? Api.Peer } var _5: Api.Peer? - if Int(_1!) & Int(1 << 28) != 0 { + if Int(_1 ?? 0) & Int(1 << 28) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.Peer } } var _6: Api.MessageReplyHeader? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader } @@ -1594,25 +1606,25 @@ public extension Api { _8 = Api.parse(reader, signature: signature) as? Api.MessageAction } var _9: Api.MessageReactions? - if Int(_1!) & Int(1 << 20) != 0 { + if Int(_1 ?? 0) & Int(1 << 20) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.MessageReactions } } var _10: Int32? - if Int(_1!) & Int(1 << 25) != 0 { + if Int(_1 ?? 0) & Int(1 << 25) != 0 { _10 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 8) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 28) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 28) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 20) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 25) == 0) || _10 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 20) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 25) == 0) || _10 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { return Api.Message.messageService(Cons_messageService(flags: _1!, id: _2!, fromId: _3, peerId: _4!, savedPeerId: _5, replyTo: _6, date: _7!, action: _8!, reactions: _9, ttlPeriod: _10)) } @@ -3292,18 +3304,18 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = parseString(reader) } var _3: Api.BotApp? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.BotApp } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.MessageAction.messageActionBotAllowed(Cons_messageActionBotAllowed(flags: _1!, domain: _2, app: _3)) } @@ -3445,19 +3457,19 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _3 = reader.readInt32() } var _4: [Api.Peer]? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.MessageAction.messageActionConferenceCall(Cons_messageActionConferenceCall(flags: _1!, callId: _2!, duration: _3, otherParticipants: _4)) } @@ -3521,7 +3533,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Peer? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Peer } @@ -3531,36 +3543,36 @@ public extension Api { var _4: String? _4 = parseString(reader) var _5: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = parseString(reader) } var _6: Int64? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _6 = reader.readInt64() } var _7: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _7 = parseString(reader) } var _8: Int64? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _8 = reader.readInt64() } var _9: Api.TextWithEntities? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.MessageAction.messageActionGiftCode(Cons_messageActionGiftCode(flags: _1!, boostPeer: _2, days: _3!, slug: _4!, currency: _5, amount: _6, cryptoCurrency: _7, cryptoAmount: _8, message: _9)) } @@ -3578,15 +3590,15 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() var _5: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = parseString(reader) } var _6: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = reader.readInt64() } var _7: Api.TextWithEntities? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } @@ -3595,9 +3607,9 @@ public extension Api { let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.MessageAction.messageActionGiftPremium(Cons_messageActionGiftPremium(flags: _1!, currency: _2!, amount: _3!, days: _4!, cryptoCurrency: _5, cryptoAmount: _6, message: _7)) } @@ -3615,24 +3627,24 @@ public extension Api { var _4: Int64? _4 = reader.readInt64() var _5: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = parseString(reader) } var _6: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = reader.readInt64() } var _7: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _7 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.MessageAction.messageActionGiftStars(Cons_messageActionGiftStars(flags: _1!, currency: _2!, amount: _3!, stars: _4!, cryptoCurrency: _5, cryptoAmount: _6, transactionId: _7)) } @@ -3652,7 +3664,7 @@ public extension Api { var _5: Int64? _5 = reader.readInt64() var _6: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = parseString(reader) } let _c1 = _1 != nil @@ -3660,7 +3672,7 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.MessageAction.messageActionGiftTon(Cons_messageActionGiftTon(flags: _1!, currency: _2!, amount: _3!, cryptoCurrency: _4!, cryptoAmount: _5!, transactionId: _6)) } @@ -3672,11 +3684,11 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt64() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil if _c1 && _c2 { return Api.MessageAction.messageActionGiveawayLaunch(Cons_messageActionGiveawayLaunch(flags: _1!, stars: _2)) } @@ -3709,12 +3721,12 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.InputGroupCall } var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.MessageAction.messageActionGroupCall(Cons_messageActionGroupCall(flags: _1!, call: _2!, duration: _3)) } @@ -3860,7 +3872,7 @@ public extension Api { var _4: Int64? _4 = reader.readInt64() var _5: Buffer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = parseBytes(reader) } var _6: Api.PaymentCharge? @@ -3871,7 +3883,7 @@ public extension Api { let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil let _c6 = _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.MessageAction.messageActionPaymentRefunded(Cons_messageActionPaymentRefunded(flags: _1!, peer: _2!, currency: _3!, totalAmount: _4!, payload: _5, charge: _6!)) @@ -3888,18 +3900,18 @@ public extension Api { var _3: Int64? _3 = reader.readInt64() var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } var _5: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.MessageAction.messageActionPaymentSent(Cons_messageActionPaymentSent(flags: _1!, currency: _2!, totalAmount: _3!, invoiceSlug: _4, subscriptionUntilDate: _5)) } @@ -3917,13 +3929,13 @@ public extension Api { var _4: Buffer? _4 = parseBytes(reader) var _5: Api.PaymentRequestedInfo? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo } } var _6: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _6 = parseString(reader) } var _7: Api.PaymentCharge? @@ -3931,17 +3943,17 @@ public extension Api { _7 = Api.parse(reader, signature: signature) as? Api.PaymentCharge } var _8: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _8 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.MessageAction.messageActionPaymentSentMe(Cons_messageActionPaymentSentMe(flags: _1!, currency: _2!, totalAmount: _3!, payload: _4!, info: _5, shippingOptionId: _6, charge: _7!, subscriptionUntilDate: _8)) } @@ -3955,19 +3967,19 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Api.PhoneCallDiscardReason? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.PhoneCallDiscardReason } } var _4: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.MessageAction.messageActionPhoneCall(Cons_messageActionPhoneCall(flags: _1!, callId: _2!, reason: _3, duration: _4)) } @@ -4130,12 +4142,12 @@ public extension Api { var _2: Int32? _2 = reader.readInt32() var _3: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.MessageAction.messageActionSetMessagesTTL(Cons_messageActionSetMessagesTTL(flags: _1!, period: _2!, autoSettingFrom: _3)) } @@ -4151,70 +4163,70 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.StarGift } var _3: Api.TextWithEntities? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } } var _4: Int64? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _4 = reader.readInt64() } var _5: Int32? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _5 = reader.readInt32() } var _6: Int64? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _6 = reader.readInt64() } var _7: Api.Peer? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.Peer } } var _8: Api.Peer? - if Int(_1!) & Int(1 << 12) != 0 { + if Int(_1 ?? 0) & Int(1 << 12) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.Peer } } var _9: Int64? - if Int(_1!) & Int(1 << 12) != 0 { + if Int(_1 ?? 0) & Int(1 << 12) != 0 { _9 = reader.readInt64() } var _10: String? - if Int(_1!) & Int(1 << 14) != 0 { + if Int(_1 ?? 0) & Int(1 << 14) != 0 { _10 = parseString(reader) } var _11: Int32? - if Int(_1!) & Int(1 << 15) != 0 { + if Int(_1 ?? 0) & Int(1 << 15) != 0 { _11 = reader.readInt32() } var _12: Api.Peer? - if Int(_1!) & Int(1 << 18) != 0 { + if Int(_1 ?? 0) & Int(1 << 18) != 0 { if let signature = reader.readInt32() { _12 = Api.parse(reader, signature: signature) as? Api.Peer } } var _13: Int32? - if Int(_1!) & Int(1 << 19) != 0 { + if Int(_1 ?? 0) & Int(1 << 19) != 0 { _13 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 5) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 8) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 12) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 12) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 15) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 18) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 19) == 0) || _13 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 12) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 12) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 14) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 18) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 19) == 0) || _13 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 { return Api.MessageAction.messageActionStarGift(Cons_messageActionStarGift(flags: _1!, gift: _2!, message: _3, convertStars: _4, upgradeMsgId: _5, upgradeStars: _6, fromId: _7, peer: _8, savedId: _9, prepaidUpgradeHash: _10, giftMsgId: _11, toId: _12, giftNum: _13)) } @@ -4275,63 +4287,63 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.StarGift } var _3: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _3 = reader.readInt32() } var _4: Int64? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _4 = reader.readInt64() } var _5: Api.Peer? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.Peer } } var _6: Api.Peer? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Peer } } var _7: Int64? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _7 = reader.readInt64() } var _8: Api.StarsAmount? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.StarsAmount } } var _9: Int32? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { _9 = reader.readInt32() } var _10: Int32? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { _10 = reader.readInt32() } var _11: Int64? - if Int(_1!) & Int(1 << 12) != 0 { + if Int(_1 ?? 0) & Int(1 << 12) != 0 { _11 = reader.readInt64() } var _12: Int32? - if Int(_1!) & Int(1 << 15) != 0 { + if Int(_1 ?? 0) & Int(1 << 15) != 0 { _12 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 6) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 7) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 7) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 8) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 9) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 10) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 12) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 15) == 0) || _12 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 12) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _12 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { return Api.MessageAction.messageActionStarGiftUnique(Cons_messageActionStarGiftUnique(flags: _1!, gift: _2!, canExportAt: _3, transferStars: _4, fromId: _5, peer: _6, savedId: _7, resaleAmount: _8, canTransferAt: _9, canResellAt: _10, dropOriginalDetailsStars: _11, canCraftAt: _12)) } @@ -4369,23 +4381,23 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _2 = parseString(reader) } var _3: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _3 = reader.readInt32() } var _4: Api.StarsAmount? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.StarsAmount } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.MessageAction.messageActionSuggestedPostApproval(Cons_messageActionSuggestedPostApproval(flags: _1!, rejectComment: _2, scheduleDate: _3, price: _4)) } @@ -4456,13 +4468,13 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.MessageAction.messageActionTopicCreate(Cons_messageActionTopicCreate(flags: _1!, title: _2!, iconColor: _3!, iconEmojiId: _4)) } @@ -4474,30 +4486,30 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = parseString(reader) } var _3: Int64? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = reader.readInt64() } var _4: Api.Bool? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Bool } } var _5: Api.Bool? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.Bool } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.MessageAction.messageActionTopicEdit(Cons_messageActionTopicEdit(flags: _1!, title: _2, iconEmojiId: _3, closed: _4, hidden: _5)) } diff --git a/submodules/TelegramApi/Sources/Api16.swift b/submodules/TelegramApi/Sources/Api16.swift index c774f8f008..bf75b3d074 100644 --- a/submodules/TelegramApi/Sources/Api16.swift +++ b/submodules/TelegramApi/Sources/Api16.swift @@ -1030,28 +1030,28 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Api.PhotoSize? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.PhotoSize } } var _5: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = reader.readInt32() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.MessageExtendedMedia.messageExtendedMediaPreview(Cons_messageExtendedMediaPreview(flags: _1!, w: _2, h: _3, thumb: _4, videoDuration: _5)) } @@ -1149,65 +1149,65 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Peer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Peer } } var _3: String? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _3 = parseString(reader) } var _4: Int32? _4 = reader.readInt32() var _5: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = reader.readInt32() } var _6: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _6 = parseString(reader) } var _7: Api.Peer? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.Peer } } var _8: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _8 = reader.readInt32() } var _9: Api.Peer? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.Peer } } var _10: String? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { _10 = parseString(reader) } var _11: Int32? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { _11 = reader.readInt32() } var _12: String? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _12 = parseString(reader) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 5) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 8) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 9) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 10) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 6) == 0) || _12 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _12 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { return Api.MessageFwdHeader.messageFwdHeader(Cons_messageFwdHeader(flags: _1!, fromId: _2, fromName: _3, date: _4!, channelPost: _5, postAuthor: _6, savedFromPeer: _7, savedFromMsgId: _8, savedFromId: _9, savedFromName: _10, savedDate: _11, psaType: _12)) } @@ -1837,7 +1837,7 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Api.messages.EmojiGameOutcome? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.messages.EmojiGameOutcome } @@ -1845,7 +1845,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.MessageMedia.messageMediaDice(Cons_messageMediaDice(flags: _1!, value: _2!, emoticon: _3!, gameOutcome: _4)) } @@ -1857,37 +1857,37 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Document? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Document } } var _3: [Api.Document]? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) } } var _4: Api.Photo? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Photo } } var _5: Int32? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { _5 = reader.readInt32() } var _6: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _6 = reader.readInt32() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 5) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 9) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 10) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.MessageMedia.messageMediaDocument(Cons_messageMediaDocument(flags: _1!, document: _2, altDocuments: _3, videoCover: _4, videoTimestamp: _5, ttlSeconds: _6)) } @@ -1932,20 +1932,20 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.GeoPoint } var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Int32? _4 = reader.readInt32() var _5: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.MessageMedia.messageMediaGeoLive(Cons_messageMediaGeoLive(flags: _1!, geo: _2!, heading: _3, period: _4!, proximityNotificationRadius: _5)) } @@ -1961,34 +1961,34 @@ public extension Api { _2 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } var _3: [String]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) } } var _4: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _4 = parseString(reader) } var _5: Int32? _5 = reader.readInt32() var _6: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _6 = reader.readInt32() } var _7: Int64? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _7 = reader.readInt64() } var _8: Int32? _8 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 5) == 0) || _7 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _7 != nil let _c8 = _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.MessageMedia.messageMediaGiveaway(Cons_messageMediaGiveaway(flags: _1!, channels: _2!, countriesIso2: _3, prizeDescription: _4, quantity: _5!, months: _6, stars: _7, untilDate: _8!)) @@ -2003,7 +2003,7 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _3 = reader.readInt32() } var _4: Int32? @@ -2017,29 +2017,29 @@ public extension Api { _7 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } var _8: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _8 = reader.readInt32() } var _9: Int64? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _9 = reader.readInt64() } var _10: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _10 = parseString(reader) } var _11: Int32? _11 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 5) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _10 != nil let _c11 = _11 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { return Api.MessageMedia.messageMediaGiveawayResults(Cons_messageMediaGiveawayResults(flags: _1!, channelId: _2!, additionalPeersCount: _3, launchMsgId: _4!, winnersCount: _5!, unclaimedCount: _6!, winners: _7!, months: _8, stars: _9, prizeDescription: _10, untilDate: _11!)) @@ -2056,13 +2056,13 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Api.WebDocument? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.WebDocument } } var _5: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = reader.readInt32() } var _6: String? @@ -2072,7 +2072,7 @@ public extension Api { var _8: String? _8 = parseString(reader) var _9: Api.MessageExtendedMedia? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.MessageExtendedMedia } @@ -2080,12 +2080,12 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.MessageMedia.messageMediaInvoice(Cons_messageMediaInvoice(flags: _1!, title: _2!, description: _3!, photo: _4, receiptMsgId: _5, currency: _6!, totalAmount: _7!, startParam: _8!, extendedMedia: _9)) } @@ -2113,25 +2113,25 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Photo? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Photo } } var _3: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _3 = reader.readInt32() } var _4: Api.Document? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Document } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.MessageMedia.messageMediaPhoto(Cons_messageMediaPhoto(flags: _1!, photo: _2, ttlSeconds: _3, video: _4)) } @@ -2151,7 +2151,7 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.PollResults } var _4: Api.MessageMedia? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.MessageMedia } @@ -2159,7 +2159,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.MessageMedia.messageMediaPoll(Cons_messageMediaPoll(flags: _1!, poll: _2!, results: _3!, attachedMedia: _4)) } @@ -2177,7 +2177,7 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Api.StoryItem? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.StoryItem } @@ -2185,7 +2185,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.MessageMedia.messageMediaStory(Cons_messageMediaStory(flags: _1!, peer: _2!, id: _3!, story: _4)) } @@ -2201,14 +2201,14 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.TodoList } var _3: [Api.TodoCompletion]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TodoCompletion.self) } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.MessageMedia.messageMediaToDo(Cons_messageMediaToDo(flags: _1!, todo: _2!, completions: _3)) } diff --git a/submodules/TelegramApi/Sources/Api17.swift b/submodules/TelegramApi/Sources/Api17.swift index cde9dbb53c..f371ca3819 100644 --- a/submodules/TelegramApi/Sources/Api17.swift +++ b/submodules/TelegramApi/Sources/Api17.swift @@ -322,21 +322,21 @@ public extension Api { _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ReactionCount.self) } var _3: [Api.MessagePeerReaction]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessagePeerReaction.self) } } var _4: [Api.MessageReactor]? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageReactor.self) } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.MessageReactions.messageReactions(Cons_messageReactions(flags: _1!, results: _2!, recentReactions: _3, topReactors: _4)) } @@ -389,7 +389,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Peer? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Peer } @@ -397,7 +397,7 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 3) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { return Api.MessageReactor.messageReactor(Cons_messageReactor(flags: _1!, peerId: _2, count: _3!)) @@ -477,30 +477,30 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: [Api.Peer]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) } } var _5: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = reader.readInt64() } var _6: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _6 = reader.readInt32() } var _7: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _7 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.MessageReplies.messageReplies(Cons_messageReplies(flags: _1!, replies: _2!, repliesPts: _3!, recentRepliers: _4, channelId: _5, maxId: _6, readMaxId: _7)) } @@ -620,64 +620,64 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _2 = reader.readInt32() } var _3: Api.Peer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Peer } } var _4: Api.MessageFwdHeader? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.MessageFwdHeader } } var _5: Api.MessageMedia? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.MessageMedia } } var _6: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _6 = reader.readInt32() } var _7: String? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _7 = parseString(reader) } var _8: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { if let _ = reader.readInt32() { _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _9: Int32? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { _9 = reader.readInt32() } var _10: Int32? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _10 = reader.readInt32() } var _11: Buffer? - if Int(_1!) & Int(1 << 12) != 0 { + if Int(_1 ?? 0) & Int(1 << 12) != 0 { _11 = parseBytes(reader) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 5) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 8) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 6) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 7) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 10) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 11) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 12) == 0) || _11 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 12) == 0) || _11 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { return Api.MessageReplyHeader.messageReplyHeader(Cons_messageReplyHeader(flags: _1!, replyToMsgId: _2, replyToPeerId: _3, replyFrom: _4, replyMedia: _5, replyToTopId: _6, quoteText: _7, quoteEntities: _8, quoteOffset: _9, todoItemId: _10, pollOption: _11)) } @@ -803,23 +803,23 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = reader.readInt32() } var _4: Api.MessageReplies? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.MessageReplies } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.MessageViews.messageViews(Cons_messageViews(flags: _1!, views: _2, forwards: _3, replies: _4)) } @@ -1166,7 +1166,7 @@ public extension Api { var _2: Int32? _2 = reader.readInt32() var _3: Api.Peer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Peer } @@ -1176,15 +1176,15 @@ public extension Api { var _5: Int32? _5 = reader.readInt32() var _6: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _6 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.MyBoost.myBoost(Cons_myBoost(flags: _1!, slot: _2!, peer: _3, date: _4!, expires: _5!, cooldownUntilDate: _6)) } diff --git a/submodules/TelegramApi/Sources/Api18.swift b/submodules/TelegramApi/Sources/Api18.swift index 4cdba8c6d6..c4b43cfef0 100644 --- a/submodules/TelegramApi/Sources/Api18.swift +++ b/submodules/TelegramApi/Sources/Api18.swift @@ -393,7 +393,7 @@ public extension Api { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) } var _6: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _6 = reader.readInt32() } let _c1 = _1 != nil @@ -401,7 +401,7 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.Page.page(Cons_page(flags: _1!, url: _2!, blocks: _3!, photos: _4!, documents: _5!, views: _6)) } @@ -1207,23 +1207,23 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _2 = parseString(reader) } var _3: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _3 = parseString(reader) } var _4: Int64? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _4 = reader.readInt64() } var _5: Int32? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _5 = reader.readInt32() } var _6: Int32? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _6 = reader.readInt32() } var _7: Api.PageCaption? @@ -1231,11 +1231,11 @@ public extension Api { _7 = Api.parse(reader, signature: signature) as? Api.PageCaption } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 5) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 5) == 0) || _6 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _6 != nil let _c7 = _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.PageBlock.pageBlockEmbed(Cons_pageBlockEmbed(flags: _1!, url: _2, html: _3, posterPhotoId: _4, w: _5, h: _6, caption: _7!)) @@ -1392,18 +1392,18 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.PageCaption } var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } var _5: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.PageBlock.pageBlockPhoto(Cons_pageBlockPhoto(flags: _1!, photoId: _2!, caption: _3!, url: _4, webpageId: _5)) } diff --git a/submodules/TelegramApi/Sources/Api19.swift b/submodules/TelegramApi/Sources/Api19.swift index dad55093d8..586573d025 100644 --- a/submodules/TelegramApi/Sources/Api19.swift +++ b/submodules/TelegramApi/Sources/Api19.swift @@ -296,33 +296,33 @@ public extension Api { var _3: Int64? _3 = reader.readInt64() var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } var _5: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = parseString(reader) } var _6: Int64? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _6 = reader.readInt64() } var _7: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _7 = parseString(reader) } var _8: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _8 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.PageRelatedArticle.pageRelatedArticle(Cons_pageRelatedArticle(flags: _1!, url: _2!, webpageId: _3!, title: _4, description: _5, photoId: _6, author: _7, publishedDate: _8)) } @@ -382,23 +382,23 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.RichText? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.RichText } } var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = reader.readInt32() } var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = reader.readInt32() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 7) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.PageTableCell.pageTableCell(Cons_pageTableCell(flags: _1!, text: _2, colspan: _3, rowspan: _4)) } @@ -586,19 +586,19 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() var _5: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = reader.readInt64() } var _6: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _6 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.Passkey.passkey(Cons_passkey(flags: _1!, id: _2!, name: _3!, date: _4!, softwareEmojiId: _5, lastUsageDate: _6)) } @@ -836,28 +836,28 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = parseString(reader) } var _3: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = parseString(reader) } var _4: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = parseString(reader) } var _5: Api.PostAddress? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.PostAddress } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.PaymentRequestedInfo.paymentRequestedInfo(Cons_paymentRequestedInfo(flags: _1!, name: _2, phone: _3, email: _4, shippingAddress: _5)) } @@ -1196,16 +1196,16 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } var _3: Int64? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = reader.readInt64() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.PeerColor.peerColor(Cons_peerColor(flags: _1!, color: _2, backgroundEmojiId: _3)) } @@ -1229,11 +1229,11 @@ public extension Api { _6 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } var _7: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _7 = reader.readInt32() } var _8: [Int32]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _8 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } @@ -1244,8 +1244,8 @@ public extension Api { let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.PeerColor.peerColorCollectible(Cons_peerColorCollectible(flags: _1!, collectibleId: _2!, giftEmojiId: _3!, backgroundEmojiId: _4!, accentColor: _5!, colors: _6!, darkAccentColor: _7, darkColors: _8)) } @@ -1432,81 +1432,81 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Bool? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Bool } } var _3: Api.Bool? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Bool } } var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = reader.readInt32() } var _5: Api.NotificationSound? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.NotificationSound } } var _6: Api.NotificationSound? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.NotificationSound } } var _7: Api.NotificationSound? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.NotificationSound } } var _8: Api.Bool? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.Bool } } var _9: Api.Bool? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.Bool } } var _10: Api.NotificationSound? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.NotificationSound } } var _11: Api.NotificationSound? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { if let signature = reader.readInt32() { _11 = Api.parse(reader, signature: signature) as? Api.NotificationSound } } var _12: Api.NotificationSound? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _12 = Api.parse(reader, signature: signature) as? Api.NotificationSound } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 5) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 6) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 7) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 8) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 9) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 10) == 0) || _12 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _12 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { return Api.PeerNotifySettings.peerNotifySettings(Cons_peerNotifySettings(flags: _1!, showPreviews: _2, silent: _3, muteUntil: _4, iosSound: _5, androidSound: _6, otherSound: _7, storiesMuted: _8, storiesHideSender: _9, storiesIosSound: _10, storiesAndroidSound: _11, storiesOtherSound: _12)) } @@ -1601,56 +1601,56 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _2 = reader.readInt32() } var _3: String? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { _3 = parseString(reader) } var _4: Int32? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { _4 = reader.readInt32() } var _5: Int64? - if Int(_1!) & Int(1 << 13) != 0 { + if Int(_1 ?? 0) & Int(1 << 13) != 0 { _5 = reader.readInt64() } var _6: String? - if Int(_1!) & Int(1 << 13) != 0 { + if Int(_1 ?? 0) & Int(1 << 13) != 0 { _6 = parseString(reader) } var _7: Int64? - if Int(_1!) & Int(1 << 14) != 0 { + if Int(_1 ?? 0) & Int(1 << 14) != 0 { _7 = reader.readInt64() } var _8: String? - if Int(_1!) & Int(1 << 15) != 0 { + if Int(_1 ?? 0) & Int(1 << 15) != 0 { _8 = parseString(reader) } var _9: String? - if Int(_1!) & Int(1 << 16) != 0 { + if Int(_1 ?? 0) & Int(1 << 16) != 0 { _9 = parseString(reader) } var _10: Int32? - if Int(_1!) & Int(1 << 17) != 0 { + if Int(_1 ?? 0) & Int(1 << 17) != 0 { _10 = reader.readInt32() } var _11: Int32? - if Int(_1!) & Int(1 << 18) != 0 { + if Int(_1 ?? 0) & Int(1 << 18) != 0 { _11 = reader.readInt32() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 6) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 9) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 9) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 13) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 13) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 14) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 15) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 16) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 17) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 18) == 0) || _11 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 13) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 13) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 14) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 16) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 18) == 0) || _11 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { return Api.PeerSettings.peerSettings(Cons_peerSettings(flags: _1!, geoDistance: _2, requestChatTitle: _3, requestChatDate: _4, businessBotId: _5, businessBotManageUrl: _6, chargePaidMessageStars: _7, registrationMonth: _8, phoneCountry: _9, nameChangeDate: _10, photoChangeDate: _11)) } @@ -1714,7 +1714,7 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.Peer } var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: [Api.StoryItem]? @@ -1723,7 +1723,7 @@ public extension Api { } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.PeerStories.peerStories(Cons_peerStories(flags: _1!, peer: _2!, maxReadId: _3, stories: _4!)) @@ -2066,7 +2066,7 @@ public extension Api { var _11: Int32? _11 = reader.readInt32() var _12: Api.DataJSON? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { if let signature = reader.readInt32() { _12 = Api.parse(reader, signature: signature) as? Api.DataJSON } @@ -2082,7 +2082,7 @@ public extension Api { let _c9 = _9 != nil let _c10 = _10 != nil let _c11 = _11 != nil - let _c12 = (Int(_1!) & Int(1 << 7) == 0) || _12 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _12 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { return Api.PhoneCall.phoneCall(Cons_phoneCall(flags: _1!, id: _2!, accessHash: _3!, date: _4!, adminId: _5!, participantId: _6!, gAOrB: _7!, keyFingerprint: _8!, protocol: _9!, connections: _10!, startDate: _11!, customParameters: _12)) } @@ -2130,19 +2130,19 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Api.PhoneCallDiscardReason? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.PhoneCallDiscardReason } } var _4: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.PhoneCall.phoneCallDiscarded(Cons_phoneCallDiscarded(flags: _1!, id: _2!, reason: _3, duration: _4)) } @@ -2213,7 +2213,7 @@ public extension Api { _7 = Api.parse(reader, signature: signature) as? Api.PhoneCallProtocol } var _8: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _8 = reader.readInt32() } let _c1 = _1 != nil @@ -2223,7 +2223,7 @@ public extension Api { let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 0) == 0) || _8 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.PhoneCall.phoneCallWaiting(Cons_phoneCallWaiting(flags: _1!, id: _2!, accessHash: _3!, date: _4!, adminId: _5!, participantId: _6!, protocol: _7!, receiveDate: _8)) } diff --git a/submodules/TelegramApi/Sources/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index f9f45b3af7..d6f92cf706 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -1,3 +1,208 @@ +public extension Api { + enum BotApp: TypeConstructorDescription { + public class Cons_botApp: TypeConstructorDescription { + public var flags: Int32 + public var id: Int64 + public var accessHash: Int64 + public var shortName: String + public var title: String + public var description: String + public var photo: Api.Photo + public var document: Api.Document? + public var hash: Int64 + public init(flags: Int32, id: Int64, accessHash: Int64, shortName: String, title: String, description: String, photo: Api.Photo, document: Api.Document?, hash: Int64) { + self.flags = flags + self.id = id + self.accessHash = accessHash + self.shortName = shortName + self.title = title + self.description = description + self.photo = photo + self.document = document + self.hash = hash + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("botApp", [("flags", ConstructorParameterDescription(self.flags)), ("id", ConstructorParameterDescription(self.id)), ("accessHash", ConstructorParameterDescription(self.accessHash)), ("shortName", ConstructorParameterDescription(self.shortName)), ("title", ConstructorParameterDescription(self.title)), ("description", ConstructorParameterDescription(self.description)), ("photo", ConstructorParameterDescription(self.photo)), ("document", ConstructorParameterDescription(self.document)), ("hash", ConstructorParameterDescription(self.hash))]) + } + } + case botApp(Cons_botApp) + case botAppNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botApp(let _data): + if boxed { + buffer.appendInt32(-1778593322) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeInt64(_data.id, buffer: buffer, boxed: false) + serializeInt64(_data.accessHash, buffer: buffer, boxed: false) + serializeString(_data.shortName, buffer: buffer, boxed: false) + serializeString(_data.title, buffer: buffer, boxed: false) + serializeString(_data.description, buffer: buffer, boxed: false) + _data.photo.serialize(buffer, true) + if Int(_data.flags) & Int(1 << 0) != 0 { + _data.document!.serialize(buffer, true) + } + serializeInt64(_data.hash, buffer: buffer, boxed: false) + break + case .botAppNotModified: + if boxed { + buffer.appendInt32(1571189943) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .botApp(let _data): + return ("botApp", [("flags", ConstructorParameterDescription(_data.flags)), ("id", ConstructorParameterDescription(_data.id)), ("accessHash", ConstructorParameterDescription(_data.accessHash)), ("shortName", ConstructorParameterDescription(_data.shortName)), ("title", ConstructorParameterDescription(_data.title)), ("description", ConstructorParameterDescription(_data.description)), ("photo", ConstructorParameterDescription(_data.photo)), ("document", ConstructorParameterDescription(_data.document)), ("hash", ConstructorParameterDescription(_data.hash))]) + case .botAppNotModified: + return ("botAppNotModified", []) + } + } + + public static func parse_botApp(_ reader: BufferReader) -> BotApp? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int64? + _3 = reader.readInt64() + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: String? + _6 = parseString(reader) + var _7: Api.Photo? + if let signature = reader.readInt32() { + _7 = Api.parse(reader, signature: signature) as? Api.Photo + } + var _8: Api.Document? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.Document + } + } + var _9: Int64? + _9 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.BotApp.botApp(Cons_botApp(flags: _1!, id: _2!, accessHash: _3!, shortName: _4!, title: _5!, description: _6!, photo: _7!, document: _8, hash: _9!)) + } + else { + return nil + } + } + public static func parse_botAppNotModified(_ reader: BufferReader) -> BotApp? { + return Api.BotApp.botAppNotModified + } + } +} +public extension Api { + enum BotAppSettings: TypeConstructorDescription { + public class Cons_botAppSettings: TypeConstructorDescription { + public var flags: Int32 + public var placeholderPath: Buffer? + public var backgroundColor: Int32? + public var backgroundDarkColor: Int32? + public var headerColor: Int32? + public var headerDarkColor: Int32? + public init(flags: Int32, placeholderPath: Buffer?, backgroundColor: Int32?, backgroundDarkColor: Int32?, headerColor: Int32?, headerDarkColor: Int32?) { + self.flags = flags + self.placeholderPath = placeholderPath + self.backgroundColor = backgroundColor + self.backgroundDarkColor = backgroundDarkColor + self.headerColor = headerColor + self.headerDarkColor = headerDarkColor + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("botAppSettings", [("flags", ConstructorParameterDescription(self.flags)), ("placeholderPath", ConstructorParameterDescription(self.placeholderPath)), ("backgroundColor", ConstructorParameterDescription(self.backgroundColor)), ("backgroundDarkColor", ConstructorParameterDescription(self.backgroundDarkColor)), ("headerColor", ConstructorParameterDescription(self.headerColor)), ("headerDarkColor", ConstructorParameterDescription(self.headerDarkColor))]) + } + } + case botAppSettings(Cons_botAppSettings) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .botAppSettings(let _data): + if boxed { + buffer.appendInt32(-912582320) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeBytes(_data.placeholderPath!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 1) != 0 { + serializeInt32(_data.backgroundColor!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 2) != 0 { + serializeInt32(_data.backgroundDarkColor!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 3) != 0 { + serializeInt32(_data.headerColor!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 4) != 0 { + serializeInt32(_data.headerDarkColor!, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .botAppSettings(let _data): + return ("botAppSettings", [("flags", ConstructorParameterDescription(_data.flags)), ("placeholderPath", ConstructorParameterDescription(_data.placeholderPath)), ("backgroundColor", ConstructorParameterDescription(_data.backgroundColor)), ("backgroundDarkColor", ConstructorParameterDescription(_data.backgroundDarkColor)), ("headerColor", ConstructorParameterDescription(_data.headerColor)), ("headerDarkColor", ConstructorParameterDescription(_data.headerDarkColor))]) + } + } + + public static func parse_botAppSettings(_ reader: BufferReader) -> BotAppSettings? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Buffer? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _2 = parseBytes(reader) + } + var _3: Int32? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _3 = reader.readInt32() + } + var _4: Int32? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _4 = reader.readInt32() + } + var _5: Int32? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _5 = reader.readInt32() + } + var _6: Int32? + if Int(_1 ?? 0) & Int(1 << 4) != 0 { + _6 = reader.readInt32() + } + let _c1 = _1 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.BotAppSettings.botAppSettings(Cons_botAppSettings(flags: _1!, placeholderPath: _2, backgroundColor: _3, backgroundDarkColor: _4, headerColor: _5, headerDarkColor: _6)) + } + else { + return nil + } + } + } +} public extension Api { enum BotBusinessConnection: TypeConstructorDescription { public class Cons_botBusinessConnection: TypeConstructorDescription { @@ -58,7 +263,7 @@ public extension Api { var _5: Int32? _5 = reader.readInt32() var _6: Api.BusinessBotRights? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.BusinessBotRights } @@ -68,7 +273,7 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.BotBusinessConnection.botBusinessConnection(Cons_botBusinessConnection(flags: _1!, connectionId: _2!, userId: _3!, dcId: _4!, date: _5!, rights: _6)) } @@ -372,63 +577,63 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt64() } var _3: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = parseString(reader) } var _4: Api.Photo? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Photo } } var _5: Api.Document? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.Document } } var _6: [Api.BotCommand]? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let _ = reader.readInt32() { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotCommand.self) } } var _7: Api.BotMenuButton? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.BotMenuButton } } var _8: String? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _8 = parseString(reader) } var _9: Api.BotAppSettings? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.BotAppSettings } } var _10: Api.BotVerifierSettings? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.BotVerifierSettings } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 5) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 7) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 8) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 9) == 0) || _10 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _10 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { return Api.BotInfo.botInfo(Cons_botInfo(flags: _1!, userId: _2, description: _3, descriptionPhoto: _4, descriptionDocument: _5, commands: _6, menuButton: _7, privacyPolicyUrl: _8, appSettings: _9, verifierSettings: _10)) } @@ -722,21 +927,21 @@ public extension Api { var _2: String? _2 = parseString(reader) var _3: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _4: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.BotInlineMessage.botInlineMessageMediaAuto(Cons_botInlineMessageMediaAuto(flags: _1!, message: _2!, entities: _3, replyMarkup: _4)) } @@ -756,7 +961,7 @@ public extension Api { var _5: String? _5 = parseString(reader) var _6: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } @@ -766,7 +971,7 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.BotInlineMessage.botInlineMessageMediaContact(Cons_botInlineMessageMediaContact(flags: _1!, phoneNumber: _2!, firstName: _3!, lastName: _4!, vcard: _5!, replyMarkup: _6)) } @@ -782,29 +987,29 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.GeoPoint } var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = reader.readInt32() } var _5: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _5 = reader.readInt32() } var _6: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.BotInlineMessage.botInlineMessageMediaGeo(Cons_botInlineMessageMediaGeo(flags: _1!, geo: _2!, heading: _3, period: _4, proximityNotificationRadius: _5, replyMarkup: _6)) } @@ -820,7 +1025,7 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Api.WebDocument? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.WebDocument } @@ -830,7 +1035,7 @@ public extension Api { var _6: Int64? _6 = reader.readInt64() var _7: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } @@ -838,10 +1043,10 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.BotInlineMessage.botInlineMessageMediaInvoice(Cons_botInlineMessageMediaInvoice(flags: _1!, title: _2!, description: _3!, photo: _4, currency: _5!, totalAmount: _6!, replyMarkup: _7)) } @@ -867,7 +1072,7 @@ public extension Api { var _7: String? _7 = parseString(reader) var _8: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } @@ -879,7 +1084,7 @@ public extension Api { let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.BotInlineMessage.botInlineMessageMediaVenue(Cons_botInlineMessageMediaVenue(flags: _1!, geo: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!, venueType: _7!, replyMarkup: _8)) } @@ -893,7 +1098,7 @@ public extension Api { var _2: String? _2 = parseString(reader) var _3: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } @@ -901,16 +1106,16 @@ public extension Api { var _4: String? _4 = parseString(reader) var _5: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.BotInlineMessage.botInlineMessageMediaWebPage(Cons_botInlineMessageMediaWebPage(flags: _1!, message: _2!, entities: _3, url: _4!, replyMarkup: _5)) } @@ -924,21 +1129,21 @@ public extension Api { var _2: String? _2 = parseString(reader) var _3: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _4: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.BotInlineMessage.botInlineMessageText(Cons_botInlineMessageText(flags: _1!, message: _2!, entities: _3, replyMarkup: _4)) } @@ -1068,23 +1273,23 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Api.Photo? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Photo } } var _5: Api.Document? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.Document } } var _6: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _6 = parseString(reader) } var _7: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _7 = parseString(reader) } var _8: Api.BotInlineMessage? @@ -1094,10 +1299,10 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _7 != nil let _c8 = _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.BotInlineResult.botInlineMediaResult(Cons_botInlineMediaResult(flags: _1!, id: _2!, type: _3!, photo: _4, document: _5, title: _6, description: _7, sendMessage: _8!)) @@ -1114,25 +1319,25 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = parseString(reader) } var _5: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = parseString(reader) } var _6: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _6 = parseString(reader) } var _7: Api.WebDocument? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.WebDocument } } var _8: Api.WebDocument? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.WebDocument } @@ -1144,11 +1349,11 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 5) == 0) || _8 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _8 != nil let _c9 = _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.BotInlineResult.botInlineResult(Cons_botInlineResult(flags: _1!, id: _2!, type: _3!, title: _4, description: _5, url: _6, thumb: _7, content: _8, sendMessage: _9!)) @@ -1389,13 +1594,13 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.BotVerifierSettings.botVerifierSettings(Cons_botVerifierSettings(flags: _1!, icon: _2!, company: _3!, customDescription: _4)) } @@ -1543,211 +1748,3 @@ public extension Api { } } } -public extension Api { - enum BusinessBotRecipients: TypeConstructorDescription { - public class Cons_businessBotRecipients: TypeConstructorDescription { - public var flags: Int32 - public var users: [Int64]? - public var excludeUsers: [Int64]? - public init(flags: Int32, users: [Int64]?, excludeUsers: [Int64]?) { - self.flags = flags - self.users = users - self.excludeUsers = excludeUsers - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("businessBotRecipients", [("flags", ConstructorParameterDescription(self.flags)), ("users", ConstructorParameterDescription(self.users)), ("excludeUsers", ConstructorParameterDescription(self.excludeUsers))]) - } - } - case businessBotRecipients(Cons_businessBotRecipients) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .businessBotRecipients(let _data): - if boxed { - buffer.appendInt32(-1198722189) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 4) != 0 { - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users!.count)) - for item in _data.users! { - serializeInt64(item, buffer: buffer, boxed: false) - } - } - if Int(_data.flags) & Int(1 << 6) != 0 { - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.excludeUsers!.count)) - for item in _data.excludeUsers! { - serializeInt64(item, buffer: buffer, boxed: false) - } - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .businessBotRecipients(let _data): - return ("businessBotRecipients", [("flags", ConstructorParameterDescription(_data.flags)), ("users", ConstructorParameterDescription(_data.users)), ("excludeUsers", ConstructorParameterDescription(_data.excludeUsers))]) - } - } - - public static func parse_businessBotRecipients(_ reader: BufferReader) -> BusinessBotRecipients? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Int64]? - if Int(_1!) & Int(1 << 4) != 0 { - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - } - var _3: [Int64]? - if Int(_1!) & Int(1 << 6) != 0 { - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 6) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.BusinessBotRecipients.businessBotRecipients(Cons_businessBotRecipients(flags: _1!, users: _2, excludeUsers: _3)) - } - else { - return nil - } - } - } -} -public extension Api { - enum BusinessBotRights: TypeConstructorDescription { - public class Cons_businessBotRights: TypeConstructorDescription { - public var flags: Int32 - public init(flags: Int32) { - self.flags = flags - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("businessBotRights", [("flags", ConstructorParameterDescription(self.flags))]) - } - } - case businessBotRights(Cons_businessBotRights) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .businessBotRights(let _data): - if boxed { - buffer.appendInt32(-1604170505) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .businessBotRights(let _data): - return ("businessBotRights", [("flags", ConstructorParameterDescription(_data.flags))]) - } - } - - public static func parse_businessBotRights(_ reader: BufferReader) -> BusinessBotRights? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.BusinessBotRights.businessBotRights(Cons_businessBotRights(flags: _1!)) - } - else { - return nil - } - } - } -} -public extension Api { - enum BusinessChatLink: TypeConstructorDescription { - public class Cons_businessChatLink: TypeConstructorDescription { - public var flags: Int32 - public var link: String - public var message: String - public var entities: [Api.MessageEntity]? - public var title: String? - public var views: Int32 - public init(flags: Int32, link: String, message: String, entities: [Api.MessageEntity]?, title: String?, views: Int32) { - self.flags = flags - self.link = link - self.message = message - self.entities = entities - self.title = title - self.views = views - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("businessChatLink", [("flags", ConstructorParameterDescription(self.flags)), ("link", ConstructorParameterDescription(self.link)), ("message", ConstructorParameterDescription(self.message)), ("entities", ConstructorParameterDescription(self.entities)), ("title", ConstructorParameterDescription(self.title)), ("views", ConstructorParameterDescription(self.views))]) - } - } - case businessChatLink(Cons_businessChatLink) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .businessChatLink(let _data): - if boxed { - buffer.appendInt32(-1263638929) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeString(_data.link, buffer: buffer, boxed: false) - serializeString(_data.message, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 0) != 0 { - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.entities!.count)) - for item in _data.entities! { - item.serialize(buffer, true) - } - } - if Int(_data.flags) & Int(1 << 1) != 0 { - serializeString(_data.title!, buffer: buffer, boxed: false) - } - serializeInt32(_data.views, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .businessChatLink(let _data): - return ("businessChatLink", [("flags", ConstructorParameterDescription(_data.flags)), ("link", ConstructorParameterDescription(_data.link)), ("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("title", ConstructorParameterDescription(_data.title)), ("views", ConstructorParameterDescription(_data.views))]) - } - } - - public static func parse_businessChatLink(_ reader: BufferReader) -> BusinessChatLink? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 0) != 0 { - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } - } - var _5: String? - if Int(_1!) & Int(1 << 1) != 0 { - _5 = parseString(reader) - } - var _6: Int32? - _6 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.BusinessChatLink.businessChatLink(Cons_businessChatLink(flags: _1!, link: _2!, message: _3!, entities: _4, title: _5, views: _6!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api20.swift b/submodules/TelegramApi/Sources/Api20.swift index 5b522e41e1..a630173cb1 100644 --- a/submodules/TelegramApi/Sources/Api20.swift +++ b/submodules/TelegramApi/Sources/Api20.swift @@ -389,7 +389,7 @@ public extension Api { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PhotoSize.self) } var _7: [Api.VideoSize]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.VideoSize.self) } @@ -402,7 +402,7 @@ public extension Api { let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil let _c8 = _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.Photo.photo(Cons_photo(flags: _1!, id: _2!, accessHash: _3!, fileReference: _4!, date: _5!, sizes: _6!, videoSizes: _7, dcId: _8!)) @@ -694,18 +694,20 @@ public extension Api { public var answers: [Api.PollAnswer] public var closePeriod: Int32? public var closeDate: Int32? + public var countriesIso2: [String]? public var hash: Int64 - public init(id: Int64, flags: Int32, question: Api.TextWithEntities, answers: [Api.PollAnswer], closePeriod: Int32?, closeDate: Int32?, hash: Int64) { + public init(id: Int64, flags: Int32, question: Api.TextWithEntities, answers: [Api.PollAnswer], closePeriod: Int32?, closeDate: Int32?, countriesIso2: [String]?, hash: Int64) { self.id = id self.flags = flags self.question = question self.answers = answers self.closePeriod = closePeriod self.closeDate = closeDate + self.countriesIso2 = countriesIso2 self.hash = hash } public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("poll", [("id", ConstructorParameterDescription(self.id)), ("flags", ConstructorParameterDescription(self.flags)), ("question", ConstructorParameterDescription(self.question)), ("answers", ConstructorParameterDescription(self.answers)), ("closePeriod", ConstructorParameterDescription(self.closePeriod)), ("closeDate", ConstructorParameterDescription(self.closeDate)), ("hash", ConstructorParameterDescription(self.hash))]) + return ("poll", [("id", ConstructorParameterDescription(self.id)), ("flags", ConstructorParameterDescription(self.flags)), ("question", ConstructorParameterDescription(self.question)), ("answers", ConstructorParameterDescription(self.answers)), ("closePeriod", ConstructorParameterDescription(self.closePeriod)), ("closeDate", ConstructorParameterDescription(self.closeDate)), ("countriesIso2", ConstructorParameterDescription(self.countriesIso2)), ("hash", ConstructorParameterDescription(self.hash))]) } } case poll(Cons_poll) @@ -714,7 +716,7 @@ public extension Api { switch self { case .poll(let _data): if boxed { - buffer.appendInt32(-1203610647) + buffer.appendInt32(-1771164225) } serializeInt64(_data.id, buffer: buffer, boxed: false) serializeInt32(_data.flags, buffer: buffer, boxed: false) @@ -730,6 +732,13 @@ public extension Api { if Int(_data.flags) & Int(1 << 5) != 0 { serializeInt32(_data.closeDate!, buffer: buffer, boxed: false) } + if Int(_data.flags) & Int(1 << 12) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.countriesIso2!.count)) + for item in _data.countriesIso2! { + serializeString(item, buffer: buffer, boxed: false) + } + } serializeInt64(_data.hash, buffer: buffer, boxed: false) break } @@ -738,7 +747,7 @@ public extension Api { public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { switch self { case .poll(let _data): - return ("poll", [("id", ConstructorParameterDescription(_data.id)), ("flags", ConstructorParameterDescription(_data.flags)), ("question", ConstructorParameterDescription(_data.question)), ("answers", ConstructorParameterDescription(_data.answers)), ("closePeriod", ConstructorParameterDescription(_data.closePeriod)), ("closeDate", ConstructorParameterDescription(_data.closeDate)), ("hash", ConstructorParameterDescription(_data.hash))]) + return ("poll", [("id", ConstructorParameterDescription(_data.id)), ("flags", ConstructorParameterDescription(_data.flags)), ("question", ConstructorParameterDescription(_data.question)), ("answers", ConstructorParameterDescription(_data.answers)), ("closePeriod", ConstructorParameterDescription(_data.closePeriod)), ("closeDate", ConstructorParameterDescription(_data.closeDate)), ("countriesIso2", ConstructorParameterDescription(_data.countriesIso2)), ("hash", ConstructorParameterDescription(_data.hash))]) } } @@ -756,24 +765,31 @@ public extension Api { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PollAnswer.self) } var _5: Int32? - if Int(_2!) & Int(1 << 4) != 0 { + if Int(_2 ?? 0) & Int(1 << 4) != 0 { _5 = reader.readInt32() } var _6: Int32? - if Int(_2!) & Int(1 << 5) != 0 { + if Int(_2 ?? 0) & Int(1 << 5) != 0 { _6 = reader.readInt32() } - var _7: Int64? - _7 = reader.readInt64() + var _7: [String]? + if Int(_2 ?? 0) & Int(1 << 12) != 0 { + if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) + } + } + var _8: Int64? + _8 = reader.readInt64() let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_2!) & Int(1 << 4) == 0) || _5 != nil - let _c6 = (Int(_2!) & Int(1 << 5) == 0) || _6 != nil - let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.Poll.poll(Cons_poll(id: _1!, flags: _2!, question: _3!, answers: _4!, closePeriod: _5, closeDate: _6, hash: _7!)) + let _c5 = (Int(_2 ?? 0) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_2 ?? 0) & Int(1 << 5) == 0) || _6 != nil + let _c7 = (Int(_2 ?? 0) & Int(1 << 12) == 0) || _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.Poll.poll(Cons_poll(id: _1!, flags: _2!, question: _3!, answers: _4!, closePeriod: _5, closeDate: _6, countriesIso2: _7, hash: _8!)) } else { return nil @@ -867,14 +883,14 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } var _3: Api.InputMedia? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.InputMedia } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.PollAnswer.inputPollAnswer(Cons_inputPollAnswer(flags: _1!, text: _2!, media: _3)) } @@ -892,27 +908,27 @@ public extension Api { var _3: Buffer? _3 = parseBytes(reader) var _4: Api.MessageMedia? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.MessageMedia } } var _5: Api.Peer? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.Peer } } var _6: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _6 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.PollAnswer.pollAnswer(Cons_pollAnswer(flags: _1!, text: _2!, option: _3!, media: _4, addedBy: _5, date: _6)) } @@ -976,19 +992,19 @@ public extension Api { var _2: Buffer? _2 = parseBytes(reader) var _3: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _3 = reader.readInt32() } var _4: [Api.Peer]? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.PollAnswerVoters.pollAnswerVoters(Cons_pollAnswerVoters(flags: _1!, option: _2!, voters: _3, recentVoters: _4)) } @@ -1075,44 +1091,44 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: [Api.PollAnswerVoters]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PollAnswerVoters.self) } } var _3: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _3 = reader.readInt32() } var _4: [Api.Peer]? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) } } var _5: String? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _5 = parseString(reader) } var _6: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let _ = reader.readInt32() { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _7: Api.MessageMedia? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.MessageMedia } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 5) == 0) || _7 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.PollResults.pollResults(Cons_pollResults(flags: _1!, results: _2, totalVoters: _3, recentVoters: _4, solution: _5, solutionEntities: _6, solutionMedia: _7)) } @@ -1416,11 +1432,11 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } var _5: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = reader.readInt32() } var _6: String? @@ -1430,8 +1446,8 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { @@ -1500,7 +1516,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _2 = parseString(reader) } var _3: Int32? @@ -1512,16 +1528,16 @@ public extension Api { var _6: String? _6 = parseString(reader) var _7: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _7 = parseString(reader) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 3) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.PremiumSubscriptionOption.premiumSubscriptionOption(Cons_premiumSubscriptionOption(flags: _1!, transaction: _2, months: _3!, currency: _4!, amount: _5!, botUrl: _6!, storeProduct: _7)) } diff --git a/submodules/TelegramApi/Sources/Api21.swift b/submodules/TelegramApi/Sources/Api21.swift index a14d943b32..102550071d 100644 --- a/submodules/TelegramApi/Sources/Api21.swift +++ b/submodules/TelegramApi/Sources/Api21.swift @@ -632,7 +632,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } var _3: Api.Reaction? @@ -642,7 +642,7 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { @@ -747,19 +747,19 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.ReactionNotificationsFrom? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.ReactionNotificationsFrom } } var _3: Api.ReactionNotificationsFrom? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.ReactionNotificationsFrom } } var _4: Api.ReactionNotificationsFrom? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.ReactionNotificationsFrom } @@ -773,9 +773,9 @@ public extension Api { _6 = Api.parse(reader, signature: signature) as? Api.Bool } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { diff --git a/submodules/TelegramApi/Sources/Api22.swift b/submodules/TelegramApi/Sources/Api22.swift index 200a882362..798549678a 100644 --- a/submodules/TelegramApi/Sources/Api22.swift +++ b/submodules/TelegramApi/Sources/Api22.swift @@ -276,11 +276,11 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _2 = reader.readInt32() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil if _c1 && _c2 { return Api.RecentStory.recentStory(Cons_recentStory(flags: _1!, maxId: _2)) } @@ -413,11 +413,11 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _2 = parseString(reader) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 3) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _2 != nil if _c1 && _c2 { return Api.ReplyMarkup.replyKeyboardForceReply(Cons_replyKeyboardForceReply(flags: _1!, placeholder: _2)) } @@ -444,12 +444,12 @@ public extension Api { _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.KeyboardButtonRow.self) } var _3: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _3 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.ReplyMarkup.replyKeyboardMarkup(Cons_replyKeyboardMarkup(flags: _1!, rows: _2!, placeholder: _3)) } @@ -830,27 +830,27 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Bool? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Bool } } var _3: Api.ChatAdminRights? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } } var _4: Api.ChatAdminRights? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 3) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.RequestPeerType.requestPeerTypeBroadcast(Cons_requestPeerTypeBroadcast(flags: _1!, hasUsername: _2, userAdminRights: _3, botAdminRights: _4)) } @@ -862,34 +862,34 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Bool? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Bool } } var _3: Api.Bool? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Bool } } var _4: Api.ChatAdminRights? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } } var _5: Api.ChatAdminRights? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 3) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 4) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.RequestPeerType.requestPeerTypeChat(Cons_requestPeerTypeChat(flags: _1!, hasUsername: _2, forum: _3, userAdminRights: _4, botAdminRights: _5)) } @@ -901,16 +901,16 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _2 = parseString(reader) } var _3: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _3 = parseString(reader) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.RequestPeerType.requestPeerTypeCreateBot(Cons_requestPeerTypeCreateBot(flags: _1!, suggestedName: _2, suggestedUsername: _3)) } @@ -922,20 +922,20 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Bool? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Bool } } var _3: Api.Bool? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Bool } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.RequestPeerType.requestPeerTypeUser(Cons_requestPeerTypeUser(flags: _1!, bot: _2, premium: _3)) } @@ -1072,24 +1072,24 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } var _4: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = parseString(reader) } var _5: Api.Photo? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.Photo } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.RequestedPeer.requestedPeerChannel(Cons_requestedPeerChannel(flags: _1!, channelId: _2!, title: _3, username: _4, photo: _5)) } @@ -1103,19 +1103,19 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } var _4: Api.Photo? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Photo } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.RequestedPeer.requestedPeerChat(Cons_requestedPeerChat(flags: _1!, chatId: _2!, title: _3, photo: _4)) } @@ -1129,29 +1129,29 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } var _5: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = parseString(reader) } var _6: Api.Photo? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Photo } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.RequestedPeer.requestedPeerUser(Cons_requestedPeerUser(flags: _1!, userId: _2!, firstName: _3, lastName: _4, username: _5, photo: _6)) } diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 9e9f2b2b5c..7d816e2222 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -745,7 +745,7 @@ public extension Api { var _7: Int32? _7 = reader.readInt32() var _8: Api.DraftMessage? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.DraftMessage } @@ -757,7 +757,7 @@ public extension Api { let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.SavedDialog.monoForumDialog(Cons_monoForumDialog(flags: _1!, peer: _2!, topMessage: _3!, readInboxMaxId: _4!, readOutboxMaxId: _5!, unreadCount: _6!, unreadReactionsCount: _7!, draft: _8)) } @@ -836,14 +836,14 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.Reaction } var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } var _4: Int32? _4 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.SavedReactionTag.savedReactionTag(Cons_savedReactionTag(flags: _1!, reaction: _2!, title: _3, count: _4!)) @@ -974,7 +974,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Peer? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Peer } @@ -986,83 +986,83 @@ public extension Api { _4 = Api.parse(reader, signature: signature) as? Api.StarGift } var _5: Api.TextWithEntities? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } } var _6: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _6 = reader.readInt32() } var _7: Int64? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _7 = reader.readInt64() } var _8: Int64? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _8 = reader.readInt64() } var _9: Int64? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _9 = reader.readInt64() } var _10: Int32? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _10 = reader.readInt32() } var _11: Int64? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _11 = reader.readInt64() } var _12: Int32? - if Int(_1!) & Int(1 << 13) != 0 { + if Int(_1 ?? 0) & Int(1 << 13) != 0 { _12 = reader.readInt32() } var _13: Int32? - if Int(_1!) & Int(1 << 14) != 0 { + if Int(_1 ?? 0) & Int(1 << 14) != 0 { _13 = reader.readInt32() } var _14: [Int32]? - if Int(_1!) & Int(1 << 15) != 0 { + if Int(_1 ?? 0) & Int(1 << 15) != 0 { if let _ = reader.readInt32() { _14 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } } var _15: String? - if Int(_1!) & Int(1 << 16) != 0 { + if Int(_1 ?? 0) & Int(1 << 16) != 0 { _15 = parseString(reader) } var _16: Int64? - if Int(_1!) & Int(1 << 18) != 0 { + if Int(_1 ?? 0) & Int(1 << 18) != 0 { _16 = reader.readInt64() } var _17: Int32? - if Int(_1!) & Int(1 << 19) != 0 { + if Int(_1 ?? 0) & Int(1 << 19) != 0 { _17 = reader.readInt32() } var _18: Int32? - if Int(_1!) & Int(1 << 20) != 0 { + if Int(_1 ?? 0) & Int(1 << 20) != 0 { _18 = reader.readInt32() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 7) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 8) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 13) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 14) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 15) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 16) == 0) || _15 != nil - let _c16 = (Int(_1!) & Int(1 << 18) == 0) || _16 != nil - let _c17 = (Int(_1!) & Int(1 << 19) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 20) == 0) || _18 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 13) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 14) == 0) || _13 != nil + let _c14 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _14 != nil + let _c15 = (Int(_1 ?? 0) & Int(1 << 16) == 0) || _15 != nil + let _c16 = (Int(_1 ?? 0) & Int(1 << 18) == 0) || _16 != nil + let _c17 = (Int(_1 ?? 0) & Int(1 << 19) == 0) || _17 != nil + let _c18 = (Int(_1 ?? 0) & Int(1 << 20) == 0) || _18 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 { return Api.SavedStarGift.savedStarGift(Cons_savedStarGift(flags: _1!, fromId: _2, date: _3!, gift: _4!, message: _5, msgId: _6, savedId: _7, convertStars: _8, upgradeStars: _9, canExportAt: _10, transferStars: _11, canTransferAt: _12, canResellAt: _13, collectionId: _14, prepaidUpgradeHash: _15, dropOriginalDetailsStars: _16, giftNum: _17, canCraftAt: _18)) } @@ -1125,7 +1125,7 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = reader.readInt32() } var _5: Int64? @@ -1133,7 +1133,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.SearchPostsFlood.searchPostsFlood(Cons_searchPostsFlood(flags: _1!, totalDaily: _2!, remains: _3!, waitTill: _4, starsAmount: _5!)) diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index a72b6cde79..cf8473ccd7 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -304,43 +304,43 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.SecureValueType } var _3: Api.SecureData? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.SecureData } } var _4: Api.SecureFile? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.SecureFile } } var _5: Api.SecureFile? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.SecureFile } } var _6: Api.SecureFile? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.SecureFile } } var _7: [Api.SecureFile]? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let _ = reader.readInt32() { _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SecureFile.self) } } var _8: [Api.SecureFile]? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let _ = reader.readInt32() { _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SecureFile.self) } } var _9: Api.SecurePlainData? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.SecurePlainData } @@ -349,13 +349,13 @@ public extension Api { _10 = parseBytes(reader) let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 6) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 5) == 0) || _9 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _9 != nil let _c10 = _10 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { return Api.SecureValue.secureValue(Cons_secureValue(flags: _1!, type: _2!, data: _3, frontSide: _4, reverseSide: _5, selfie: _6, translation: _7, files: _8, plainData: _9, hash: _10!)) diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 36ee647b44..0c487ab302 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -218,25 +218,25 @@ public extension Api { var _5: String? _5 = parseString(reader) var _6: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _7: Api.Photo? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.Photo } } var _8: Api.MessageMedia? - if Int(_1!) & Int(1 << 14) != 0 { + if Int(_1 ?? 0) & Int(1 << 14) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.MessageMedia } } var _9: Api.PeerColor? - if Int(_1!) & Int(1 << 13) != 0 { + if Int(_1 ?? 0) & Int(1 << 13) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.PeerColor } @@ -244,19 +244,19 @@ public extension Api { var _10: String? _10 = parseString(reader) var _11: String? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _11 = parseString(reader) } var _12: String? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _12 = parseString(reader) } var _13: Int32? - if Int(_1!) & Int(1 << 15) != 0 { + if Int(_1 ?? 0) & Int(1 << 15) != 0 { _13 = reader.readInt32() } var _14: Int32? - if Int(_1!) & Int(1 << 15) != 0 { + if Int(_1 ?? 0) & Int(1 << 15) != 0 { _14 = reader.readInt32() } let _c1 = _1 != nil @@ -264,15 +264,15 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 6) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 14) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 13) == 0) || _9 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 14) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 13) == 0) || _9 != nil let _c10 = _10 != nil - let _c11 = (Int(_1!) & Int(1 << 7) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 8) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 15) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 15) == 0) || _14 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _13 != nil + let _c14 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _14 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { return Api.SponsoredMessage.sponsoredMessage(Cons_sponsoredMessage(flags: _1!, randomId: _2!, url: _3!, title: _4!, message: _5!, entities: _6, photo: _7, media: _8, color: _9, buttonText: _10!, sponsorInfo: _11, additionalInfo: _12, minDisplayDuration: _13, maxDisplayDuration: _14)) } @@ -389,18 +389,18 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.Peer } var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } var _5: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.SponsoredPeer.sponsoredPeer(Cons_sponsoredPeer(flags: _1!, randomId: _2!, peer: _3!, sponsorInfo: _4, additionalInfo: _5)) } @@ -670,75 +670,75 @@ public extension Api { var _4: Int64? _4 = reader.readInt64() var _5: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = reader.readInt32() } var _6: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = reader.readInt32() } var _7: Int64? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _7 = reader.readInt64() } var _8: Int64? _8 = reader.readInt64() var _9: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _9 = reader.readInt32() } var _10: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _10 = reader.readInt32() } var _11: Int64? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _11 = reader.readInt64() } var _12: Int64? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _12 = reader.readInt64() } var _13: String? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _13 = parseString(reader) } var _14: Api.Peer? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let signature = reader.readInt32() { _14 = Api.parse(reader, signature: signature) as? Api.Peer } } var _15: Int32? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _15 = reader.readInt32() } var _16: Int32? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _16 = reader.readInt32() } var _17: Int32? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { _17 = reader.readInt32() } var _18: String? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _18 = parseString(reader) } var _19: Int32? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _19 = reader.readInt32() } var _20: Int32? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _20 = reader.readInt32() } var _21: Int32? - if Int(_1!) & Int(1 << 12) != 0 { + if Int(_1 ?? 0) & Int(1 << 12) != 0 { _21 = reader.readInt32() } var _22: Api.StarGiftBackground? - if Int(_1!) & Int(1 << 13) != 0 { + if Int(_1 ?? 0) & Int(1 << 13) != 0 { if let signature = reader.readInt32() { _22 = Api.parse(reader, signature: signature) as? Api.StarGiftBackground } @@ -747,24 +747,24 @@ public extension Api { let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _7 != nil let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 3) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 4) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 5) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 6) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 8) == 0) || _15 != nil - let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil - let _c17 = (Int(_1!) & Int(1 << 9) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 11) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 11) == 0) || _19 != nil - let _c20 = (Int(_1!) & Int(1 << 11) == 0) || _20 != nil - let _c21 = (Int(_1!) & Int(1 << 12) == 0) || _21 != nil - let _c22 = (Int(_1!) & Int(1 << 13) == 0) || _22 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _13 != nil + let _c14 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _14 != nil + let _c15 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _15 != nil + let _c16 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _16 != nil + let _c17 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _17 != nil + let _c18 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _18 != nil + let _c19 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _19 != nil + let _c20 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _20 != nil + let _c21 = (Int(_1 ?? 0) & Int(1 << 12) == 0) || _21 != nil + let _c22 = (Int(_1 ?? 0) & Int(1 << 13) == 0) || _22 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 { return Api.StarGift.starGift(Cons_starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, availabilityResale: _7, convertStars: _8!, firstSaleDate: _9, lastSaleDate: _10, upgradeStars: _11, resellMinStars: _12, title: _13, releasedBy: _14, perUserTotal: _15, perUserRemains: _16, lockedUntilDate: _17, auctionSlug: _18, giftsPerRound: _19, auctionStartDate: _20, upgradeVariants: _21, background: _22)) } @@ -786,17 +786,17 @@ public extension Api { var _6: Int32? _6 = reader.readInt32() var _7: Api.Peer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.Peer } } var _8: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _8 = parseString(reader) } var _9: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _9 = parseString(reader) } var _10: [Api.StarGiftAttribute]? @@ -808,57 +808,57 @@ public extension Api { var _12: Int32? _12 = reader.readInt32() var _13: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _13 = parseString(reader) } var _14: [Api.StarsAmount]? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let _ = reader.readInt32() { _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsAmount.self) } } var _15: Api.Peer? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _15 = Api.parse(reader, signature: signature) as? Api.Peer } } var _16: Int64? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _16 = reader.readInt64() } var _17: String? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _17 = parseString(reader) } var _18: Int64? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _18 = reader.readInt64() } var _19: Api.Peer? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _19 = Api.parse(reader, signature: signature) as? Api.Peer } } var _20: Api.PeerColor? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { if let signature = reader.readInt32() { _20 = Api.parse(reader, signature: signature) as? Api.PeerColor } } var _21: Api.Peer? - if Int(_1!) & Int(1 << 12) != 0 { + if Int(_1 ?? 0) & Int(1 << 12) != 0 { if let signature = reader.readInt32() { _21 = Api.parse(reader, signature: signature) as? Api.Peer } } var _22: Int32? - if Int(_1!) & Int(1 << 13) != 0 { + if Int(_1 ?? 0) & Int(1 << 13) != 0 { _22 = reader.readInt32() } var _23: Int32? - if Int(_1!) & Int(1 << 16) != 0 { + if Int(_1 ?? 0) & Int(1 << 16) != 0 { _23 = reader.readInt32() } let _c1 = _1 != nil @@ -867,23 +867,23 @@ public extension Api { let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _9 != nil let _c10 = _10 != nil let _c11 = _11 != nil let _c12 = _12 != nil - let _c13 = (Int(_1!) & Int(1 << 3) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 4) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 5) == 0) || _15 != nil - let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil - let _c17 = (Int(_1!) & Int(1 << 8) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 8) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 10) == 0) || _19 != nil - let _c20 = (Int(_1!) & Int(1 << 11) == 0) || _20 != nil - let _c21 = (Int(_1!) & Int(1 << 12) == 0) || _21 != nil - let _c22 = (Int(_1!) & Int(1 << 13) == 0) || _22 != nil - let _c23 = (Int(_1!) & Int(1 << 16) == 0) || _23 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _13 != nil + let _c14 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _14 != nil + let _c15 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _15 != nil + let _c16 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _16 != nil + let _c17 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _17 != nil + let _c18 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _18 != nil + let _c19 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _19 != nil + let _c20 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _20 != nil + let _c21 = (Int(_1 ?? 0) & Int(1 << 12) == 0) || _21 != nil + let _c22 = (Int(_1 ?? 0) & Int(1 << 13) == 0) || _22 != nil + let _c23 = (Int(_1 ?? 0) & Int(1 << 16) == 0) || _23 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 { return Api.StarGift.starGiftUnique(Cons_starGiftUnique(flags: _1!, id: _2!, giftId: _3!, title: _4!, slug: _5!, num: _6!, ownerId: _7, ownerName: _8, ownerAddress: _9, attributes: _10!, availabilityIssued: _11!, availabilityTotal: _12!, giftAddress: _13, resellAmount: _14, releasedBy: _15, valueAmount: _16, valueCurrency: _17, valueUsdAmount: _18, themePeer: _19, peerColor: _20, hostId: _21, offerMinStars: _22, craftChancePermille: _23)) } @@ -1148,7 +1148,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Peer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Peer } @@ -1160,16 +1160,16 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() var _5: Api.TextWithEntities? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.StarGiftAttribute.starGiftAttributeOriginalDetails(Cons_starGiftAttributeOriginalDetails(flags: _1!, senderId: _2, recipientId: _3!, date: _4!, message: _5)) } @@ -1514,13 +1514,13 @@ public extension Api { var _6: Int32? _6 = reader.readInt32() var _7: Api.TextWithEntities? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } } var _8: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _8 = reader.readInt32() } let _c1 = _1 != nil @@ -1529,8 +1529,8 @@ public extension Api { let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.StarGiftAuctionAcquiredGift.starGiftAuctionAcquiredGift(Cons_starGiftAuctionAcquiredGift(flags: _1!, peer: _2!, date: _3!, bidAmount: _4!, round: _5!, pos: _6!, message: _7, giftNum: _8)) } @@ -1823,24 +1823,24 @@ public extension Api { var _4: Int64? _4 = reader.readInt64() var _5: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = reader.readInt32() } var _6: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _6 = reader.readInt32() } var _7: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _7 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.StarGiftAuctionState.starGiftAuctionStateFinished(Cons_starGiftAuctionStateFinished(flags: _1!, startDate: _2!, endDate: _3!, averagePrice: _4!, listedCount: _5, fragmentListedCount: _6, fragmentListedUrl: _7)) } @@ -1911,19 +1911,19 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt64() } var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = reader.readInt64() } var _5: Api.Peer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.Peer } @@ -1931,10 +1931,10 @@ public extension Api { var _6: Int32? _6 = reader.readInt32() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil let _c6 = _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.StarGiftAuctionUserState.starGiftAuctionUserState(Cons_starGiftAuctionUserState(flags: _1!, bidAmount: _2, bidDate: _3, minBidAmount: _4, bidPeer: _5, acquiredCount: _6!)) @@ -2057,7 +2057,7 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Api.Document? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Document } @@ -2069,7 +2069,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { @@ -2191,15 +2191,15 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = reader.readInt32() } var _5: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = reader.readInt32() } var _6: Api.StarsAmount? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.StarsAmount } @@ -2207,9 +2207,9 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.StarRefProgram.starRefProgram(Cons_starRefProgram(flags: _1!, botId: _2!, commissionPermille: _3!, durationMonths: _4, endDate: _5, dailyRevenuePerUser: _6)) } diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index 8bc5c73f45..0fa1dcfe1c 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -128,7 +128,7 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } var _4: String? @@ -137,7 +137,7 @@ public extension Api { _5 = reader.readInt64() let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { @@ -212,7 +212,7 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = parseString(reader) } var _5: String? @@ -226,7 +226,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil @@ -350,14 +350,14 @@ public extension Api { var _4: Int64? _4 = reader.readInt64() var _5: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.StarsRating.starsRating(Cons_starsRating(flags: _1!, level: _2!, currentLevelStars: _3!, stars: _4!, nextLevelStars: _5)) } @@ -428,14 +428,14 @@ public extension Api { _4 = Api.parse(reader, signature: signature) as? Api.StarsAmount } var _5: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.StarsRevenueStatus.starsRevenueStatus(Cons_starsRevenueStatus(flags: _1!, currentBalance: _2!, availableBalance: _3!, overallRevenue: _4!, nextWithdrawalAt: _5)) } @@ -524,21 +524,21 @@ public extension Api { _5 = Api.parse(reader, signature: signature) as? Api.StarsSubscriptionPricing } var _6: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _6 = parseString(reader) } var _7: String? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _7 = parseString(reader) } var _8: Api.WebDocument? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.WebDocument } } var _9: String? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _9 = parseString(reader) } let _c1 = _1 != nil @@ -546,10 +546,10 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 5) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.StarsSubscription.starsSubscription(Cons_starsSubscription(flags: _1!, id: _2!, peer: _3!, untilDate: _4!, pricing: _5!, chatInviteHash: _6, title: _7, photo: _8, invoiceSlug: _9)) } @@ -660,7 +660,7 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } var _4: String? @@ -669,7 +669,7 @@ public extension Api { _5 = reader.readInt64() let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { @@ -839,89 +839,89 @@ public extension Api { _5 = Api.parse(reader, signature: signature) as? Api.StarsTransactionPeer } var _6: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = parseString(reader) } var _7: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _7 = parseString(reader) } var _8: Api.WebDocument? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.WebDocument } } var _9: Int32? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _9 = reader.readInt32() } var _10: String? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _10 = parseString(reader) } var _11: Buffer? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _11 = parseBytes(reader) } var _12: Int32? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _12 = reader.readInt32() } var _13: [Api.MessageMedia]? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { if let _ = reader.readInt32() { _13 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageMedia.self) } } var _14: Int32? - if Int(_1!) & Int(1 << 12) != 0 { + if Int(_1 ?? 0) & Int(1 << 12) != 0 { _14 = reader.readInt32() } var _15: Int32? - if Int(_1!) & Int(1 << 13) != 0 { + if Int(_1 ?? 0) & Int(1 << 13) != 0 { _15 = reader.readInt32() } var _16: Api.StarGift? - if Int(_1!) & Int(1 << 14) != 0 { + if Int(_1 ?? 0) & Int(1 << 14) != 0 { if let signature = reader.readInt32() { _16 = Api.parse(reader, signature: signature) as? Api.StarGift } } var _17: Int32? - if Int(_1!) & Int(1 << 15) != 0 { + if Int(_1 ?? 0) & Int(1 << 15) != 0 { _17 = reader.readInt32() } var _18: Int32? - if Int(_1!) & Int(1 << 16) != 0 { + if Int(_1 ?? 0) & Int(1 << 16) != 0 { _18 = reader.readInt32() } var _19: Api.Peer? - if Int(_1!) & Int(1 << 17) != 0 { + if Int(_1 ?? 0) & Int(1 << 17) != 0 { if let signature = reader.readInt32() { _19 = Api.parse(reader, signature: signature) as? Api.Peer } } var _20: Api.StarsAmount? - if Int(_1!) & Int(1 << 17) != 0 { + if Int(_1 ?? 0) & Int(1 << 17) != 0 { if let signature = reader.readInt32() { _20 = Api.parse(reader, signature: signature) as? Api.StarsAmount } } var _21: Int32? - if Int(_1!) & Int(1 << 19) != 0 { + if Int(_1 ?? 0) & Int(1 << 19) != 0 { _21 = reader.readInt32() } var _22: Int32? - if Int(_1!) & Int(1 << 20) != 0 { + if Int(_1 ?? 0) & Int(1 << 20) != 0 { _22 = reader.readInt32() } var _23: Int32? - if Int(_1!) & Int(1 << 23) != 0 { + if Int(_1 ?? 0) & Int(1 << 23) != 0 { _23 = reader.readInt32() } var _24: Int32? - if Int(_1!) & Int(1 << 23) != 0 { + if Int(_1 ?? 0) & Int(1 << 23) != 0 { _24 = reader.readInt32() } let _c1 = _1 != nil @@ -929,25 +929,25 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 5) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 5) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 7) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 8) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 9) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 12) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 13) == 0) || _15 != nil - let _c16 = (Int(_1!) & Int(1 << 14) == 0) || _16 != nil - let _c17 = (Int(_1!) & Int(1 << 15) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 16) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 17) == 0) || _19 != nil - let _c20 = (Int(_1!) & Int(1 << 17) == 0) || _20 != nil - let _c21 = (Int(_1!) & Int(1 << 19) == 0) || _21 != nil - let _c22 = (Int(_1!) & Int(1 << 20) == 0) || _22 != nil - let _c23 = (Int(_1!) & Int(1 << 23) == 0) || _23 != nil - let _c24 = (Int(_1!) & Int(1 << 23) == 0) || _24 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _13 != nil + let _c14 = (Int(_1 ?? 0) & Int(1 << 12) == 0) || _14 != nil + let _c15 = (Int(_1 ?? 0) & Int(1 << 13) == 0) || _15 != nil + let _c16 = (Int(_1 ?? 0) & Int(1 << 14) == 0) || _16 != nil + let _c17 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _17 != nil + let _c18 = (Int(_1 ?? 0) & Int(1 << 16) == 0) || _18 != nil + let _c19 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _19 != nil + let _c20 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _20 != nil + let _c21 = (Int(_1 ?? 0) & Int(1 << 19) == 0) || _21 != nil + let _c22 = (Int(_1 ?? 0) & Int(1 << 20) == 0) || _22 != nil + let _c23 = (Int(_1 ?? 0) & Int(1 << 23) == 0) || _23 != nil + let _c24 = (Int(_1 ?? 0) & Int(1 << 23) == 0) || _24 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 { return Api.StarsTransaction.starsTransaction(Cons_starsTransaction(flags: _1!, id: _2!, amount: _3!, date: _4!, peer: _5!, title: _6, description: _7, photo: _8, transactionDate: _9, transactionUrl: _10, botPayload: _11, msgId: _12, extendedMedia: _13, subscriptionPeriod: _14, giveawayPostId: _15, stargift: _16, floodskipNumber: _17, starrefCommissionPermille: _18, starrefPeer: _19, starrefAmount: _20, paidMessages: _21, premiumGiftMonths: _22, adsProceedsFromDate: _23, adsProceedsToDate: _24)) } @@ -1263,12 +1263,12 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.DataJSON } var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.StatsGraph.statsGraph(Cons_statsGraph(flags: _1!, json: _2!, zoomToken: _3)) } @@ -1756,7 +1756,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } var _3: Int64? @@ -1768,21 +1768,21 @@ public extension Api { var _6: String? _6 = parseString(reader) var _7: [Api.PhotoSize]? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let _ = reader.readInt32() { _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PhotoSize.self) } } var _8: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _8 = reader.readInt32() } var _9: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _9 = reader.readInt32() } var _10: Int64? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _10 = reader.readInt64() } var _11: Int32? @@ -1790,15 +1790,15 @@ public extension Api { var _12: Int32? _12 = reader.readInt32() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 8) == 0) || _10 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _10 != nil let _c11 = _11 != nil let _c12 = _12 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index eb5b139182..018d2238a9 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -238,16 +238,16 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = reader.readInt32() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.StoriesStealthMode.storiesStealthMode(Cons_storiesStealthMode(flags: _1!, activeUntilDate: _2, cooldownUntilDate: _3)) } @@ -312,13 +312,13 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Api.Photo? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Photo } } var _5: Api.Document? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.Document } @@ -326,8 +326,8 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.StoryAlbum.storyAlbum(Cons_storyAlbum(flags: _1!, albumId: _2!, title: _3!, iconPhoto: _4, iconVideo: _5)) } @@ -387,23 +387,23 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Peer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Peer } } var _3: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = parseString(reader) } var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = reader.readInt32() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.StoryFwdHeader.storyFwdHeader(Cons_storyFwdHeader(flags: _1!, from: _2, fromName: _3, storyId: _4)) } @@ -575,13 +575,13 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Api.Peer? - if Int(_1!) & Int(1 << 18) != 0 { + if Int(_1 ?? 0) & Int(1 << 18) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Peer } } var _5: Api.StoryFwdHeader? - if Int(_1!) & Int(1 << 17) != 0 { + if Int(_1 ?? 0) & Int(1 << 17) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.StoryFwdHeader } @@ -589,11 +589,11 @@ public extension Api { var _6: Int32? _6 = reader.readInt32() var _7: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _7 = parseString(reader) } var _8: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } @@ -603,37 +603,37 @@ public extension Api { _9 = Api.parse(reader, signature: signature) as? Api.MessageMedia } var _10: [Api.MediaArea]? - if Int(_1!) & Int(1 << 14) != 0 { + if Int(_1 ?? 0) & Int(1 << 14) != 0 { if let _ = reader.readInt32() { _10 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MediaArea.self) } } var _11: [Api.PrivacyRule]? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let _ = reader.readInt32() { _11 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PrivacyRule.self) } } var _12: Api.StoryViews? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _12 = Api.parse(reader, signature: signature) as? Api.StoryViews } } var _13: Api.Reaction? - if Int(_1!) & Int(1 << 15) != 0 { + if Int(_1 ?? 0) & Int(1 << 15) != 0 { if let signature = reader.readInt32() { _13 = Api.parse(reader, signature: signature) as? Api.Reaction } } var _14: [Int32]? - if Int(_1!) & Int(1 << 19) != 0 { + if Int(_1 ?? 0) & Int(1 << 19) != 0 { if let _ = reader.readInt32() { _14 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } } var _15: Api.Document? - if Int(_1!) & Int(1 << 20) != 0 { + if Int(_1 ?? 0) & Int(1 << 20) != 0 { if let signature = reader.readInt32() { _15 = Api.parse(reader, signature: signature) as? Api.Document } @@ -641,18 +641,18 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 18) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 17) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 18) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _8 != nil let _c9 = _9 != nil - let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 2) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 3) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 15) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 19) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 20) == 0) || _15 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 14) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _13 != nil + let _c14 = (Int(_1 ?? 0) & Int(1 << 19) == 0) || _14 != nil + let _c15 = (Int(_1 ?? 0) & Int(1 << 20) == 0) || _15 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { return Api.StoryItem.storyItem(Cons_storyItem(flags: _1!, id: _2!, date: _3!, fromId: _4, fwdFrom: _5, expireDate: _6!, caption: _7, entities: _8, media: _9!, mediaAreas: _10, privacy: _11, views: _12, sentReaction: _13, albums: _14, music: _15)) } @@ -918,7 +918,7 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Api.Reaction? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Reaction } @@ -926,7 +926,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.StoryView.storyView(Cons_storyView(flags: _1!, userId: _2!, date: _3!, reaction: _4)) } @@ -1041,31 +1041,31 @@ public extension Api { var _2: Int32? _2 = reader.readInt32() var _3: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _3 = reader.readInt32() } var _4: [Api.ReactionCount]? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ReactionCount.self) } } var _5: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _5 = reader.readInt32() } var _6: [Int64]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _6 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.StoryViews.storyViews(Cons_storyViews(flags: _1!, viewsCount: _2!, forwardsCount: _3, reactions: _4, reactionsCount: _5, recentViewers: _6)) } @@ -1120,18 +1120,18 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.StarsAmount? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.StarsAmount } } var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 3) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.SuggestedPost.suggestedPost(Cons_suggestedPost(flags: _1!, price: _2, scheduleDate: _3)) } @@ -1276,23 +1276,23 @@ public extension Api { var _5: String? _5 = parseString(reader) var _6: Api.Document? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Document } } var _7: [Api.ThemeSettings]? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let _ = reader.readInt32() { _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ThemeSettings.self) } } var _8: String? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _8 = parseString(reader) } var _9: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _9 = reader.readInt32() } let _c1 = _1 != nil @@ -1300,10 +1300,10 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 6) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.Theme.theme(Cons_theme(flags: _1!, id: _2!, accessHash: _3!, slug: _4!, title: _5!, document: _6, settings: _7, emoticon: _8, installsCount: _9)) } @@ -1379,17 +1379,17 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _4 = reader.readInt32() } var _5: [Int32]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _5 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } } var _6: Api.WallPaper? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.WallPaper } @@ -1397,9 +1397,9 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.ThemeSettings.themeSettings(Cons_themeSettings(flags: _1!, baseTheme: _2!, accentColor: _3!, outboxAccentColor: _4, messageColors: _5, wallpaper: _6)) } @@ -1694,6 +1694,7 @@ public extension Api { public extension Api { enum TopPeerCategory: TypeConstructorDescription { case topPeerCategoryBotsApp + case topPeerCategoryBotsGuestChat case topPeerCategoryBotsInline case topPeerCategoryBotsPM case topPeerCategoryChannels @@ -1710,6 +1711,11 @@ public extension Api { buffer.appendInt32(-39945236) } break + case .topPeerCategoryBotsGuestChat: + if boxed { + buffer.appendInt32(1814361053) + } + break case .topPeerCategoryBotsInline: if boxed { buffer.appendInt32(344356834) @@ -1757,6 +1763,8 @@ public extension Api { switch self { case .topPeerCategoryBotsApp: return ("topPeerCategoryBotsApp", []) + case .topPeerCategoryBotsGuestChat: + return ("topPeerCategoryBotsGuestChat", []) case .topPeerCategoryBotsInline: return ("topPeerCategoryBotsInline", []) case .topPeerCategoryBotsPM: @@ -1779,6 +1787,9 @@ public extension Api { public static func parse_topPeerCategoryBotsApp(_ reader: BufferReader) -> TopPeerCategory? { return Api.TopPeerCategory.topPeerCategoryBotsApp } + public static func parse_topPeerCategoryBotsGuestChat(_ reader: BufferReader) -> TopPeerCategory? { + return Api.TopPeerCategory.topPeerCategoryBotsGuestChat + } public static func parse_topPeerCategoryBotsInline(_ reader: BufferReader) -> TopPeerCategory? { return Api.TopPeerCategory.topPeerCategoryBotsInline } diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index e3507ba121..9412289751 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -175,6 +175,23 @@ public extension Api { return ("updateBotEditBusinessMessage", [("flags", ConstructorParameterDescription(self.flags)), ("connectionId", ConstructorParameterDescription(self.connectionId)), ("message", ConstructorParameterDescription(self.message)), ("replyToMessage", ConstructorParameterDescription(self.replyToMessage)), ("qts", ConstructorParameterDescription(self.qts))]) } } + public class Cons_updateBotGuestChatQuery: TypeConstructorDescription { + public var flags: Int32 + public var queryId: Int64 + public var message: Api.Message + public var referenceMessages: [Api.Message]? + public var qts: Int32 + public init(flags: Int32, queryId: Int64, message: Api.Message, referenceMessages: [Api.Message]?, qts: Int32) { + self.flags = flags + self.queryId = queryId + self.message = message + self.referenceMessages = referenceMessages + self.qts = qts + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("updateBotGuestChatQuery", [("flags", ConstructorParameterDescription(self.flags)), ("queryId", ConstructorParameterDescription(self.queryId)), ("message", ConstructorParameterDescription(self.message)), ("referenceMessages", ConstructorParameterDescription(self.referenceMessages)), ("qts", ConstructorParameterDescription(self.qts))]) + } + } public class Cons_updateBotInlineQuery: TypeConstructorDescription { public var flags: Int32 public var queryId: Int64 @@ -1883,6 +1900,7 @@ public extension Api { return ("updateWebViewResultSent", [("queryId", ConstructorParameterDescription(self.queryId))]) } } + case updateAiComposeTones case updateAttachMenuBots case updateAutoSaveSettings case updateBotBusinessConnect(Cons_updateBotBusinessConnect) @@ -1892,6 +1910,7 @@ public extension Api { case updateBotCommands(Cons_updateBotCommands) case updateBotDeleteBusinessMessage(Cons_updateBotDeleteBusinessMessage) case updateBotEditBusinessMessage(Cons_updateBotEditBusinessMessage) + case updateBotGuestChatQuery(Cons_updateBotGuestChatQuery) case updateBotInlineQuery(Cons_updateBotInlineQuery) case updateBotInlineSend(Cons_updateBotInlineSend) case updateBotMenuButton(Cons_updateBotMenuButton) @@ -2040,6 +2059,11 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .updateAiComposeTones: + if boxed { + buffer.appendInt32(-1945136645) + } + break case .updateAttachMenuBots: if boxed { buffer.appendInt32(397910539) @@ -2130,6 +2154,22 @@ public extension Api { } serializeInt32(_data.qts, buffer: buffer, boxed: false) break + case .updateBotGuestChatQuery(let _data): + if boxed { + buffer.appendInt32(-841742019) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeInt64(_data.queryId, buffer: buffer, boxed: false) + _data.message.serialize(buffer, true) + if Int(_data.flags) & Int(1 << 0) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.referenceMessages!.count)) + for item in _data.referenceMessages! { + item.serialize(buffer, true) + } + } + serializeInt32(_data.qts, buffer: buffer, boxed: false) + break case .updateBotInlineQuery(let _data): if boxed { buffer.appendInt32(1232025500) @@ -3485,6 +3525,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { switch self { + case .updateAiComposeTones: + return ("updateAiComposeTones", []) case .updateAttachMenuBots: return ("updateAttachMenuBots", []) case .updateAutoSaveSettings: @@ -3503,6 +3545,8 @@ public extension Api { return ("updateBotDeleteBusinessMessage", [("connectionId", ConstructorParameterDescription(_data.connectionId)), ("peer", ConstructorParameterDescription(_data.peer)), ("messages", ConstructorParameterDescription(_data.messages)), ("qts", ConstructorParameterDescription(_data.qts))]) case .updateBotEditBusinessMessage(let _data): return ("updateBotEditBusinessMessage", [("flags", ConstructorParameterDescription(_data.flags)), ("connectionId", ConstructorParameterDescription(_data.connectionId)), ("message", ConstructorParameterDescription(_data.message)), ("replyToMessage", ConstructorParameterDescription(_data.replyToMessage)), ("qts", ConstructorParameterDescription(_data.qts))]) + case .updateBotGuestChatQuery(let _data): + return ("updateBotGuestChatQuery", [("flags", ConstructorParameterDescription(_data.flags)), ("queryId", ConstructorParameterDescription(_data.queryId)), ("message", ConstructorParameterDescription(_data.message)), ("referenceMessages", ConstructorParameterDescription(_data.referenceMessages)), ("qts", ConstructorParameterDescription(_data.qts))]) case .updateBotInlineQuery(let _data): return ("updateBotInlineQuery", [("flags", ConstructorParameterDescription(_data.flags)), ("queryId", ConstructorParameterDescription(_data.queryId)), ("userId", ConstructorParameterDescription(_data.userId)), ("query", ConstructorParameterDescription(_data.query)), ("geo", ConstructorParameterDescription(_data.geo)), ("peerType", ConstructorParameterDescription(_data.peerType)), ("offset", ConstructorParameterDescription(_data.offset))]) case .updateBotInlineSend(let _data): @@ -3796,6 +3840,9 @@ public extension Api { } } + public static func parse_updateAiComposeTones(_ reader: BufferReader) -> Update? { + return Api.Update.updateAiComposeTones + } public static func parse_updateAttachMenuBots(_ reader: BufferReader) -> Update? { return Api.Update.updateAttachMenuBots } @@ -3834,11 +3881,11 @@ public extension Api { var _6: Int64? _6 = reader.readInt64() var _7: Buffer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _7 = parseBytes(reader) } var _8: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _8 = parseString(reader) } let _c1 = _1 != nil @@ -3847,8 +3894,8 @@ public extension Api { let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.Update.updateBotCallbackQuery(Cons_updateBotCallbackQuery(flags: _1!, queryId: _2!, userId: _3!, peer: _4!, msgId: _5!, chatInstance: _6!, data: _7, gameShortName: _8)) } @@ -3962,7 +4009,7 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.Message } var _4: Api.Message? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Message } @@ -3972,7 +4019,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.Update.updateBotEditBusinessMessage(Cons_updateBotEditBusinessMessage(flags: _1!, connectionId: _2!, message: _3!, replyToMessage: _4, qts: _5!)) @@ -3981,6 +4028,35 @@ public extension Api { return nil } } + public static func parse_updateBotGuestChatQuery(_ reader: BufferReader) -> Update? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Api.Message? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.Message + } + var _4: [Api.Message]? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + } + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.Update.updateBotGuestChatQuery(Cons_updateBotGuestChatQuery(flags: _1!, queryId: _2!, message: _3!, referenceMessages: _4, qts: _5!)) + } + else { + return nil + } + } public static func parse_updateBotInlineQuery(_ reader: BufferReader) -> Update? { var _1: Int32? _1 = reader.readInt32() @@ -3991,13 +4067,13 @@ public extension Api { var _4: String? _4 = parseString(reader) var _5: Api.GeoPoint? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.GeoPoint } } var _6: Api.InlineQueryPeerType? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.InlineQueryPeerType } @@ -4008,8 +4084,8 @@ public extension Api { let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil let _c7 = _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.Update.updateBotInlineQuery(Cons_updateBotInlineQuery(flags: _1!, queryId: _2!, userId: _3!, query: _4!, geo: _5, peerType: _6, offset: _7!)) @@ -4026,7 +4102,7 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Api.GeoPoint? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.GeoPoint } @@ -4034,7 +4110,7 @@ public extension Api { var _5: String? _5 = parseString(reader) var _6: Api.InputBotInlineMessageID? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.InputBotInlineMessageID } @@ -4042,9 +4118,9 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.Update.updateBotInlineSend(Cons_updateBotInlineSend(flags: _1!, userId: _2!, query: _3!, geo: _4, id: _5!, msgId: _6)) } @@ -4142,7 +4218,7 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.Message } var _4: Api.Message? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Message } @@ -4152,7 +4228,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.Update.updateBotNewBusinessMessage(Cons_updateBotNewBusinessMessage(flags: _1!, connectionId: _2!, message: _3!, replyToMessage: _4, qts: _5!)) @@ -4171,13 +4247,13 @@ public extension Api { var _4: Buffer? _4 = parseBytes(reader) var _5: Api.PaymentRequestedInfo? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo } } var _6: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _6 = parseString(reader) } var _7: String? @@ -4188,8 +4264,8 @@ public extension Api { let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { @@ -4306,7 +4382,7 @@ public extension Api { _5 = Api.parse(reader, signature: signature) as? Api.Message } var _6: Api.Message? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Message } @@ -4314,7 +4390,7 @@ public extension Api { var _7: Int64? _7 = reader.readInt64() var _8: Buffer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _8 = parseBytes(reader) } let _c1 = _1 != nil @@ -4322,9 +4398,9 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 0) == 0) || _8 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.Update.updateBusinessBotCallbackQuery(Cons_updateBusinessBotCallbackQuery(flags: _1!, queryId: _2!, userId: _3!, connectionId: _4!, message: _5!, replyToMessage: _6, chatInstance: _7!, data: _8)) } @@ -4403,19 +4479,19 @@ public extension Api { var _5: Int64? _5 = reader.readInt64() var _6: Api.ChannelParticipant? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.ChannelParticipant } } var _7: Api.ChannelParticipant? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.ChannelParticipant } } var _8: Api.ExportedChatInvite? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite } @@ -4427,9 +4503,9 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _8 != nil let _c9 = _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.Update.updateChannelParticipant(Cons_updateChannelParticipant(flags: _1!, channelId: _2!, date: _3!, actorId: _4!, userId: _5!, prevParticipant: _6, newParticipant: _7, invite: _8, qts: _9!)) @@ -4444,11 +4520,11 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Api.Peer? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Peer } @@ -4459,8 +4535,8 @@ public extension Api { } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.Update.updateChannelReadMessagesContents(Cons_updateChannelReadMessagesContents(flags: _1!, channelId: _2!, topMsgId: _3, savedPeerId: _4, messages: _5!)) @@ -4475,12 +4551,12 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.Update.updateChannelTooLong(Cons_updateChannelTooLong(flags: _1!, channelId: _2!, pts: _3)) } @@ -4494,7 +4570,7 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Api.Peer? @@ -4507,7 +4583,7 @@ public extension Api { } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { @@ -4599,19 +4675,19 @@ public extension Api { var _5: Int64? _5 = reader.readInt64() var _6: Api.ChatParticipant? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.ChatParticipant } } var _7: Api.ChatParticipant? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.ChatParticipant } } var _8: Api.ExportedChatInvite? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite } @@ -4623,9 +4699,9 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _8 != nil let _c9 = _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.Update.updateChatParticipant(Cons_updateChatParticipant(flags: _1!, chatId: _2!, date: _3!, actorId: _4!, userId: _5!, prevParticipant: _6, newParticipant: _7, invite: _8, qts: _9!)) @@ -4867,7 +4943,7 @@ public extension Api { _3 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } var _4: [Int32]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } @@ -4875,7 +4951,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.Update.updateDeleteScheduledMessages(Cons_updateDeleteScheduledMessages(flags: _1!, peer: _2!, messages: _3!, sentMessages: _4)) } @@ -4889,14 +4965,14 @@ public extension Api { var _2: Int32? _2 = reader.readInt32() var _3: Api.DialogFilter? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.DialogFilter } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.Update.updateDialogFilter(Cons_updateDialogFilter(flags: _1!, id: _2!, filter: _3)) } @@ -4924,7 +5000,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _2 = reader.readInt32() } var _3: Api.DialogPeer? @@ -4932,7 +5008,7 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.DialogPeer } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { return Api.Update.updateDialogPinned(Cons_updateDialogPinned(flags: _1!, folderId: _2, peer: _3!)) @@ -4949,14 +5025,14 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.DialogPeer } var _3: Api.Peer? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Peer } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.Update.updateDialogUnreadMark(Cons_updateDialogUnreadMark(flags: _1!, peer: _2!, savedPeerId: _3)) } @@ -4972,11 +5048,11 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.Peer } var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Api.Peer? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Peer } @@ -4987,8 +5063,8 @@ public extension Api { } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.Update.updateDraftMessage(Cons_updateDraftMessage(flags: _1!, peer: _2!, topMsgId: _3, savedPeerId: _4, draft: _5!)) @@ -5134,7 +5210,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Peer? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Peer } @@ -5144,7 +5220,7 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.GroupCall } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { return Api.Update.updateGroupCall(Cons_updateGroupCall(flags: _1!, peer: _2, call: _3!)) @@ -5267,11 +5343,11 @@ public extension Api { var _5: Int64? _5 = reader.readInt64() var _6: Buffer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = parseBytes(reader) } var _7: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _7 = parseString(reader) } let _c1 = _1 != nil @@ -5279,8 +5355,8 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.Update.updateInlineBotCallbackQuery(Cons_updateInlineBotCallbackQuery(flags: _1!, queryId: _2!, userId: _3!, msgId: _4!, chatInstance: _5!, data: _6, gameShortName: _7)) } @@ -5371,23 +5447,23 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Peer? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Peer } } var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = reader.readInt32() } var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = reader.readInt32() } var _5: Int64? _5 = reader.readInt64() var _6: Api.Poll? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Poll } @@ -5397,11 +5473,11 @@ public extension Api { _7 = Api.parse(reader, signature: signature) as? Api.PollResults } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil let _c7 = _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.Update.updateMessagePoll(Cons_updateMessagePoll(flags: _1!, peer: _2, msgId: _3, topMsgId: _4, pollId: _5!, poll: _6, results: _7!)) @@ -5449,11 +5525,11 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = reader.readInt32() } var _5: Api.Peer? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.Peer } @@ -5465,8 +5541,8 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil let _c6 = _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.Update.updateMessageReactions(Cons_updateMessageReactions(flags: _1!, peer: _2!, msgId: _3!, topMsgId: _4, savedPeerId: _5, reactions: _6!)) @@ -5514,22 +5590,22 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } var _5: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.Update.updateNewAuthorization(Cons_updateNewAuthorization(flags: _1!, hash: _2!, date: _3, device: _4, location: _5)) } @@ -5706,12 +5782,12 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.Peer } var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.Update.updatePeerHistoryTTL(Cons_updatePeerHistoryTTL(flags: _1!, peer: _2!, ttlPeriod: _3)) } @@ -5758,14 +5834,14 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.Peer } var _3: Api.WallPaper? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.WallPaper } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.Update.updatePeerWallpaper(Cons_updatePeerWallpaper(flags: _1!, peer: _2!, wallpaper: _3)) } @@ -5850,18 +5926,18 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _2 = reader.readInt32() } var _3: [Api.DialogPeer]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DialogPeer.self) } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.Update.updatePinnedDialogs(Cons_updatePinnedDialogs(flags: _1!, folderId: _2, order: _3)) } @@ -5896,14 +5972,14 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.Peer } var _3: [Int32]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.Update.updatePinnedForumTopics(Cons_updatePinnedForumTopics(flags: _1!, peer: _2!, order: _3)) } @@ -5942,13 +6018,13 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: [Api.DialogPeer]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DialogPeer.self) } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil if _c1 && _c2 { return Api.Update.updatePinnedSavedDialogs(Cons_updatePinnedSavedDialogs(flags: _1!, order: _2)) } @@ -6013,19 +6089,19 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() var _5: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = reader.readInt64() } var _6: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.Update.updateReadChannelDiscussionInbox(Cons_updateReadChannelDiscussionInbox(flags: _1!, channelId: _2!, topMsgId: _3!, readMaxId: _4!, broadcastId: _5, broadcastPost: _6)) } @@ -6054,7 +6130,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } var _3: Int64? @@ -6066,7 +6142,7 @@ public extension Api { var _6: Int32? _6 = reader.readInt32() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil @@ -6102,7 +6178,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } var _3: Api.Peer? @@ -6110,7 +6186,7 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.Peer } var _4: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = reader.readInt32() } var _5: Int32? @@ -6122,9 +6198,9 @@ public extension Api { var _8: Int32? _8 = reader.readInt32() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil @@ -6170,14 +6246,14 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() var _5: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.Update.updateReadMessagesContents(Cons_updateReadMessagesContents(flags: _1!, messages: _2!, pts: _3!, ptsCount: _4!, date: _5)) } @@ -6311,7 +6387,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _2 = reader.readInt32() } var _3: String? @@ -6327,7 +6403,7 @@ public extension Api { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil @@ -6611,7 +6687,7 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Api.SendMessageAction? @@ -6620,7 +6696,7 @@ public extension Api { } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.Update.updateUserTyping(Cons_updateUserTyping(flags: _1!, userId: _2!, topMsgId: _3, action: _4!)) diff --git a/submodules/TelegramApi/Sources/Api29.swift b/submodules/TelegramApi/Sources/Api29.swift index c9cb55d312..6943418b04 100644 --- a/submodules/TelegramApi/Sources/Api29.swift +++ b/submodules/TelegramApi/Sources/Api29.swift @@ -343,29 +343,29 @@ public extension Api { var _8: Int32? _8 = reader.readInt32() var _9: Api.MessageFwdHeader? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.MessageFwdHeader } } var _10: Int64? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _10 = reader.readInt64() } var _11: Api.MessageReplyHeader? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _11 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader } } var _12: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { if let _ = reader.readInt32() { _12 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _13: Int32? - if Int(_1!) & Int(1 << 25) != 0 { + if Int(_1 ?? 0) & Int(1 << 25) != 0 { _13 = reader.readInt32() } let _c1 = _1 != nil @@ -376,11 +376,11 @@ public extension Api { let _c6 = _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 11) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 3) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 7) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 25) == 0) || _13 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 25) == 0) || _13 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 { return Api.Updates.updateShortChatMessage(Cons_updateShortChatMessage(flags: _1!, id: _2!, fromId: _3!, chatId: _4!, message: _5!, pts: _6!, ptsCount: _7!, date: _8!, fwdFrom: _9, viaBotId: _10, replyTo: _11, entities: _12, ttlPeriod: _13)) } @@ -404,29 +404,29 @@ public extension Api { var _7: Int32? _7 = reader.readInt32() var _8: Api.MessageFwdHeader? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.MessageFwdHeader } } var _9: Int64? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _9 = reader.readInt64() } var _10: Api.MessageReplyHeader? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.MessageReplyHeader } } var _11: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { if let _ = reader.readInt32() { _11 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _12: Int32? - if Int(_1!) & Int(1 << 25) != 0 { + if Int(_1 ?? 0) & Int(1 << 25) != 0 { _12 = reader.readInt32() } let _c1 = _1 != nil @@ -436,11 +436,11 @@ public extension Api { let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 11) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 3) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 7) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 25) == 0) || _12 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 25) == 0) || _12 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { return Api.Updates.updateShortMessage(Cons_updateShortMessage(flags: _1!, id: _2!, userId: _3!, message: _4!, pts: _5!, ptsCount: _6!, date: _7!, fwdFrom: _8, viaBotId: _9, replyTo: _10, entities: _11, ttlPeriod: _12)) } @@ -460,19 +460,19 @@ public extension Api { var _5: Int32? _5 = reader.readInt32() var _6: Api.MessageMedia? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.MessageMedia } } var _7: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { if let _ = reader.readInt32() { _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _8: Int32? - if Int(_1!) & Int(1 << 25) != 0 { + if Int(_1 ?? 0) & Int(1 << 25) != 0 { _8 = reader.readInt32() } let _c1 = _1 != nil @@ -480,9 +480,9 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 9) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 7) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 25) == 0) || _8 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 25) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.Updates.updateShortSentMessage(Cons_updateShortSentMessage(flags: _1!, id: _2!, pts: _3!, ptsCount: _4!, date: _5!, media: _6, entities: _7, ttlPeriod: _8)) } @@ -667,11 +667,11 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = parseString(reader) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil if _c1 && _c2 { return Api.UrlAuthResult.urlAuthResultAccepted(Cons_urlAuthResultAccepted(flags: _1!, url: _2)) } @@ -692,45 +692,45 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = parseString(reader) } var _5: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = parseString(reader) } var _6: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _6 = parseString(reader) } var _7: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _7 = parseString(reader) } var _8: [String]? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let _ = reader.readInt32() { _8 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) } } var _9: Int64? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _9 = reader.readInt64() } var _10: String? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _10 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 4) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 7) == 0) || _10 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _10 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { return Api.UrlAuthResult.urlAuthResultRequest(Cons_urlAuthResultRequest(flags: _1!, bot: _2!, domain: _3!, browser: _4, platform: _5, ip: _6, region: _7, matchCodes: _8, userIdHint: _9, verifiedAppName: _10)) } @@ -906,119 +906,119 @@ public extension Api { var _3: Int64? _3 = reader.readInt64() var _4: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = reader.readInt64() } var _5: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = parseString(reader) } var _6: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _6 = parseString(reader) } var _7: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _7 = parseString(reader) } var _8: String? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _8 = parseString(reader) } var _9: Api.UserProfilePhoto? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.UserProfilePhoto } } var _10: Api.UserStatus? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.UserStatus } } var _11: Int32? - if Int(_1!) & Int(1 << 14) != 0 { + if Int(_1 ?? 0) & Int(1 << 14) != 0 { _11 = reader.readInt32() } var _12: [Api.RestrictionReason]? - if Int(_1!) & Int(1 << 18) != 0 { + if Int(_1 ?? 0) & Int(1 << 18) != 0 { if let _ = reader.readInt32() { _12 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RestrictionReason.self) } } var _13: String? - if Int(_1!) & Int(1 << 19) != 0 { + if Int(_1 ?? 0) & Int(1 << 19) != 0 { _13 = parseString(reader) } var _14: String? - if Int(_1!) & Int(1 << 22) != 0 { + if Int(_1 ?? 0) & Int(1 << 22) != 0 { _14 = parseString(reader) } var _15: Api.EmojiStatus? - if Int(_1!) & Int(1 << 30) != 0 { + if Int(_1 ?? 0) & Int(1 << 30) != 0 { if let signature = reader.readInt32() { _15 = Api.parse(reader, signature: signature) as? Api.EmojiStatus } } var _16: [Api.Username]? - if Int(_2!) & Int(1 << 0) != 0 { + if Int(_2 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _16 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Username.self) } } var _17: Api.RecentStory? - if Int(_2!) & Int(1 << 5) != 0 { + if Int(_2 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _17 = Api.parse(reader, signature: signature) as? Api.RecentStory } } var _18: Api.PeerColor? - if Int(_2!) & Int(1 << 8) != 0 { + if Int(_2 ?? 0) & Int(1 << 8) != 0 { if let signature = reader.readInt32() { _18 = Api.parse(reader, signature: signature) as? Api.PeerColor } } var _19: Api.PeerColor? - if Int(_2!) & Int(1 << 9) != 0 { + if Int(_2 ?? 0) & Int(1 << 9) != 0 { if let signature = reader.readInt32() { _19 = Api.parse(reader, signature: signature) as? Api.PeerColor } } var _20: Int32? - if Int(_2!) & Int(1 << 12) != 0 { + if Int(_2 ?? 0) & Int(1 << 12) != 0 { _20 = reader.readInt32() } var _21: Int64? - if Int(_2!) & Int(1 << 14) != 0 { + if Int(_2 ?? 0) & Int(1 << 14) != 0 { _21 = reader.readInt64() } var _22: Int64? - if Int(_2!) & Int(1 << 15) != 0 { + if Int(_2 ?? 0) & Int(1 << 15) != 0 { _22 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 3) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 5) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 6) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 14) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 18) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 19) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 22) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 30) == 0) || _15 != nil - let _c16 = (Int(_2!) & Int(1 << 0) == 0) || _16 != nil - let _c17 = (Int(_2!) & Int(1 << 5) == 0) || _17 != nil - let _c18 = (Int(_2!) & Int(1 << 8) == 0) || _18 != nil - let _c19 = (Int(_2!) & Int(1 << 9) == 0) || _19 != nil - let _c20 = (Int(_2!) & Int(1 << 12) == 0) || _20 != nil - let _c21 = (Int(_2!) & Int(1 << 14) == 0) || _21 != nil - let _c22 = (Int(_2!) & Int(1 << 15) == 0) || _22 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 14) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 18) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 19) == 0) || _13 != nil + let _c14 = (Int(_1 ?? 0) & Int(1 << 22) == 0) || _14 != nil + let _c15 = (Int(_1 ?? 0) & Int(1 << 30) == 0) || _15 != nil + let _c16 = (Int(_2 ?? 0) & Int(1 << 0) == 0) || _16 != nil + let _c17 = (Int(_2 ?? 0) & Int(1 << 5) == 0) || _17 != nil + let _c18 = (Int(_2 ?? 0) & Int(1 << 8) == 0) || _18 != nil + let _c19 = (Int(_2 ?? 0) & Int(1 << 9) == 0) || _19 != nil + let _c20 = (Int(_2 ?? 0) & Int(1 << 12) == 0) || _20 != nil + let _c21 = (Int(_2 ?? 0) & Int(1 << 14) == 0) || _21 != nil + let _c22 = (Int(_2 ?? 0) & Int(1 << 15) == 0) || _22 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 { return Api.User.user(Cons_user(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, firstName: _5, lastName: _6, username: _7, phone: _8, photo: _9, status: _10, botInfoVersion: _11, restrictionReason: _12, botInlinePlaceholder: _13, langCode: _14, emojiStatus: _15, usernames: _16, storiesMaxId: _17, color: _18, profileColor: _19, botActiveUsers: _20, botVerificationIcon: _21, sendPaidMessagesStars: _22)) } @@ -1263,7 +1263,7 @@ public extension Api { var _3: Int64? _3 = reader.readInt64() var _4: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = parseString(reader) } var _5: Api.PeerSettings? @@ -1271,19 +1271,19 @@ public extension Api { _5 = Api.parse(reader, signature: signature) as? Api.PeerSettings } var _6: Api.Photo? - if Int(_1!) & Int(1 << 21) != 0 { + if Int(_1 ?? 0) & Int(1 << 21) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Photo } } var _7: Api.Photo? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.Photo } } var _8: Api.Photo? - if Int(_1!) & Int(1 << 22) != 0 { + if Int(_1 ?? 0) & Int(1 << 22) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.Photo } @@ -1293,207 +1293,207 @@ public extension Api { _9 = Api.parse(reader, signature: signature) as? Api.PeerNotifySettings } var _10: Api.BotInfo? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.BotInfo } } var _11: Int32? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _11 = reader.readInt32() } var _12: Int32? _12 = reader.readInt32() var _13: Int32? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _13 = reader.readInt32() } var _14: Int32? - if Int(_1!) & Int(1 << 14) != 0 { + if Int(_1 ?? 0) & Int(1 << 14) != 0 { _14 = reader.readInt32() } var _15: Api.ChatTheme? - if Int(_1!) & Int(1 << 15) != 0 { + if Int(_1 ?? 0) & Int(1 << 15) != 0 { if let signature = reader.readInt32() { _15 = Api.parse(reader, signature: signature) as? Api.ChatTheme } } var _16: String? - if Int(_1!) & Int(1 << 16) != 0 { + if Int(_1 ?? 0) & Int(1 << 16) != 0 { _16 = parseString(reader) } var _17: Api.ChatAdminRights? - if Int(_1!) & Int(1 << 17) != 0 { + if Int(_1 ?? 0) & Int(1 << 17) != 0 { if let signature = reader.readInt32() { _17 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } } var _18: Api.ChatAdminRights? - if Int(_1!) & Int(1 << 18) != 0 { + if Int(_1 ?? 0) & Int(1 << 18) != 0 { if let signature = reader.readInt32() { _18 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } } var _19: Api.WallPaper? - if Int(_1!) & Int(1 << 24) != 0 { + if Int(_1 ?? 0) & Int(1 << 24) != 0 { if let signature = reader.readInt32() { _19 = Api.parse(reader, signature: signature) as? Api.WallPaper } } var _20: Api.PeerStories? - if Int(_1!) & Int(1 << 25) != 0 { + if Int(_1 ?? 0) & Int(1 << 25) != 0 { if let signature = reader.readInt32() { _20 = Api.parse(reader, signature: signature) as? Api.PeerStories } } var _21: Api.BusinessWorkHours? - if Int(_2!) & Int(1 << 0) != 0 { + if Int(_2 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _21 = Api.parse(reader, signature: signature) as? Api.BusinessWorkHours } } var _22: Api.BusinessLocation? - if Int(_2!) & Int(1 << 1) != 0 { + if Int(_2 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _22 = Api.parse(reader, signature: signature) as? Api.BusinessLocation } } var _23: Api.BusinessGreetingMessage? - if Int(_2!) & Int(1 << 2) != 0 { + if Int(_2 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _23 = Api.parse(reader, signature: signature) as? Api.BusinessGreetingMessage } } var _24: Api.BusinessAwayMessage? - if Int(_2!) & Int(1 << 3) != 0 { + if Int(_2 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _24 = Api.parse(reader, signature: signature) as? Api.BusinessAwayMessage } } var _25: Api.BusinessIntro? - if Int(_2!) & Int(1 << 4) != 0 { + if Int(_2 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _25 = Api.parse(reader, signature: signature) as? Api.BusinessIntro } } var _26: Api.Birthday? - if Int(_2!) & Int(1 << 5) != 0 { + if Int(_2 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _26 = Api.parse(reader, signature: signature) as? Api.Birthday } } var _27: Int64? - if Int(_2!) & Int(1 << 6) != 0 { + if Int(_2 ?? 0) & Int(1 << 6) != 0 { _27 = reader.readInt64() } var _28: Int32? - if Int(_2!) & Int(1 << 6) != 0 { + if Int(_2 ?? 0) & Int(1 << 6) != 0 { _28 = reader.readInt32() } var _29: Int32? - if Int(_2!) & Int(1 << 8) != 0 { + if Int(_2 ?? 0) & Int(1 << 8) != 0 { _29 = reader.readInt32() } var _30: Api.StarRefProgram? - if Int(_2!) & Int(1 << 11) != 0 { + if Int(_2 ?? 0) & Int(1 << 11) != 0 { if let signature = reader.readInt32() { _30 = Api.parse(reader, signature: signature) as? Api.StarRefProgram } } var _31: Api.BotVerification? - if Int(_2!) & Int(1 << 12) != 0 { + if Int(_2 ?? 0) & Int(1 << 12) != 0 { if let signature = reader.readInt32() { _31 = Api.parse(reader, signature: signature) as? Api.BotVerification } } var _32: Int64? - if Int(_2!) & Int(1 << 14) != 0 { + if Int(_2 ?? 0) & Int(1 << 14) != 0 { _32 = reader.readInt64() } var _33: Api.DisallowedGiftsSettings? - if Int(_2!) & Int(1 << 15) != 0 { + if Int(_2 ?? 0) & Int(1 << 15) != 0 { if let signature = reader.readInt32() { _33 = Api.parse(reader, signature: signature) as? Api.DisallowedGiftsSettings } } var _34: Api.StarsRating? - if Int(_2!) & Int(1 << 17) != 0 { + if Int(_2 ?? 0) & Int(1 << 17) != 0 { if let signature = reader.readInt32() { _34 = Api.parse(reader, signature: signature) as? Api.StarsRating } } var _35: Api.StarsRating? - if Int(_2!) & Int(1 << 18) != 0 { + if Int(_2 ?? 0) & Int(1 << 18) != 0 { if let signature = reader.readInt32() { _35 = Api.parse(reader, signature: signature) as? Api.StarsRating } } var _36: Int32? - if Int(_2!) & Int(1 << 18) != 0 { + if Int(_2 ?? 0) & Int(1 << 18) != 0 { _36 = reader.readInt32() } var _37: Api.ProfileTab? - if Int(_2!) & Int(1 << 20) != 0 { + if Int(_2 ?? 0) & Int(1 << 20) != 0 { if let signature = reader.readInt32() { _37 = Api.parse(reader, signature: signature) as? Api.ProfileTab } } var _38: Api.Document? - if Int(_2!) & Int(1 << 21) != 0 { + if Int(_2 ?? 0) & Int(1 << 21) != 0 { if let signature = reader.readInt32() { _38 = Api.parse(reader, signature: signature) as? Api.Document } } var _39: Api.TextWithEntities? - if Int(_2!) & Int(1 << 22) != 0 { + if Int(_2 ?? 0) & Int(1 << 22) != 0 { if let signature = reader.readInt32() { _39 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } } var _40: Int64? - if Int(_2!) & Int(1 << 25) != 0 { + if Int(_2 ?? 0) & Int(1 << 25) != 0 { _40 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 21) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 22) == 0) || _8 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 21) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 22) == 0) || _8 != nil let _c9 = _9 != nil - let _c10 = (Int(_1!) & Int(1 << 3) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 6) == 0) || _11 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _11 != nil let _c12 = _12 != nil - let _c13 = (Int(_1!) & Int(1 << 11) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 14) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 15) == 0) || _15 != nil - let _c16 = (Int(_1!) & Int(1 << 16) == 0) || _16 != nil - let _c17 = (Int(_1!) & Int(1 << 17) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 18) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 24) == 0) || _19 != nil - let _c20 = (Int(_1!) & Int(1 << 25) == 0) || _20 != nil - let _c21 = (Int(_2!) & Int(1 << 0) == 0) || _21 != nil - let _c22 = (Int(_2!) & Int(1 << 1) == 0) || _22 != nil - let _c23 = (Int(_2!) & Int(1 << 2) == 0) || _23 != nil - let _c24 = (Int(_2!) & Int(1 << 3) == 0) || _24 != nil - let _c25 = (Int(_2!) & Int(1 << 4) == 0) || _25 != nil - let _c26 = (Int(_2!) & Int(1 << 5) == 0) || _26 != nil - let _c27 = (Int(_2!) & Int(1 << 6) == 0) || _27 != nil - let _c28 = (Int(_2!) & Int(1 << 6) == 0) || _28 != nil - let _c29 = (Int(_2!) & Int(1 << 8) == 0) || _29 != nil - let _c30 = (Int(_2!) & Int(1 << 11) == 0) || _30 != nil - let _c31 = (Int(_2!) & Int(1 << 12) == 0) || _31 != nil - let _c32 = (Int(_2!) & Int(1 << 14) == 0) || _32 != nil - let _c33 = (Int(_2!) & Int(1 << 15) == 0) || _33 != nil - let _c34 = (Int(_2!) & Int(1 << 17) == 0) || _34 != nil - let _c35 = (Int(_2!) & Int(1 << 18) == 0) || _35 != nil - let _c36 = (Int(_2!) & Int(1 << 18) == 0) || _36 != nil - let _c37 = (Int(_2!) & Int(1 << 20) == 0) || _37 != nil - let _c38 = (Int(_2!) & Int(1 << 21) == 0) || _38 != nil - let _c39 = (Int(_2!) & Int(1 << 22) == 0) || _39 != nil - let _c40 = (Int(_2!) & Int(1 << 25) == 0) || _40 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _13 != nil + let _c14 = (Int(_1 ?? 0) & Int(1 << 14) == 0) || _14 != nil + let _c15 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _15 != nil + let _c16 = (Int(_1 ?? 0) & Int(1 << 16) == 0) || _16 != nil + let _c17 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _17 != nil + let _c18 = (Int(_1 ?? 0) & Int(1 << 18) == 0) || _18 != nil + let _c19 = (Int(_1 ?? 0) & Int(1 << 24) == 0) || _19 != nil + let _c20 = (Int(_1 ?? 0) & Int(1 << 25) == 0) || _20 != nil + let _c21 = (Int(_2 ?? 0) & Int(1 << 0) == 0) || _21 != nil + let _c22 = (Int(_2 ?? 0) & Int(1 << 1) == 0) || _22 != nil + let _c23 = (Int(_2 ?? 0) & Int(1 << 2) == 0) || _23 != nil + let _c24 = (Int(_2 ?? 0) & Int(1 << 3) == 0) || _24 != nil + let _c25 = (Int(_2 ?? 0) & Int(1 << 4) == 0) || _25 != nil + let _c26 = (Int(_2 ?? 0) & Int(1 << 5) == 0) || _26 != nil + let _c27 = (Int(_2 ?? 0) & Int(1 << 6) == 0) || _27 != nil + let _c28 = (Int(_2 ?? 0) & Int(1 << 6) == 0) || _28 != nil + let _c29 = (Int(_2 ?? 0) & Int(1 << 8) == 0) || _29 != nil + let _c30 = (Int(_2 ?? 0) & Int(1 << 11) == 0) || _30 != nil + let _c31 = (Int(_2 ?? 0) & Int(1 << 12) == 0) || _31 != nil + let _c32 = (Int(_2 ?? 0) & Int(1 << 14) == 0) || _32 != nil + let _c33 = (Int(_2 ?? 0) & Int(1 << 15) == 0) || _33 != nil + let _c34 = (Int(_2 ?? 0) & Int(1 << 17) == 0) || _34 != nil + let _c35 = (Int(_2 ?? 0) & Int(1 << 18) == 0) || _35 != nil + let _c36 = (Int(_2 ?? 0) & Int(1 << 18) == 0) || _36 != nil + let _c37 = (Int(_2 ?? 0) & Int(1 << 20) == 0) || _37 != nil + let _c38 = (Int(_2 ?? 0) & Int(1 << 21) == 0) || _38 != nil + let _c39 = (Int(_2 ?? 0) & Int(1 << 22) == 0) || _39 != nil + let _c40 = (Int(_2 ?? 0) & Int(1 << 25) == 0) || _40 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 && _c30 && _c31 && _c32 && _c33 && _c34 && _c35 && _c36 && _c37 && _c38 && _c39 && _c40 { return Api.UserFull.userFull(Cons_userFull(flags: _1!, flags2: _2!, id: _3!, about: _4, settings: _5!, personalPhoto: _6, profilePhoto: _7, fallbackPhoto: _8, notifySettings: _9!, botInfo: _10, pinnedMsgId: _11, commonChatsCount: _12!, folderId: _13, ttlPeriod: _14, theme: _15, privateForwardName: _16, botGroupAdminRights: _17, botBroadcastAdminRights: _18, wallpaper: _19, stories: _20, businessWorkHours: _21, businessLocation: _22, businessGreetingMessage: _23, businessAwayMessage: _24, businessIntro: _25, birthday: _26, personalChannelId: _27, personalChannelMessage: _28, stargiftsCount: _29, starrefProgram: _30, botVerification: _31, sendPaidMessagesStars: _32, disallowedGifts: _33, starsRating: _34, starsMyPendingRating: _35, starsMyPendingRatingDate: _36, mainTab: _37, savedMusic: _38, note: _39, botManagerId: _40)) } @@ -1559,14 +1559,14 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Buffer? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = parseBytes(reader) } var _4: Int32? _4 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.UserProfilePhoto.userProfilePhoto(Cons_userProfilePhoto(flags: _1!, photoId: _2!, strippedThumb: _3, dcId: _4!)) @@ -1914,7 +1914,7 @@ public extension Api { var _5: Int32? _5 = reader.readInt32() var _6: Double? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = reader.readDouble() } let _c1 = _1 != nil @@ -1922,7 +1922,7 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.VideoSize.videoSize(Cons_videoSize(flags: _1!, type: _2!, w: _3!, h: _4!, size: _5!, videoStartTs: _6)) } @@ -2057,7 +2057,7 @@ public extension Api { _5 = Api.parse(reader, signature: signature) as? Api.Document } var _6: Api.WallPaperSettings? - if Int(_2!) & Int(1 << 2) != 0 { + if Int(_2 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.WallPaperSettings } @@ -2067,7 +2067,7 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_2!) & Int(1 << 2) == 0) || _6 != nil + let _c6 = (Int(_2 ?? 0) & Int(1 << 2) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.WallPaper.wallPaper(Cons_wallPaper(id: _1!, flags: _2!, accessHash: _3!, slug: _4!, document: _5!, settings: _6)) } @@ -2081,14 +2081,14 @@ public extension Api { var _2: Int32? _2 = reader.readInt32() var _3: Api.WallPaperSettings? - if Int(_2!) & Int(1 << 2) != 0 { + if Int(_2 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.WallPaperSettings } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_2!) & Int(1 << 2) == 0) || _3 != nil + let _c3 = (Int(_2 ?? 0) & Int(1 << 2) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.WallPaper.wallPaperNoFile(Cons_wallPaperNoFile(id: _1!, flags: _2!, settings: _3)) } @@ -2168,41 +2168,41 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } var _3: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _3 = reader.readInt32() } var _4: Int32? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _4 = reader.readInt32() } var _5: Int32? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _5 = reader.readInt32() } var _6: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _6 = reader.readInt32() } var _7: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _7 = reader.readInt32() } var _8: String? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _8 = parseString(reader) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 4) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 5) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 6) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 7) == 0) || _8 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.WallPaperSettings.wallPaperSettings(Cons_wallPaperSettings(flags: _1!, backgroundColor: _2, secondBackgroundColor: _3, thirdBackgroundColor: _4, fourthBackgroundColor: _5, intensity: _6, rotation: _7, emoticon: _8)) } @@ -2638,65 +2638,65 @@ public extension Api { var _5: Int32? _5 = reader.readInt32() var _6: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = parseString(reader) } var _7: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _7 = parseString(reader) } var _8: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _8 = parseString(reader) } var _9: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _9 = parseString(reader) } var _10: Api.Photo? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.Photo } } var _11: String? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _11 = parseString(reader) } var _12: String? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _12 = parseString(reader) } var _13: Int32? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _13 = reader.readInt32() } var _14: Int32? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _14 = reader.readInt32() } var _15: Int32? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _15 = reader.readInt32() } var _16: String? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _16 = parseString(reader) } var _17: Api.Document? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { if let signature = reader.readInt32() { _17 = Api.parse(reader, signature: signature) as? Api.Document } } var _18: Api.Page? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _18 = Api.parse(reader, signature: signature) as? Api.Page } } var _19: [Api.WebPageAttribute]? - if Int(_1!) & Int(1 << 12) != 0 { + if Int(_1 ?? 0) & Int(1 << 12) != 0 { if let _ = reader.readInt32() { _19 = Api.parseVector(reader, elementSignature: 0, elementType: Api.WebPageAttribute.self) } @@ -2706,20 +2706,20 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 3) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 4) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 5) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 5) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 6) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 6) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 7) == 0) || _15 != nil - let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil - let _c17 = (Int(_1!) & Int(1 << 9) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 10) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 12) == 0) || _19 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _13 != nil + let _c14 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _14 != nil + let _c15 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _15 != nil + let _c16 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _16 != nil + let _c17 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _17 != nil + let _c18 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _18 != nil + let _c19 = (Int(_1 ?? 0) & Int(1 << 12) == 0) || _19 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 { return Api.WebPage.webPage(Cons_webPage(flags: _1!, id: _2!, url: _3!, displayUrl: _4!, hash: _5!, type: _6, siteName: _7, title: _8, description: _9, photo: _10, embedUrl: _11, embedType: _12, embedWidth: _13, embedHeight: _14, duration: _15, author: _16, document: _17, cachedPage: _18, attributes: _19)) } @@ -2733,12 +2733,12 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.WebPage.webPageEmpty(Cons_webPageEmpty(flags: _1!, id: _2!, url: _3)) } @@ -2750,11 +2750,11 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil if _c1 && _c2 { return Api.WebPage.webPageNotModified(Cons_webPageNotModified(flags: _1!, cachedPageViews: _2)) } @@ -2768,14 +2768,14 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } var _4: Int32? _4 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.WebPage.webPagePending(Cons_webPagePending(flags: _1!, id: _2!, url: _3, date: _4!)) diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index 36621d885d..e61edad873 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -1,3 +1,211 @@ +public extension Api { + enum BusinessBotRecipients: TypeConstructorDescription { + public class Cons_businessBotRecipients: TypeConstructorDescription { + public var flags: Int32 + public var users: [Int64]? + public var excludeUsers: [Int64]? + public init(flags: Int32, users: [Int64]?, excludeUsers: [Int64]?) { + self.flags = flags + self.users = users + self.excludeUsers = excludeUsers + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("businessBotRecipients", [("flags", ConstructorParameterDescription(self.flags)), ("users", ConstructorParameterDescription(self.users)), ("excludeUsers", ConstructorParameterDescription(self.excludeUsers))]) + } + } + case businessBotRecipients(Cons_businessBotRecipients) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessBotRecipients(let _data): + if boxed { + buffer.appendInt32(-1198722189) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 4) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users!.count)) + for item in _data.users! { + serializeInt64(item, buffer: buffer, boxed: false) + } + } + if Int(_data.flags) & Int(1 << 6) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.excludeUsers!.count)) + for item in _data.excludeUsers! { + serializeInt64(item, buffer: buffer, boxed: false) + } + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .businessBotRecipients(let _data): + return ("businessBotRecipients", [("flags", ConstructorParameterDescription(_data.flags)), ("users", ConstructorParameterDescription(_data.users)), ("excludeUsers", ConstructorParameterDescription(_data.excludeUsers))]) + } + } + + public static func parse_businessBotRecipients(_ reader: BufferReader) -> BusinessBotRecipients? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Int64]? + if Int(_1 ?? 0) & Int(1 << 4) != 0 { + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + } + var _3: [Int64]? + if Int(_1 ?? 0) & Int(1 << 6) != 0 { + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + } + let _c1 = _1 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.BusinessBotRecipients.businessBotRecipients(Cons_businessBotRecipients(flags: _1!, users: _2, excludeUsers: _3)) + } + else { + return nil + } + } + } +} +public extension Api { + enum BusinessBotRights: TypeConstructorDescription { + public class Cons_businessBotRights: TypeConstructorDescription { + public var flags: Int32 + public init(flags: Int32) { + self.flags = flags + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("businessBotRights", [("flags", ConstructorParameterDescription(self.flags))]) + } + } + case businessBotRights(Cons_businessBotRights) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessBotRights(let _data): + if boxed { + buffer.appendInt32(-1604170505) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .businessBotRights(let _data): + return ("businessBotRights", [("flags", ConstructorParameterDescription(_data.flags))]) + } + } + + public static func parse_businessBotRights(_ reader: BufferReader) -> BusinessBotRights? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.BusinessBotRights.businessBotRights(Cons_businessBotRights(flags: _1!)) + } + else { + return nil + } + } + } +} +public extension Api { + enum BusinessChatLink: TypeConstructorDescription { + public class Cons_businessChatLink: TypeConstructorDescription { + public var flags: Int32 + public var link: String + public var message: String + public var entities: [Api.MessageEntity]? + public var title: String? + public var views: Int32 + public init(flags: Int32, link: String, message: String, entities: [Api.MessageEntity]?, title: String?, views: Int32) { + self.flags = flags + self.link = link + self.message = message + self.entities = entities + self.title = title + self.views = views + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("businessChatLink", [("flags", ConstructorParameterDescription(self.flags)), ("link", ConstructorParameterDescription(self.link)), ("message", ConstructorParameterDescription(self.message)), ("entities", ConstructorParameterDescription(self.entities)), ("title", ConstructorParameterDescription(self.title)), ("views", ConstructorParameterDescription(self.views))]) + } + } + case businessChatLink(Cons_businessChatLink) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .businessChatLink(let _data): + if boxed { + buffer.appendInt32(-1263638929) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeString(_data.link, buffer: buffer, boxed: false) + serializeString(_data.message, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 0) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.entities!.count)) + for item in _data.entities! { + item.serialize(buffer, true) + } + } + if Int(_data.flags) & Int(1 << 1) != 0 { + serializeString(_data.title!, buffer: buffer, boxed: false) + } + serializeInt32(_data.views, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .businessChatLink(let _data): + return ("businessChatLink", [("flags", ConstructorParameterDescription(_data.flags)), ("link", ConstructorParameterDescription(_data.link)), ("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("title", ConstructorParameterDescription(_data.title)), ("views", ConstructorParameterDescription(_data.views))]) + } + } + + public static func parse_businessChatLink(_ reader: BufferReader) -> BusinessChatLink? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: [Api.MessageEntity]? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } + } + var _5: String? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _5 = parseString(reader) + } + var _6: Int32? + _6 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.BusinessChatLink.businessChatLink(Cons_businessChatLink(flags: _1!, link: _2!, message: _3!, entities: _4, title: _5, views: _6!)) + } + else { + return nil + } + } + } +} public extension Api { enum BusinessGreetingMessage: TypeConstructorDescription { public class Cons_businessGreetingMessage: TypeConstructorDescription { @@ -106,7 +314,7 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Api.Document? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Document } @@ -114,7 +322,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.BusinessIntro.businessIntro(Cons_businessIntro(flags: _1!, title: _2!, description: _3!, sticker: _4)) } @@ -167,7 +375,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.GeoPoint? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.GeoPoint } @@ -175,7 +383,7 @@ public extension Api { var _3: String? _3 = parseString(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { return Api.BusinessLocation.businessLocation(Cons_businessLocation(flags: _1!, geoPoint: _2, address: _3!)) @@ -230,13 +438,13 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: [Int64]? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let _ = reader.readInt32() { _2 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _2 != nil if _c1 && _c2 { return Api.BusinessRecipients.businessRecipients(Cons_businessRecipients(flags: _1!, users: _2)) } @@ -2115,20 +2323,20 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.ForumTopic? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.ForumTopic } } var _3: Api.ForumTopic? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.ForumTopic } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.ChannelAdminLogEventAction.channelAdminLogEventActionPinTopic(Cons_channelAdminLogEventActionPinTopic(flags: _1!, prevTopic: _2, newTopic: _3)) } diff --git a/submodules/TelegramApi/Sources/Api30.swift b/submodules/TelegramApi/Sources/Api30.swift index 321e789202..4e8f33efba 100644 --- a/submodules/TelegramApi/Sources/Api30.swift +++ b/submodules/TelegramApi/Sources/Api30.swift @@ -1,5 +1,14 @@ public extension Api { indirect enum WebPageAttribute: TypeConstructorDescription { + public class Cons_webPageAttributeAiComposeTone: TypeConstructorDescription { + public var emojiId: Int64 + public init(emojiId: Int64) { + self.emojiId = emojiId + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("webPageAttributeAiComposeTone", [("emojiId", ConstructorParameterDescription(self.emojiId))]) + } + } public class Cons_webPageAttributeStarGiftAuction: TypeConstructorDescription { public var gift: Api.StarGift public var endDate: Int32 @@ -68,6 +77,7 @@ public extension Api { return ("webPageAttributeUniqueStarGift", [("gift", ConstructorParameterDescription(self.gift))]) } } + case webPageAttributeAiComposeTone(Cons_webPageAttributeAiComposeTone) case webPageAttributeStarGiftAuction(Cons_webPageAttributeStarGiftAuction) case webPageAttributeStarGiftCollection(Cons_webPageAttributeStarGiftCollection) case webPageAttributeStickerSet(Cons_webPageAttributeStickerSet) @@ -77,6 +87,12 @@ public extension Api { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .webPageAttributeAiComposeTone(let _data): + if boxed { + buffer.appendInt32(2005007896) + } + serializeInt64(_data.emojiId, buffer: buffer, boxed: false) + break case .webPageAttributeStarGiftAuction(let _data): if boxed { buffer.appendInt32(29770178) @@ -143,6 +159,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { switch self { + case .webPageAttributeAiComposeTone(let _data): + return ("webPageAttributeAiComposeTone", [("emojiId", ConstructorParameterDescription(_data.emojiId))]) case .webPageAttributeStarGiftAuction(let _data): return ("webPageAttributeStarGiftAuction", [("gift", ConstructorParameterDescription(_data.gift)), ("endDate", ConstructorParameterDescription(_data.endDate))]) case .webPageAttributeStarGiftCollection(let _data): @@ -158,6 +176,17 @@ public extension Api { } } + public static func parse_webPageAttributeAiComposeTone(_ reader: BufferReader) -> WebPageAttribute? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.WebPageAttribute.webPageAttributeAiComposeTone(Cons_webPageAttributeAiComposeTone(emojiId: _1!)) + } + else { + return nil + } + } public static func parse_webPageAttributeStarGiftAuction(_ reader: BufferReader) -> WebPageAttribute? { var _1: Api.StarGift? if let signature = reader.readInt32() { @@ -213,7 +242,7 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Api.StoryItem? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.StoryItem } @@ -221,7 +250,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.WebPageAttribute.webPageAttributeStory(Cons_webPageAttributeStory(flags: _1!, peer: _2!, id: _3!, story: _4)) } @@ -233,20 +262,20 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: [Api.Document]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) } } var _3: Api.ThemeSettings? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.ThemeSettings } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.WebPageAttribute.webPageAttributeTheme(Cons_webPageAttributeTheme(flags: _1!, documents: _2, settings: _3)) } @@ -309,13 +338,13 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.InputBotInlineMessageID? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.InputBotInlineMessageID } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil if _c1 && _c2 { return Api.WebViewMessageSent.webViewMessageSent(Cons_webViewMessageSent(flags: _1!, msgId: _2)) } @@ -368,13 +397,13 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt64() } var _3: String? _3 = parseString(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { return Api.WebViewResult.webViewResultUrl(Cons_webViewResultUrl(flags: _1!, queryId: _2, url: _3!)) @@ -469,7 +498,7 @@ public extension Api.account { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } var _6: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = parseString(reader) } let _c1 = _1 != nil @@ -477,7 +506,7 @@ public extension Api.account { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.account.AuthorizationForm.authorizationForm(Cons_authorizationForm(flags: _1!, requiredTypes: _2!, values: _3!, errors: _4!, users: _5!, privacyPolicyUrl: _6)) } @@ -863,7 +892,7 @@ public extension Api.account { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } var _6: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = parseString(reader) } let _c1 = _1 != nil @@ -871,7 +900,7 @@ public extension Api.account { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.account.ChatThemes.chatThemes(Cons_chatThemes(flags: _1!, hash: _2!, themes: _3!, chats: _4!, users: _5!, nextOffset: _6)) } @@ -1357,25 +1386,25 @@ public extension Api.account { var _1: Int32? _1 = reader.readInt32() var _2: Api.PasswordKdfAlgo? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.PasswordKdfAlgo } } var _3: Buffer? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _3 = parseBytes(reader) } var _4: Int64? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = reader.readInt64() } var _5: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _5 = parseString(reader) } var _6: String? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _6 = parseString(reader) } var _7: Api.PasswordKdfAlgo? @@ -1389,24 +1418,24 @@ public extension Api.account { var _9: Buffer? _9 = parseBytes(reader) var _10: Int32? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _10 = reader.readInt32() } var _11: String? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _11 = parseString(reader) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil let _c9 = _9 != nil - let _c10 = (Int(_1!) & Int(1 << 5) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 6) == 0) || _11 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _11 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { return Api.account.Password.password(Cons_password(flags: _1!, currentAlgo: _2, srpB: _3, srpId: _4, hint: _5, emailUnconfirmedPattern: _6, newAlgo: _7!, newSecureAlgo: _8!, secureRandom: _9!, pendingResetDate: _10, loginEmailPattern: _11)) } @@ -1476,35 +1505,35 @@ public extension Api.account { var _1: Int32? _1 = reader.readInt32() var _2: Api.PasswordKdfAlgo? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.PasswordKdfAlgo } } var _3: Buffer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseBytes(reader) } var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } var _5: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = parseString(reader) } var _6: Api.SecureSecretSettings? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.SecureSecretSettings } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.account.PasswordInputSettings.passwordInputSettings(Cons_passwordInputSettings(flags: _1!, newAlgo: _2, newPasswordHash: _3, hint: _4, email: _5, newSecureSettings: _6)) } @@ -1559,18 +1588,18 @@ public extension Api.account { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = parseString(reader) } var _3: Api.SecureSecretSettings? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.SecureSecretSettings } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.account.PasswordSettings.passwordSettings(Cons_passwordSettings(flags: _1!, email: _2, secureSettings: _3)) } @@ -1738,101 +1767,3 @@ public extension Api.account { } } } -public extension Api.account { - enum ResolvedBusinessChatLinks: TypeConstructorDescription { - public class Cons_resolvedBusinessChatLinks: TypeConstructorDescription { - public var flags: Int32 - public var peer: Api.Peer - public var message: String - public var entities: [Api.MessageEntity]? - public var chats: [Api.Chat] - public var users: [Api.User] - public init(flags: Int32, peer: Api.Peer, message: String, entities: [Api.MessageEntity]?, chats: [Api.Chat], users: [Api.User]) { - self.flags = flags - self.peer = peer - self.message = message - self.entities = entities - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("resolvedBusinessChatLinks", [("flags", ConstructorParameterDescription(self.flags)), ("peer", ConstructorParameterDescription(self.peer)), ("message", ConstructorParameterDescription(self.message)), ("entities", ConstructorParameterDescription(self.entities)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case resolvedBusinessChatLinks(Cons_resolvedBusinessChatLinks) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .resolvedBusinessChatLinks(let _data): - if boxed { - buffer.appendInt32(-1708937439) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - _data.peer.serialize(buffer, true) - serializeString(_data.message, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 0) != 0 { - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.entities!.count)) - for item in _data.entities! { - item.serialize(buffer, true) - } - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .resolvedBusinessChatLinks(let _data): - return ("resolvedBusinessChatLinks", [("flags", ConstructorParameterDescription(_data.flags)), ("peer", ConstructorParameterDescription(_data.peer)), ("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_resolvedBusinessChatLinks(_ reader: BufferReader) -> ResolvedBusinessChatLinks? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.Peer? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.Peer - } - var _3: String? - _3 = parseString(reader) - var _4: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 0) != 0 { - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } - } - var _5: [Api.Chat]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _6: [Api.User]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.account.ResolvedBusinessChatLinks.resolvedBusinessChatLinks(Cons_resolvedBusinessChatLinks(flags: _1!, peer: _2!, message: _3!, entities: _4, chats: _5!, users: _6!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 46d8b246c9..4e02876037 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -1,3 +1,101 @@ +public extension Api.account { + enum ResolvedBusinessChatLinks: TypeConstructorDescription { + public class Cons_resolvedBusinessChatLinks: TypeConstructorDescription { + public var flags: Int32 + public var peer: Api.Peer + public var message: String + public var entities: [Api.MessageEntity]? + public var chats: [Api.Chat] + public var users: [Api.User] + public init(flags: Int32, peer: Api.Peer, message: String, entities: [Api.MessageEntity]?, chats: [Api.Chat], users: [Api.User]) { + self.flags = flags + self.peer = peer + self.message = message + self.entities = entities + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("resolvedBusinessChatLinks", [("flags", ConstructorParameterDescription(self.flags)), ("peer", ConstructorParameterDescription(self.peer)), ("message", ConstructorParameterDescription(self.message)), ("entities", ConstructorParameterDescription(self.entities)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case resolvedBusinessChatLinks(Cons_resolvedBusinessChatLinks) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .resolvedBusinessChatLinks(let _data): + if boxed { + buffer.appendInt32(-1708937439) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + _data.peer.serialize(buffer, true) + serializeString(_data.message, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 0) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.entities!.count)) + for item in _data.entities! { + item.serialize(buffer, true) + } + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .resolvedBusinessChatLinks(let _data): + return ("resolvedBusinessChatLinks", [("flags", ConstructorParameterDescription(_data.flags)), ("peer", ConstructorParameterDescription(_data.peer)), ("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_resolvedBusinessChatLinks(_ reader: BufferReader) -> ResolvedBusinessChatLinks? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _3: String? + _3 = parseString(reader) + var _4: [Api.MessageEntity]? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } + } + var _5: [Api.Chat]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _6: [Api.User]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.account.ResolvedBusinessChatLinks.resolvedBusinessChatLinks(Cons_resolvedBusinessChatLinks(flags: _1!, peer: _2!, message: _3!, entities: _4, chats: _5!, users: _6!)) + } + else { + return nil + } + } + } +} public extension Api.account { enum SavedMusicIds: TypeConstructorDescription { public class Cons_savedMusicIds: TypeConstructorDescription { @@ -523,6 +621,85 @@ public extension Api.account { } } } +public extension Api.aicompose { + enum Tones: TypeConstructorDescription { + public class Cons_tones: TypeConstructorDescription { + public var hash: Int64 + public var tones: [Api.AiComposeTone] + public var users: [Api.User] + public init(hash: Int64, tones: [Api.AiComposeTone], users: [Api.User]) { + self.hash = hash + self.tones = tones + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("tones", [("hash", ConstructorParameterDescription(self.hash)), ("tones", ConstructorParameterDescription(self.tones)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case tones(Cons_tones) + case tonesNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .tones(let _data): + if boxed { + buffer.appendInt32(1822232318) + } + serializeInt64(_data.hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.tones.count)) + for item in _data.tones { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + case .tonesNotModified: + if boxed { + buffer.appendInt32(-1040948989) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .tones(let _data): + return ("tones", [("hash", ConstructorParameterDescription(_data.hash)), ("tones", ConstructorParameterDescription(_data.tones)), ("users", ConstructorParameterDescription(_data.users))]) + case .tonesNotModified: + return ("tonesNotModified", []) + } + } + + public static func parse_tones(_ reader: BufferReader) -> Tones? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.AiComposeTone]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.AiComposeTone.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.aicompose.Tones.tones(Cons_tones(hash: _1!, tones: _2!, users: _3!)) + } + else { + return nil + } + } + public static func parse_tonesNotModified(_ reader: BufferReader) -> Tones? { + return Api.aicompose.Tones.tonesNotModified + } + } +} public extension Api.auth { enum Authorization: TypeConstructorDescription { public class Cons_authorization: TypeConstructorDescription { @@ -599,15 +776,15 @@ public extension Api.auth { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _2 = reader.readInt32() } var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Buffer? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = parseBytes(reader) } var _5: Api.User? @@ -615,9 +792,9 @@ public extension Api.auth { _5 = Api.parse(reader, signature: signature) as? Api.User } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.auth.Authorization.authorization(Cons_authorization(flags: _1!, otherwiseReloginDays: _2, tmpSessions: _3, futureAuthToken: _4, user: _5!)) @@ -630,13 +807,13 @@ public extension Api.auth { var _1: Int32? _1 = reader.readInt32() var _2: Api.help.TermsOfService? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.help.TermsOfService } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil if _c1 && _c2 { return Api.auth.Authorization.authorizationSignUpRequired(Cons_authorizationSignUpRequired(flags: _1!, termsOfService: _2)) } @@ -806,11 +983,11 @@ public extension Api.auth { var _1: Int32? _1 = reader.readInt32() var _2: Buffer? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = parseBytes(reader) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil if _c1 && _c2 { return Api.auth.LoggedOut.loggedOut(Cons_loggedOut(flags: _1!, futureAuthToken: _2)) } @@ -1050,18 +1227,20 @@ public extension Api.auth { public var phoneCodeHash: String public var supportEmailAddress: String public var supportEmailSubject: String + public var premiumDays: Int32 public var currency: String public var amount: Int64 - public init(storeProduct: String, phoneCodeHash: String, supportEmailAddress: String, supportEmailSubject: String, currency: String, amount: Int64) { + public init(storeProduct: String, phoneCodeHash: String, supportEmailAddress: String, supportEmailSubject: String, premiumDays: Int32, currency: String, amount: Int64) { self.storeProduct = storeProduct self.phoneCodeHash = phoneCodeHash self.supportEmailAddress = supportEmailAddress self.supportEmailSubject = supportEmailSubject + self.premiumDays = premiumDays self.currency = currency self.amount = amount } public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("sentCodePaymentRequired", [("storeProduct", ConstructorParameterDescription(self.storeProduct)), ("phoneCodeHash", ConstructorParameterDescription(self.phoneCodeHash)), ("supportEmailAddress", ConstructorParameterDescription(self.supportEmailAddress)), ("supportEmailSubject", ConstructorParameterDescription(self.supportEmailSubject)), ("currency", ConstructorParameterDescription(self.currency)), ("amount", ConstructorParameterDescription(self.amount))]) + return ("sentCodePaymentRequired", [("storeProduct", ConstructorParameterDescription(self.storeProduct)), ("phoneCodeHash", ConstructorParameterDescription(self.phoneCodeHash)), ("supportEmailAddress", ConstructorParameterDescription(self.supportEmailAddress)), ("supportEmailSubject", ConstructorParameterDescription(self.supportEmailSubject)), ("premiumDays", ConstructorParameterDescription(self.premiumDays)), ("currency", ConstructorParameterDescription(self.currency)), ("amount", ConstructorParameterDescription(self.amount))]) } } public class Cons_sentCodeSuccess: TypeConstructorDescription { @@ -1095,12 +1274,13 @@ public extension Api.auth { break case .sentCodePaymentRequired(let _data): if boxed { - buffer.appendInt32(-527082948) + buffer.appendInt32(-125665601) } serializeString(_data.storeProduct, buffer: buffer, boxed: false) serializeString(_data.phoneCodeHash, buffer: buffer, boxed: false) serializeString(_data.supportEmailAddress, buffer: buffer, boxed: false) serializeString(_data.supportEmailSubject, buffer: buffer, boxed: false) + serializeInt32(_data.premiumDays, buffer: buffer, boxed: false) serializeString(_data.currency, buffer: buffer, boxed: false) serializeInt64(_data.amount, buffer: buffer, boxed: false) break @@ -1118,7 +1298,7 @@ public extension Api.auth { case .sentCode(let _data): return ("sentCode", [("flags", ConstructorParameterDescription(_data.flags)), ("type", ConstructorParameterDescription(_data.type)), ("phoneCodeHash", ConstructorParameterDescription(_data.phoneCodeHash)), ("nextType", ConstructorParameterDescription(_data.nextType)), ("timeout", ConstructorParameterDescription(_data.timeout))]) case .sentCodePaymentRequired(let _data): - return ("sentCodePaymentRequired", [("storeProduct", ConstructorParameterDescription(_data.storeProduct)), ("phoneCodeHash", ConstructorParameterDescription(_data.phoneCodeHash)), ("supportEmailAddress", ConstructorParameterDescription(_data.supportEmailAddress)), ("supportEmailSubject", ConstructorParameterDescription(_data.supportEmailSubject)), ("currency", ConstructorParameterDescription(_data.currency)), ("amount", ConstructorParameterDescription(_data.amount))]) + return ("sentCodePaymentRequired", [("storeProduct", ConstructorParameterDescription(_data.storeProduct)), ("phoneCodeHash", ConstructorParameterDescription(_data.phoneCodeHash)), ("supportEmailAddress", ConstructorParameterDescription(_data.supportEmailAddress)), ("supportEmailSubject", ConstructorParameterDescription(_data.supportEmailSubject)), ("premiumDays", ConstructorParameterDescription(_data.premiumDays)), ("currency", ConstructorParameterDescription(_data.currency)), ("amount", ConstructorParameterDescription(_data.amount))]) case .sentCodeSuccess(let _data): return ("sentCodeSuccess", [("authorization", ConstructorParameterDescription(_data.authorization))]) } @@ -1134,20 +1314,20 @@ public extension Api.auth { var _3: String? _3 = parseString(reader) var _4: Api.auth.CodeType? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.auth.CodeType } } var _5: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.auth.SentCode.sentCode(Cons_sentCode(flags: _1!, type: _2!, phoneCodeHash: _3!, nextType: _4, timeout: _5)) } @@ -1164,18 +1344,21 @@ public extension Api.auth { _3 = parseString(reader) var _4: String? _4 = parseString(reader) - var _5: String? - _5 = parseString(reader) - var _6: Int64? - _6 = reader.readInt64() + var _5: Int32? + _5 = reader.readInt32() + var _6: String? + _6 = parseString(reader) + var _7: Int64? + _7 = reader.readInt64() let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.auth.SentCode.sentCodePaymentRequired(Cons_sentCodePaymentRequired(storeProduct: _1!, phoneCodeHash: _2!, supportEmailAddress: _3!, supportEmailSubject: _4!, currency: _5!, amount: _6!)) + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.auth.SentCode.sentCodePaymentRequired(Cons_sentCodePaymentRequired(storeProduct: _1!, phoneCodeHash: _2!, supportEmailAddress: _3!, supportEmailSubject: _4!, premiumDays: _5!, currency: _6!, amount: _7!)) } else { return nil @@ -1196,457 +1379,3 @@ public extension Api.auth { } } } -public extension Api.auth { - enum SentCodeType: TypeConstructorDescription { - public class Cons_sentCodeTypeApp: TypeConstructorDescription { - public var length: Int32 - public init(length: Int32) { - self.length = length - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("sentCodeTypeApp", [("length", ConstructorParameterDescription(self.length))]) - } - } - public class Cons_sentCodeTypeCall: TypeConstructorDescription { - public var length: Int32 - public init(length: Int32) { - self.length = length - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("sentCodeTypeCall", [("length", ConstructorParameterDescription(self.length))]) - } - } - public class Cons_sentCodeTypeEmailCode: TypeConstructorDescription { - public var flags: Int32 - public var emailPattern: String - public var length: Int32 - public var resetAvailablePeriod: Int32? - public var resetPendingDate: Int32? - public init(flags: Int32, emailPattern: String, length: Int32, resetAvailablePeriod: Int32?, resetPendingDate: Int32?) { - self.flags = flags - self.emailPattern = emailPattern - self.length = length - self.resetAvailablePeriod = resetAvailablePeriod - self.resetPendingDate = resetPendingDate - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("sentCodeTypeEmailCode", [("flags", ConstructorParameterDescription(self.flags)), ("emailPattern", ConstructorParameterDescription(self.emailPattern)), ("length", ConstructorParameterDescription(self.length)), ("resetAvailablePeriod", ConstructorParameterDescription(self.resetAvailablePeriod)), ("resetPendingDate", ConstructorParameterDescription(self.resetPendingDate))]) - } - } - public class Cons_sentCodeTypeFirebaseSms: TypeConstructorDescription { - public var flags: Int32 - public var nonce: Buffer? - public var playIntegrityProjectId: Int64? - public var playIntegrityNonce: Buffer? - public var receipt: String? - public var pushTimeout: Int32? - public var length: Int32 - public init(flags: Int32, nonce: Buffer?, playIntegrityProjectId: Int64?, playIntegrityNonce: Buffer?, receipt: String?, pushTimeout: Int32?, length: Int32) { - self.flags = flags - self.nonce = nonce - self.playIntegrityProjectId = playIntegrityProjectId - self.playIntegrityNonce = playIntegrityNonce - self.receipt = receipt - self.pushTimeout = pushTimeout - self.length = length - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("sentCodeTypeFirebaseSms", [("flags", ConstructorParameterDescription(self.flags)), ("nonce", ConstructorParameterDescription(self.nonce)), ("playIntegrityProjectId", ConstructorParameterDescription(self.playIntegrityProjectId)), ("playIntegrityNonce", ConstructorParameterDescription(self.playIntegrityNonce)), ("receipt", ConstructorParameterDescription(self.receipt)), ("pushTimeout", ConstructorParameterDescription(self.pushTimeout)), ("length", ConstructorParameterDescription(self.length))]) - } - } - public class Cons_sentCodeTypeFlashCall: TypeConstructorDescription { - public var pattern: String - public init(pattern: String) { - self.pattern = pattern - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("sentCodeTypeFlashCall", [("pattern", ConstructorParameterDescription(self.pattern))]) - } - } - public class Cons_sentCodeTypeFragmentSms: TypeConstructorDescription { - public var url: String - public var length: Int32 - public init(url: String, length: Int32) { - self.url = url - self.length = length - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("sentCodeTypeFragmentSms", [("url", ConstructorParameterDescription(self.url)), ("length", ConstructorParameterDescription(self.length))]) - } - } - public class Cons_sentCodeTypeMissedCall: TypeConstructorDescription { - public var prefix: String - public var length: Int32 - public init(prefix: String, length: Int32) { - self.prefix = prefix - self.length = length - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("sentCodeTypeMissedCall", [("prefix", ConstructorParameterDescription(self.prefix)), ("length", ConstructorParameterDescription(self.length))]) - } - } - public class Cons_sentCodeTypeSetUpEmailRequired: TypeConstructorDescription { - public var flags: Int32 - public init(flags: Int32) { - self.flags = flags - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("sentCodeTypeSetUpEmailRequired", [("flags", ConstructorParameterDescription(self.flags))]) - } - } - public class Cons_sentCodeTypeSms: TypeConstructorDescription { - public var length: Int32 - public init(length: Int32) { - self.length = length - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("sentCodeTypeSms", [("length", ConstructorParameterDescription(self.length))]) - } - } - public class Cons_sentCodeTypeSmsPhrase: TypeConstructorDescription { - public var flags: Int32 - public var beginning: String? - public init(flags: Int32, beginning: String?) { - self.flags = flags - self.beginning = beginning - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("sentCodeTypeSmsPhrase", [("flags", ConstructorParameterDescription(self.flags)), ("beginning", ConstructorParameterDescription(self.beginning))]) - } - } - public class Cons_sentCodeTypeSmsWord: TypeConstructorDescription { - public var flags: Int32 - public var beginning: String? - public init(flags: Int32, beginning: String?) { - self.flags = flags - self.beginning = beginning - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("sentCodeTypeSmsWord", [("flags", ConstructorParameterDescription(self.flags)), ("beginning", ConstructorParameterDescription(self.beginning))]) - } - } - case sentCodeTypeApp(Cons_sentCodeTypeApp) - case sentCodeTypeCall(Cons_sentCodeTypeCall) - case sentCodeTypeEmailCode(Cons_sentCodeTypeEmailCode) - case sentCodeTypeFirebaseSms(Cons_sentCodeTypeFirebaseSms) - case sentCodeTypeFlashCall(Cons_sentCodeTypeFlashCall) - case sentCodeTypeFragmentSms(Cons_sentCodeTypeFragmentSms) - case sentCodeTypeMissedCall(Cons_sentCodeTypeMissedCall) - case sentCodeTypeSetUpEmailRequired(Cons_sentCodeTypeSetUpEmailRequired) - case sentCodeTypeSms(Cons_sentCodeTypeSms) - case sentCodeTypeSmsPhrase(Cons_sentCodeTypeSmsPhrase) - case sentCodeTypeSmsWord(Cons_sentCodeTypeSmsWord) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .sentCodeTypeApp(let _data): - if boxed { - buffer.appendInt32(1035688326) - } - serializeInt32(_data.length, buffer: buffer, boxed: false) - break - case .sentCodeTypeCall(let _data): - if boxed { - buffer.appendInt32(1398007207) - } - serializeInt32(_data.length, buffer: buffer, boxed: false) - break - case .sentCodeTypeEmailCode(let _data): - if boxed { - buffer.appendInt32(-196020837) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeString(_data.emailPattern, buffer: buffer, boxed: false) - serializeInt32(_data.length, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 3) != 0 { - serializeInt32(_data.resetAvailablePeriod!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 4) != 0 { - serializeInt32(_data.resetPendingDate!, buffer: buffer, boxed: false) - } - break - case .sentCodeTypeFirebaseSms(let _data): - if boxed { - buffer.appendInt32(10475318) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeBytes(_data.nonce!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 2) != 0 { - serializeInt64(_data.playIntegrityProjectId!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 2) != 0 { - serializeBytes(_data.playIntegrityNonce!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 1) != 0 { - serializeString(_data.receipt!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 1) != 0 { - serializeInt32(_data.pushTimeout!, buffer: buffer, boxed: false) - } - serializeInt32(_data.length, buffer: buffer, boxed: false) - break - case .sentCodeTypeFlashCall(let _data): - if boxed { - buffer.appendInt32(-1425815847) - } - serializeString(_data.pattern, buffer: buffer, boxed: false) - break - case .sentCodeTypeFragmentSms(let _data): - if boxed { - buffer.appendInt32(-648651719) - } - serializeString(_data.url, buffer: buffer, boxed: false) - serializeInt32(_data.length, buffer: buffer, boxed: false) - break - case .sentCodeTypeMissedCall(let _data): - if boxed { - buffer.appendInt32(-2113903484) - } - serializeString(_data.prefix, buffer: buffer, boxed: false) - serializeInt32(_data.length, buffer: buffer, boxed: false) - break - case .sentCodeTypeSetUpEmailRequired(let _data): - if boxed { - buffer.appendInt32(-1521934870) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - break - case .sentCodeTypeSms(let _data): - if boxed { - buffer.appendInt32(-1073693790) - } - serializeInt32(_data.length, buffer: buffer, boxed: false) - break - case .sentCodeTypeSmsPhrase(let _data): - if boxed { - buffer.appendInt32(-1284008785) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeString(_data.beginning!, buffer: buffer, boxed: false) - } - break - case .sentCodeTypeSmsWord(let _data): - if boxed { - buffer.appendInt32(-1542017919) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeString(_data.beginning!, buffer: buffer, boxed: false) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .sentCodeTypeApp(let _data): - return ("sentCodeTypeApp", [("length", ConstructorParameterDescription(_data.length))]) - case .sentCodeTypeCall(let _data): - return ("sentCodeTypeCall", [("length", ConstructorParameterDescription(_data.length))]) - case .sentCodeTypeEmailCode(let _data): - return ("sentCodeTypeEmailCode", [("flags", ConstructorParameterDescription(_data.flags)), ("emailPattern", ConstructorParameterDescription(_data.emailPattern)), ("length", ConstructorParameterDescription(_data.length)), ("resetAvailablePeriod", ConstructorParameterDescription(_data.resetAvailablePeriod)), ("resetPendingDate", ConstructorParameterDescription(_data.resetPendingDate))]) - case .sentCodeTypeFirebaseSms(let _data): - return ("sentCodeTypeFirebaseSms", [("flags", ConstructorParameterDescription(_data.flags)), ("nonce", ConstructorParameterDescription(_data.nonce)), ("playIntegrityProjectId", ConstructorParameterDescription(_data.playIntegrityProjectId)), ("playIntegrityNonce", ConstructorParameterDescription(_data.playIntegrityNonce)), ("receipt", ConstructorParameterDescription(_data.receipt)), ("pushTimeout", ConstructorParameterDescription(_data.pushTimeout)), ("length", ConstructorParameterDescription(_data.length))]) - case .sentCodeTypeFlashCall(let _data): - return ("sentCodeTypeFlashCall", [("pattern", ConstructorParameterDescription(_data.pattern))]) - case .sentCodeTypeFragmentSms(let _data): - return ("sentCodeTypeFragmentSms", [("url", ConstructorParameterDescription(_data.url)), ("length", ConstructorParameterDescription(_data.length))]) - case .sentCodeTypeMissedCall(let _data): - return ("sentCodeTypeMissedCall", [("prefix", ConstructorParameterDescription(_data.prefix)), ("length", ConstructorParameterDescription(_data.length))]) - case .sentCodeTypeSetUpEmailRequired(let _data): - return ("sentCodeTypeSetUpEmailRequired", [("flags", ConstructorParameterDescription(_data.flags))]) - case .sentCodeTypeSms(let _data): - return ("sentCodeTypeSms", [("length", ConstructorParameterDescription(_data.length))]) - case .sentCodeTypeSmsPhrase(let _data): - return ("sentCodeTypeSmsPhrase", [("flags", ConstructorParameterDescription(_data.flags)), ("beginning", ConstructorParameterDescription(_data.beginning))]) - case .sentCodeTypeSmsWord(let _data): - return ("sentCodeTypeSmsWord", [("flags", ConstructorParameterDescription(_data.flags)), ("beginning", ConstructorParameterDescription(_data.beginning))]) - } - } - - public static func parse_sentCodeTypeApp(_ reader: BufferReader) -> SentCodeType? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.auth.SentCodeType.sentCodeTypeApp(Cons_sentCodeTypeApp(length: _1!)) - } - else { - return nil - } - } - public static func parse_sentCodeTypeCall(_ reader: BufferReader) -> SentCodeType? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.auth.SentCodeType.sentCodeTypeCall(Cons_sentCodeTypeCall(length: _1!)) - } - else { - return nil - } - } - public static func parse_sentCodeTypeEmailCode(_ reader: BufferReader) -> SentCodeType? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - if Int(_1!) & Int(1 << 3) != 0 { - _4 = reader.readInt32() - } - var _5: Int32? - if Int(_1!) & Int(1 << 4) != 0 { - _5 = reader.readInt32() - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.auth.SentCodeType.sentCodeTypeEmailCode(Cons_sentCodeTypeEmailCode(flags: _1!, emailPattern: _2!, length: _3!, resetAvailablePeriod: _4, resetPendingDate: _5)) - } - else { - return nil - } - } - public static func parse_sentCodeTypeFirebaseSms(_ reader: BufferReader) -> SentCodeType? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Buffer? - if Int(_1!) & Int(1 << 0) != 0 { - _2 = parseBytes(reader) - } - var _3: Int64? - if Int(_1!) & Int(1 << 2) != 0 { - _3 = reader.readInt64() - } - var _4: Buffer? - if Int(_1!) & Int(1 << 2) != 0 { - _4 = parseBytes(reader) - } - var _5: String? - if Int(_1!) & Int(1 << 1) != 0 { - _5 = parseString(reader) - } - var _6: Int32? - if Int(_1!) & Int(1 << 1) != 0 { - _6 = reader.readInt32() - } - var _7: Int32? - _7 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil - let _c7 = _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.auth.SentCodeType.sentCodeTypeFirebaseSms(Cons_sentCodeTypeFirebaseSms(flags: _1!, nonce: _2, playIntegrityProjectId: _3, playIntegrityNonce: _4, receipt: _5, pushTimeout: _6, length: _7!)) - } - else { - return nil - } - } - public static func parse_sentCodeTypeFlashCall(_ reader: BufferReader) -> SentCodeType? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.auth.SentCodeType.sentCodeTypeFlashCall(Cons_sentCodeTypeFlashCall(pattern: _1!)) - } - else { - return nil - } - } - public static func parse_sentCodeTypeFragmentSms(_ reader: BufferReader) -> SentCodeType? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.auth.SentCodeType.sentCodeTypeFragmentSms(Cons_sentCodeTypeFragmentSms(url: _1!, length: _2!)) - } - else { - return nil - } - } - public static func parse_sentCodeTypeMissedCall(_ reader: BufferReader) -> SentCodeType? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.auth.SentCodeType.sentCodeTypeMissedCall(Cons_sentCodeTypeMissedCall(prefix: _1!, length: _2!)) - } - else { - return nil - } - } - public static func parse_sentCodeTypeSetUpEmailRequired(_ reader: BufferReader) -> SentCodeType? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.auth.SentCodeType.sentCodeTypeSetUpEmailRequired(Cons_sentCodeTypeSetUpEmailRequired(flags: _1!)) - } - else { - return nil - } - } - public static func parse_sentCodeTypeSms(_ reader: BufferReader) -> SentCodeType? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.auth.SentCodeType.sentCodeTypeSms(Cons_sentCodeTypeSms(length: _1!)) - } - else { - return nil - } - } - public static func parse_sentCodeTypeSmsPhrase(_ reader: BufferReader) -> SentCodeType? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - if Int(_1!) & Int(1 << 0) != 0 { - _2 = parseString(reader) - } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - if _c1 && _c2 { - return Api.auth.SentCodeType.sentCodeTypeSmsPhrase(Cons_sentCodeTypeSmsPhrase(flags: _1!, beginning: _2)) - } - else { - return nil - } - } - public static func parse_sentCodeTypeSmsWord(_ reader: BufferReader) -> SentCodeType? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - if Int(_1!) & Int(1 << 0) != 0 { - _2 = parseString(reader) - } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - if _c1 && _c2 { - return Api.auth.SentCodeType.sentCodeTypeSmsWord(Cons_sentCodeTypeSmsWord(flags: _1!, beginning: _2)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api32.swift b/submodules/TelegramApi/Sources/Api32.swift index c188d6b7db..fbf3ae21e5 100644 --- a/submodules/TelegramApi/Sources/Api32.swift +++ b/submodules/TelegramApi/Sources/Api32.swift @@ -1,3 +1,517 @@ +public extension Api.auth { + enum SentCodeType: TypeConstructorDescription { + public class Cons_sentCodeTypeApp: TypeConstructorDescription { + public var length: Int32 + public init(length: Int32) { + self.length = length + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("sentCodeTypeApp", [("length", ConstructorParameterDescription(self.length))]) + } + } + public class Cons_sentCodeTypeCall: TypeConstructorDescription { + public var length: Int32 + public init(length: Int32) { + self.length = length + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("sentCodeTypeCall", [("length", ConstructorParameterDescription(self.length))]) + } + } + public class Cons_sentCodeTypeEmailCode: TypeConstructorDescription { + public var flags: Int32 + public var emailPattern: String + public var length: Int32 + public var resetAvailablePeriod: Int32? + public var resetPendingDate: Int32? + public init(flags: Int32, emailPattern: String, length: Int32, resetAvailablePeriod: Int32?, resetPendingDate: Int32?) { + self.flags = flags + self.emailPattern = emailPattern + self.length = length + self.resetAvailablePeriod = resetAvailablePeriod + self.resetPendingDate = resetPendingDate + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("sentCodeTypeEmailCode", [("flags", ConstructorParameterDescription(self.flags)), ("emailPattern", ConstructorParameterDescription(self.emailPattern)), ("length", ConstructorParameterDescription(self.length)), ("resetAvailablePeriod", ConstructorParameterDescription(self.resetAvailablePeriod)), ("resetPendingDate", ConstructorParameterDescription(self.resetPendingDate))]) + } + } + public class Cons_sentCodeTypeFirebaseSms: TypeConstructorDescription { + public var flags: Int32 + public var nonce: Buffer? + public var playIntegrityProjectId: Int64? + public var playIntegrityNonce: Buffer? + public var receipt: String? + public var pushTimeout: Int32? + public var length: Int32 + public init(flags: Int32, nonce: Buffer?, playIntegrityProjectId: Int64?, playIntegrityNonce: Buffer?, receipt: String?, pushTimeout: Int32?, length: Int32) { + self.flags = flags + self.nonce = nonce + self.playIntegrityProjectId = playIntegrityProjectId + self.playIntegrityNonce = playIntegrityNonce + self.receipt = receipt + self.pushTimeout = pushTimeout + self.length = length + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("sentCodeTypeFirebaseSms", [("flags", ConstructorParameterDescription(self.flags)), ("nonce", ConstructorParameterDescription(self.nonce)), ("playIntegrityProjectId", ConstructorParameterDescription(self.playIntegrityProjectId)), ("playIntegrityNonce", ConstructorParameterDescription(self.playIntegrityNonce)), ("receipt", ConstructorParameterDescription(self.receipt)), ("pushTimeout", ConstructorParameterDescription(self.pushTimeout)), ("length", ConstructorParameterDescription(self.length))]) + } + } + public class Cons_sentCodeTypeFlashCall: TypeConstructorDescription { + public var pattern: String + public init(pattern: String) { + self.pattern = pattern + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("sentCodeTypeFlashCall", [("pattern", ConstructorParameterDescription(self.pattern))]) + } + } + public class Cons_sentCodeTypeFragmentSms: TypeConstructorDescription { + public var url: String + public var length: Int32 + public init(url: String, length: Int32) { + self.url = url + self.length = length + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("sentCodeTypeFragmentSms", [("url", ConstructorParameterDescription(self.url)), ("length", ConstructorParameterDescription(self.length))]) + } + } + public class Cons_sentCodeTypeMissedCall: TypeConstructorDescription { + public var prefix: String + public var length: Int32 + public init(prefix: String, length: Int32) { + self.prefix = prefix + self.length = length + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("sentCodeTypeMissedCall", [("prefix", ConstructorParameterDescription(self.prefix)), ("length", ConstructorParameterDescription(self.length))]) + } + } + public class Cons_sentCodeTypeSetUpEmailRequired: TypeConstructorDescription { + public var flags: Int32 + public init(flags: Int32) { + self.flags = flags + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("sentCodeTypeSetUpEmailRequired", [("flags", ConstructorParameterDescription(self.flags))]) + } + } + public class Cons_sentCodeTypeSms: TypeConstructorDescription { + public var length: Int32 + public init(length: Int32) { + self.length = length + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("sentCodeTypeSms", [("length", ConstructorParameterDescription(self.length))]) + } + } + public class Cons_sentCodeTypeSmsPhrase: TypeConstructorDescription { + public var flags: Int32 + public var beginning: String? + public init(flags: Int32, beginning: String?) { + self.flags = flags + self.beginning = beginning + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("sentCodeTypeSmsPhrase", [("flags", ConstructorParameterDescription(self.flags)), ("beginning", ConstructorParameterDescription(self.beginning))]) + } + } + public class Cons_sentCodeTypeSmsWord: TypeConstructorDescription { + public var flags: Int32 + public var beginning: String? + public init(flags: Int32, beginning: String?) { + self.flags = flags + self.beginning = beginning + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("sentCodeTypeSmsWord", [("flags", ConstructorParameterDescription(self.flags)), ("beginning", ConstructorParameterDescription(self.beginning))]) + } + } + case sentCodeTypeApp(Cons_sentCodeTypeApp) + case sentCodeTypeCall(Cons_sentCodeTypeCall) + case sentCodeTypeEmailCode(Cons_sentCodeTypeEmailCode) + case sentCodeTypeFirebaseSms(Cons_sentCodeTypeFirebaseSms) + case sentCodeTypeFlashCall(Cons_sentCodeTypeFlashCall) + case sentCodeTypeFragmentSms(Cons_sentCodeTypeFragmentSms) + case sentCodeTypeMissedCall(Cons_sentCodeTypeMissedCall) + case sentCodeTypeSetUpEmailRequired(Cons_sentCodeTypeSetUpEmailRequired) + case sentCodeTypeSms(Cons_sentCodeTypeSms) + case sentCodeTypeSmsPhrase(Cons_sentCodeTypeSmsPhrase) + case sentCodeTypeSmsWord(Cons_sentCodeTypeSmsWord) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sentCodeTypeApp(let _data): + if boxed { + buffer.appendInt32(1035688326) + } + serializeInt32(_data.length, buffer: buffer, boxed: false) + break + case .sentCodeTypeCall(let _data): + if boxed { + buffer.appendInt32(1398007207) + } + serializeInt32(_data.length, buffer: buffer, boxed: false) + break + case .sentCodeTypeEmailCode(let _data): + if boxed { + buffer.appendInt32(-196020837) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeString(_data.emailPattern, buffer: buffer, boxed: false) + serializeInt32(_data.length, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 3) != 0 { + serializeInt32(_data.resetAvailablePeriod!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 4) != 0 { + serializeInt32(_data.resetPendingDate!, buffer: buffer, boxed: false) + } + break + case .sentCodeTypeFirebaseSms(let _data): + if boxed { + buffer.appendInt32(10475318) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeBytes(_data.nonce!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 2) != 0 { + serializeInt64(_data.playIntegrityProjectId!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 2) != 0 { + serializeBytes(_data.playIntegrityNonce!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 1) != 0 { + serializeString(_data.receipt!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 1) != 0 { + serializeInt32(_data.pushTimeout!, buffer: buffer, boxed: false) + } + serializeInt32(_data.length, buffer: buffer, boxed: false) + break + case .sentCodeTypeFlashCall(let _data): + if boxed { + buffer.appendInt32(-1425815847) + } + serializeString(_data.pattern, buffer: buffer, boxed: false) + break + case .sentCodeTypeFragmentSms(let _data): + if boxed { + buffer.appendInt32(-648651719) + } + serializeString(_data.url, buffer: buffer, boxed: false) + serializeInt32(_data.length, buffer: buffer, boxed: false) + break + case .sentCodeTypeMissedCall(let _data): + if boxed { + buffer.appendInt32(-2113903484) + } + serializeString(_data.prefix, buffer: buffer, boxed: false) + serializeInt32(_data.length, buffer: buffer, boxed: false) + break + case .sentCodeTypeSetUpEmailRequired(let _data): + if boxed { + buffer.appendInt32(-1521934870) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + break + case .sentCodeTypeSms(let _data): + if boxed { + buffer.appendInt32(-1073693790) + } + serializeInt32(_data.length, buffer: buffer, boxed: false) + break + case .sentCodeTypeSmsPhrase(let _data): + if boxed { + buffer.appendInt32(-1284008785) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeString(_data.beginning!, buffer: buffer, boxed: false) + } + break + case .sentCodeTypeSmsWord(let _data): + if boxed { + buffer.appendInt32(-1542017919) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeString(_data.beginning!, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .sentCodeTypeApp(let _data): + return ("sentCodeTypeApp", [("length", ConstructorParameterDescription(_data.length))]) + case .sentCodeTypeCall(let _data): + return ("sentCodeTypeCall", [("length", ConstructorParameterDescription(_data.length))]) + case .sentCodeTypeEmailCode(let _data): + return ("sentCodeTypeEmailCode", [("flags", ConstructorParameterDescription(_data.flags)), ("emailPattern", ConstructorParameterDescription(_data.emailPattern)), ("length", ConstructorParameterDescription(_data.length)), ("resetAvailablePeriod", ConstructorParameterDescription(_data.resetAvailablePeriod)), ("resetPendingDate", ConstructorParameterDescription(_data.resetPendingDate))]) + case .sentCodeTypeFirebaseSms(let _data): + return ("sentCodeTypeFirebaseSms", [("flags", ConstructorParameterDescription(_data.flags)), ("nonce", ConstructorParameterDescription(_data.nonce)), ("playIntegrityProjectId", ConstructorParameterDescription(_data.playIntegrityProjectId)), ("playIntegrityNonce", ConstructorParameterDescription(_data.playIntegrityNonce)), ("receipt", ConstructorParameterDescription(_data.receipt)), ("pushTimeout", ConstructorParameterDescription(_data.pushTimeout)), ("length", ConstructorParameterDescription(_data.length))]) + case .sentCodeTypeFlashCall(let _data): + return ("sentCodeTypeFlashCall", [("pattern", ConstructorParameterDescription(_data.pattern))]) + case .sentCodeTypeFragmentSms(let _data): + return ("sentCodeTypeFragmentSms", [("url", ConstructorParameterDescription(_data.url)), ("length", ConstructorParameterDescription(_data.length))]) + case .sentCodeTypeMissedCall(let _data): + return ("sentCodeTypeMissedCall", [("prefix", ConstructorParameterDescription(_data.prefix)), ("length", ConstructorParameterDescription(_data.length))]) + case .sentCodeTypeSetUpEmailRequired(let _data): + return ("sentCodeTypeSetUpEmailRequired", [("flags", ConstructorParameterDescription(_data.flags))]) + case .sentCodeTypeSms(let _data): + return ("sentCodeTypeSms", [("length", ConstructorParameterDescription(_data.length))]) + case .sentCodeTypeSmsPhrase(let _data): + return ("sentCodeTypeSmsPhrase", [("flags", ConstructorParameterDescription(_data.flags)), ("beginning", ConstructorParameterDescription(_data.beginning))]) + case .sentCodeTypeSmsWord(let _data): + return ("sentCodeTypeSmsWord", [("flags", ConstructorParameterDescription(_data.flags)), ("beginning", ConstructorParameterDescription(_data.beginning))]) + } + } + + public static func parse_sentCodeTypeApp(_ reader: BufferReader) -> SentCodeType? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.auth.SentCodeType.sentCodeTypeApp(Cons_sentCodeTypeApp(length: _1!)) + } + else { + return nil + } + } + public static func parse_sentCodeTypeCall(_ reader: BufferReader) -> SentCodeType? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.auth.SentCodeType.sentCodeTypeCall(Cons_sentCodeTypeCall(length: _1!)) + } + else { + return nil + } + } + public static func parse_sentCodeTypeEmailCode(_ reader: BufferReader) -> SentCodeType? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _4 = reader.readInt32() + } + var _5: Int32? + if Int(_1 ?? 0) & Int(1 << 4) != 0 { + _5 = reader.readInt32() + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.auth.SentCodeType.sentCodeTypeEmailCode(Cons_sentCodeTypeEmailCode(flags: _1!, emailPattern: _2!, length: _3!, resetAvailablePeriod: _4, resetPendingDate: _5)) + } + else { + return nil + } + } + public static func parse_sentCodeTypeFirebaseSms(_ reader: BufferReader) -> SentCodeType? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Buffer? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _2 = parseBytes(reader) + } + var _3: Int64? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _3 = reader.readInt64() + } + var _4: Buffer? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _4 = parseBytes(reader) + } + var _5: String? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _5 = parseString(reader) + } + var _6: Int32? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _6 = reader.readInt32() + } + var _7: Int32? + _7 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil + let _c7 = _7 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { + return Api.auth.SentCodeType.sentCodeTypeFirebaseSms(Cons_sentCodeTypeFirebaseSms(flags: _1!, nonce: _2, playIntegrityProjectId: _3, playIntegrityNonce: _4, receipt: _5, pushTimeout: _6, length: _7!)) + } + else { + return nil + } + } + public static func parse_sentCodeTypeFlashCall(_ reader: BufferReader) -> SentCodeType? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.auth.SentCodeType.sentCodeTypeFlashCall(Cons_sentCodeTypeFlashCall(pattern: _1!)) + } + else { + return nil + } + } + public static func parse_sentCodeTypeFragmentSms(_ reader: BufferReader) -> SentCodeType? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.auth.SentCodeType.sentCodeTypeFragmentSms(Cons_sentCodeTypeFragmentSms(url: _1!, length: _2!)) + } + else { + return nil + } + } + public static func parse_sentCodeTypeMissedCall(_ reader: BufferReader) -> SentCodeType? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.auth.SentCodeType.sentCodeTypeMissedCall(Cons_sentCodeTypeMissedCall(prefix: _1!, length: _2!)) + } + else { + return nil + } + } + public static func parse_sentCodeTypeSetUpEmailRequired(_ reader: BufferReader) -> SentCodeType? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.auth.SentCodeType.sentCodeTypeSetUpEmailRequired(Cons_sentCodeTypeSetUpEmailRequired(flags: _1!)) + } + else { + return nil + } + } + public static func parse_sentCodeTypeSms(_ reader: BufferReader) -> SentCodeType? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.auth.SentCodeType.sentCodeTypeSms(Cons_sentCodeTypeSms(length: _1!)) + } + else { + return nil + } + } + public static func parse_sentCodeTypeSmsPhrase(_ reader: BufferReader) -> SentCodeType? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _2 = parseString(reader) + } + let _c1 = _1 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + if _c1 && _c2 { + return Api.auth.SentCodeType.sentCodeTypeSmsPhrase(Cons_sentCodeTypeSmsPhrase(flags: _1!, beginning: _2)) + } + else { + return nil + } + } + public static func parse_sentCodeTypeSmsWord(_ reader: BufferReader) -> SentCodeType? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _2 = parseString(reader) + } + let _c1 = _1 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + if _c1 && _c2 { + return Api.auth.SentCodeType.sentCodeTypeSmsWord(Cons_sentCodeTypeSmsWord(flags: _1!, beginning: _2)) + } + else { + return nil + } + } + } +} +public extension Api.bots { + enum AccessSettings: TypeConstructorDescription { + public class Cons_accessSettings: TypeConstructorDescription { + public var flags: Int32 + public var addUsers: [Api.User]? + public init(flags: Int32, addUsers: [Api.User]?) { + self.flags = flags + self.addUsers = addUsers + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("accessSettings", [("flags", ConstructorParameterDescription(self.flags)), ("addUsers", ConstructorParameterDescription(self.addUsers))]) + } + } + case accessSettings(Cons_accessSettings) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .accessSettings(let _data): + if boxed { + buffer.appendInt32(-585121901) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 1) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.addUsers!.count)) + for item in _data.addUsers! { + item.serialize(buffer, true) + } + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .accessSettings(let _data): + return ("accessSettings", [("flags", ConstructorParameterDescription(_data.flags)), ("addUsers", ConstructorParameterDescription(_data.addUsers))]) + } + } + + public static func parse_accessSettings(_ reader: BufferReader) -> AccessSettings? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.User]? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + } + let _c1 = _1 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil + if _c1 && _c2 { + return Api.bots.AccessSettings.accessSettings(Cons_accessSettings(flags: _1!, addUsers: _2)) + } + else { + return nil + } + } + } +} public extension Api.bots { enum BotInfo: TypeConstructorDescription { public class Cons_botInfo: TypeConstructorDescription { @@ -145,7 +659,7 @@ public extension Api.bots { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = parseString(reader) } var _3: [Api.User]? @@ -153,7 +667,7 @@ public extension Api.bots { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { return Api.bots.PopularAppBots.popularAppBots(Cons_popularAppBots(flags: _1!, nextOffset: _2, users: _3!)) @@ -771,7 +1285,7 @@ public extension Api.chatlists { _2 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } var _4: [Api.Peer]? @@ -788,7 +1302,7 @@ public extension Api.chatlists { } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil @@ -1173,641 +1687,3 @@ public extension Api.contacts { } } } -public extension Api.contacts { - enum ContactBirthdays: TypeConstructorDescription { - public class Cons_contactBirthdays: TypeConstructorDescription { - public var contacts: [Api.ContactBirthday] - public var users: [Api.User] - public init(contacts: [Api.ContactBirthday], users: [Api.User]) { - self.contacts = contacts - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("contactBirthdays", [("contacts", ConstructorParameterDescription(self.contacts)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case contactBirthdays(Cons_contactBirthdays) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .contactBirthdays(let _data): - if boxed { - buffer.appendInt32(290452237) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.contacts.count)) - for item in _data.contacts { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .contactBirthdays(let _data): - return ("contactBirthdays", [("contacts", ConstructorParameterDescription(_data.contacts)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_contactBirthdays(_ reader: BufferReader) -> ContactBirthdays? { - var _1: [Api.ContactBirthday]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ContactBirthday.self) - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.contacts.ContactBirthdays.contactBirthdays(Cons_contactBirthdays(contacts: _1!, users: _2!)) - } - else { - return nil - } - } - } -} -public extension Api.contacts { - enum Contacts: TypeConstructorDescription { - public class Cons_contacts: TypeConstructorDescription { - public var contacts: [Api.Contact] - public var savedCount: Int32 - public var users: [Api.User] - public init(contacts: [Api.Contact], savedCount: Int32, users: [Api.User]) { - self.contacts = contacts - self.savedCount = savedCount - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("contacts", [("contacts", ConstructorParameterDescription(self.contacts)), ("savedCount", ConstructorParameterDescription(self.savedCount)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case contacts(Cons_contacts) - case contactsNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .contacts(let _data): - if boxed { - buffer.appendInt32(-353862078) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.contacts.count)) - for item in _data.contacts { - item.serialize(buffer, true) - } - serializeInt32(_data.savedCount, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - case .contactsNotModified: - if boxed { - buffer.appendInt32(-1219778094) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .contacts(let _data): - return ("contacts", [("contacts", ConstructorParameterDescription(_data.contacts)), ("savedCount", ConstructorParameterDescription(_data.savedCount)), ("users", ConstructorParameterDescription(_data.users))]) - case .contactsNotModified: - return ("contactsNotModified", []) - } - } - - public static func parse_contacts(_ reader: BufferReader) -> Contacts? { - var _1: [Api.Contact]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Contact.self) - } - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.contacts.Contacts.contacts(Cons_contacts(contacts: _1!, savedCount: _2!, users: _3!)) - } - else { - return nil - } - } - public static func parse_contactsNotModified(_ reader: BufferReader) -> Contacts? { - return Api.contacts.Contacts.contactsNotModified - } - } -} -public extension Api.contacts { - enum Found: TypeConstructorDescription { - public class Cons_found: TypeConstructorDescription { - public var myResults: [Api.Peer] - public var results: [Api.Peer] - public var chats: [Api.Chat] - public var users: [Api.User] - public init(myResults: [Api.Peer], results: [Api.Peer], chats: [Api.Chat], users: [Api.User]) { - self.myResults = myResults - self.results = results - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("found", [("myResults", ConstructorParameterDescription(self.myResults)), ("results", ConstructorParameterDescription(self.results)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case found(Cons_found) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .found(let _data): - if boxed { - buffer.appendInt32(-1290580579) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.myResults.count)) - for item in _data.myResults { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.results.count)) - for item in _data.results { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .found(let _data): - return ("found", [("myResults", ConstructorParameterDescription(_data.myResults)), ("results", ConstructorParameterDescription(_data.results)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_found(_ reader: BufferReader) -> Found? { - var _1: [Api.Peer]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) - } - var _2: [Api.Peer]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) - } - var _3: [Api.Chat]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _4: [Api.User]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.contacts.Found.found(Cons_found(myResults: _1!, results: _2!, chats: _3!, users: _4!)) - } - else { - return nil - } - } - } -} -public extension Api.contacts { - enum ImportedContacts: TypeConstructorDescription { - public class Cons_importedContacts: TypeConstructorDescription { - public var imported: [Api.ImportedContact] - public var popularInvites: [Api.PopularContact] - public var retryContacts: [Int64] - public var users: [Api.User] - public init(imported: [Api.ImportedContact], popularInvites: [Api.PopularContact], retryContacts: [Int64], users: [Api.User]) { - self.imported = imported - self.popularInvites = popularInvites - self.retryContacts = retryContacts - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("importedContacts", [("imported", ConstructorParameterDescription(self.imported)), ("popularInvites", ConstructorParameterDescription(self.popularInvites)), ("retryContacts", ConstructorParameterDescription(self.retryContacts)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case importedContacts(Cons_importedContacts) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .importedContacts(let _data): - if boxed { - buffer.appendInt32(2010127419) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.imported.count)) - for item in _data.imported { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.popularInvites.count)) - for item in _data.popularInvites { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.retryContacts.count)) - for item in _data.retryContacts { - serializeInt64(item, buffer: buffer, boxed: false) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .importedContacts(let _data): - return ("importedContacts", [("imported", ConstructorParameterDescription(_data.imported)), ("popularInvites", ConstructorParameterDescription(_data.popularInvites)), ("retryContacts", ConstructorParameterDescription(_data.retryContacts)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_importedContacts(_ reader: BufferReader) -> ImportedContacts? { - var _1: [Api.ImportedContact]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ImportedContact.self) - } - var _2: [Api.PopularContact]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PopularContact.self) - } - var _3: [Int64]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - var _4: [Api.User]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.contacts.ImportedContacts.importedContacts(Cons_importedContacts(imported: _1!, popularInvites: _2!, retryContacts: _3!, users: _4!)) - } - else { - return nil - } - } - } -} -public extension Api.contacts { - enum ResolvedPeer: TypeConstructorDescription { - public class Cons_resolvedPeer: TypeConstructorDescription { - public var peer: Api.Peer - public var chats: [Api.Chat] - public var users: [Api.User] - public init(peer: Api.Peer, chats: [Api.Chat], users: [Api.User]) { - self.peer = peer - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("resolvedPeer", [("peer", ConstructorParameterDescription(self.peer)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case resolvedPeer(Cons_resolvedPeer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .resolvedPeer(let _data): - if boxed { - buffer.appendInt32(2131196633) - } - _data.peer.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .resolvedPeer(let _data): - return ("resolvedPeer", [("peer", ConstructorParameterDescription(_data.peer)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_resolvedPeer(_ reader: BufferReader) -> ResolvedPeer? { - var _1: Api.Peer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Peer - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.contacts.ResolvedPeer.resolvedPeer(Cons_resolvedPeer(peer: _1!, chats: _2!, users: _3!)) - } - else { - return nil - } - } - } -} -public extension Api.contacts { - enum SponsoredPeers: TypeConstructorDescription { - public class Cons_sponsoredPeers: TypeConstructorDescription { - public var peers: [Api.SponsoredPeer] - public var chats: [Api.Chat] - public var users: [Api.User] - public init(peers: [Api.SponsoredPeer], chats: [Api.Chat], users: [Api.User]) { - self.peers = peers - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("sponsoredPeers", [("peers", ConstructorParameterDescription(self.peers)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case sponsoredPeers(Cons_sponsoredPeers) - case sponsoredPeersEmpty - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .sponsoredPeers(let _data): - if boxed { - buffer.appendInt32(-352114556) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.peers.count)) - for item in _data.peers { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - case .sponsoredPeersEmpty: - if boxed { - buffer.appendInt32(-365775695) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .sponsoredPeers(let _data): - return ("sponsoredPeers", [("peers", ConstructorParameterDescription(_data.peers)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - case .sponsoredPeersEmpty: - return ("sponsoredPeersEmpty", []) - } - } - - public static func parse_sponsoredPeers(_ reader: BufferReader) -> SponsoredPeers? { - var _1: [Api.SponsoredPeer]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SponsoredPeer.self) - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.contacts.SponsoredPeers.sponsoredPeers(Cons_sponsoredPeers(peers: _1!, chats: _2!, users: _3!)) - } - else { - return nil - } - } - public static func parse_sponsoredPeersEmpty(_ reader: BufferReader) -> SponsoredPeers? { - return Api.contacts.SponsoredPeers.sponsoredPeersEmpty - } - } -} -public extension Api.contacts { - enum TopPeers: TypeConstructorDescription { - public class Cons_topPeers: TypeConstructorDescription { - public var categories: [Api.TopPeerCategoryPeers] - public var chats: [Api.Chat] - public var users: [Api.User] - public init(categories: [Api.TopPeerCategoryPeers], chats: [Api.Chat], users: [Api.User]) { - self.categories = categories - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("topPeers", [("categories", ConstructorParameterDescription(self.categories)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case topPeers(Cons_topPeers) - case topPeersDisabled - case topPeersNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .topPeers(let _data): - if boxed { - buffer.appendInt32(1891070632) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.categories.count)) - for item in _data.categories { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - case .topPeersDisabled: - if boxed { - buffer.appendInt32(-1255369827) - } - break - case .topPeersNotModified: - if boxed { - buffer.appendInt32(-567906571) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .topPeers(let _data): - return ("topPeers", [("categories", ConstructorParameterDescription(_data.categories)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - case .topPeersDisabled: - return ("topPeersDisabled", []) - case .topPeersNotModified: - return ("topPeersNotModified", []) - } - } - - public static func parse_topPeers(_ reader: BufferReader) -> TopPeers? { - var _1: [Api.TopPeerCategoryPeers]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TopPeerCategoryPeers.self) - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.contacts.TopPeers.topPeers(Cons_topPeers(categories: _1!, chats: _2!, users: _3!)) - } - else { - return nil - } - } - public static func parse_topPeersDisabled(_ reader: BufferReader) -> TopPeers? { - return Api.contacts.TopPeers.topPeersDisabled - } - public static func parse_topPeersNotModified(_ reader: BufferReader) -> TopPeers? { - return Api.contacts.TopPeers.topPeersNotModified - } - } -} -public extension Api.fragment { - enum CollectibleInfo: TypeConstructorDescription { - public class Cons_collectibleInfo: TypeConstructorDescription { - public var purchaseDate: Int32 - public var currency: String - public var amount: Int64 - public var cryptoCurrency: String - public var cryptoAmount: Int64 - public var url: String - public init(purchaseDate: Int32, currency: String, amount: Int64, cryptoCurrency: String, cryptoAmount: Int64, url: String) { - self.purchaseDate = purchaseDate - self.currency = currency - self.amount = amount - self.cryptoCurrency = cryptoCurrency - self.cryptoAmount = cryptoAmount - self.url = url - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("collectibleInfo", [("purchaseDate", ConstructorParameterDescription(self.purchaseDate)), ("currency", ConstructorParameterDescription(self.currency)), ("amount", ConstructorParameterDescription(self.amount)), ("cryptoCurrency", ConstructorParameterDescription(self.cryptoCurrency)), ("cryptoAmount", ConstructorParameterDescription(self.cryptoAmount)), ("url", ConstructorParameterDescription(self.url))]) - } - } - case collectibleInfo(Cons_collectibleInfo) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .collectibleInfo(let _data): - if boxed { - buffer.appendInt32(1857945489) - } - serializeInt32(_data.purchaseDate, buffer: buffer, boxed: false) - serializeString(_data.currency, buffer: buffer, boxed: false) - serializeInt64(_data.amount, buffer: buffer, boxed: false) - serializeString(_data.cryptoCurrency, buffer: buffer, boxed: false) - serializeInt64(_data.cryptoAmount, buffer: buffer, boxed: false) - serializeString(_data.url, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .collectibleInfo(let _data): - return ("collectibleInfo", [("purchaseDate", ConstructorParameterDescription(_data.purchaseDate)), ("currency", ConstructorParameterDescription(_data.currency)), ("amount", ConstructorParameterDescription(_data.amount)), ("cryptoCurrency", ConstructorParameterDescription(_data.cryptoCurrency)), ("cryptoAmount", ConstructorParameterDescription(_data.cryptoAmount)), ("url", ConstructorParameterDescription(_data.url))]) - } - } - - public static func parse_collectibleInfo(_ reader: BufferReader) -> CollectibleInfo? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: Int64? - _3 = reader.readInt64() - var _4: String? - _4 = parseString(reader) - var _5: Int64? - _5 = reader.readInt64() - var _6: String? - _6 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.fragment.CollectibleInfo.collectibleInfo(Cons_collectibleInfo(purchaseDate: _1!, currency: _2!, amount: _3!, cryptoCurrency: _4!, cryptoAmount: _5!, url: _6!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api33.swift b/submodules/TelegramApi/Sources/Api33.swift index d3b185ee7f..5a54e3e8a9 100644 --- a/submodules/TelegramApi/Sources/Api33.swift +++ b/submodules/TelegramApi/Sources/Api33.swift @@ -1,3 +1,641 @@ +public extension Api.contacts { + enum ContactBirthdays: TypeConstructorDescription { + public class Cons_contactBirthdays: TypeConstructorDescription { + public var contacts: [Api.ContactBirthday] + public var users: [Api.User] + public init(contacts: [Api.ContactBirthday], users: [Api.User]) { + self.contacts = contacts + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("contactBirthdays", [("contacts", ConstructorParameterDescription(self.contacts)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case contactBirthdays(Cons_contactBirthdays) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .contactBirthdays(let _data): + if boxed { + buffer.appendInt32(290452237) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.contacts.count)) + for item in _data.contacts { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .contactBirthdays(let _data): + return ("contactBirthdays", [("contacts", ConstructorParameterDescription(_data.contacts)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_contactBirthdays(_ reader: BufferReader) -> ContactBirthdays? { + var _1: [Api.ContactBirthday]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ContactBirthday.self) + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.contacts.ContactBirthdays.contactBirthdays(Cons_contactBirthdays(contacts: _1!, users: _2!)) + } + else { + return nil + } + } + } +} +public extension Api.contacts { + enum Contacts: TypeConstructorDescription { + public class Cons_contacts: TypeConstructorDescription { + public var contacts: [Api.Contact] + public var savedCount: Int32 + public var users: [Api.User] + public init(contacts: [Api.Contact], savedCount: Int32, users: [Api.User]) { + self.contacts = contacts + self.savedCount = savedCount + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("contacts", [("contacts", ConstructorParameterDescription(self.contacts)), ("savedCount", ConstructorParameterDescription(self.savedCount)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case contacts(Cons_contacts) + case contactsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .contacts(let _data): + if boxed { + buffer.appendInt32(-353862078) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.contacts.count)) + for item in _data.contacts { + item.serialize(buffer, true) + } + serializeInt32(_data.savedCount, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + case .contactsNotModified: + if boxed { + buffer.appendInt32(-1219778094) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .contacts(let _data): + return ("contacts", [("contacts", ConstructorParameterDescription(_data.contacts)), ("savedCount", ConstructorParameterDescription(_data.savedCount)), ("users", ConstructorParameterDescription(_data.users))]) + case .contactsNotModified: + return ("contactsNotModified", []) + } + } + + public static func parse_contacts(_ reader: BufferReader) -> Contacts? { + var _1: [Api.Contact]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Contact.self) + } + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.contacts.Contacts.contacts(Cons_contacts(contacts: _1!, savedCount: _2!, users: _3!)) + } + else { + return nil + } + } + public static func parse_contactsNotModified(_ reader: BufferReader) -> Contacts? { + return Api.contacts.Contacts.contactsNotModified + } + } +} +public extension Api.contacts { + enum Found: TypeConstructorDescription { + public class Cons_found: TypeConstructorDescription { + public var myResults: [Api.Peer] + public var results: [Api.Peer] + public var chats: [Api.Chat] + public var users: [Api.User] + public init(myResults: [Api.Peer], results: [Api.Peer], chats: [Api.Chat], users: [Api.User]) { + self.myResults = myResults + self.results = results + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("found", [("myResults", ConstructorParameterDescription(self.myResults)), ("results", ConstructorParameterDescription(self.results)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case found(Cons_found) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .found(let _data): + if boxed { + buffer.appendInt32(-1290580579) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.myResults.count)) + for item in _data.myResults { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.results.count)) + for item in _data.results { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .found(let _data): + return ("found", [("myResults", ConstructorParameterDescription(_data.myResults)), ("results", ConstructorParameterDescription(_data.results)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_found(_ reader: BufferReader) -> Found? { + var _1: [Api.Peer]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) + } + var _2: [Api.Peer]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Peer.self) + } + var _3: [Api.Chat]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.contacts.Found.found(Cons_found(myResults: _1!, results: _2!, chats: _3!, users: _4!)) + } + else { + return nil + } + } + } +} +public extension Api.contacts { + enum ImportedContacts: TypeConstructorDescription { + public class Cons_importedContacts: TypeConstructorDescription { + public var imported: [Api.ImportedContact] + public var popularInvites: [Api.PopularContact] + public var retryContacts: [Int64] + public var users: [Api.User] + public init(imported: [Api.ImportedContact], popularInvites: [Api.PopularContact], retryContacts: [Int64], users: [Api.User]) { + self.imported = imported + self.popularInvites = popularInvites + self.retryContacts = retryContacts + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("importedContacts", [("imported", ConstructorParameterDescription(self.imported)), ("popularInvites", ConstructorParameterDescription(self.popularInvites)), ("retryContacts", ConstructorParameterDescription(self.retryContacts)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case importedContacts(Cons_importedContacts) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .importedContacts(let _data): + if boxed { + buffer.appendInt32(2010127419) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.imported.count)) + for item in _data.imported { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.popularInvites.count)) + for item in _data.popularInvites { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.retryContacts.count)) + for item in _data.retryContacts { + serializeInt64(item, buffer: buffer, boxed: false) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .importedContacts(let _data): + return ("importedContacts", [("imported", ConstructorParameterDescription(_data.imported)), ("popularInvites", ConstructorParameterDescription(_data.popularInvites)), ("retryContacts", ConstructorParameterDescription(_data.retryContacts)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_importedContacts(_ reader: BufferReader) -> ImportedContacts? { + var _1: [Api.ImportedContact]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ImportedContact.self) + } + var _2: [Api.PopularContact]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PopularContact.self) + } + var _3: [Int64]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.contacts.ImportedContacts.importedContacts(Cons_importedContacts(imported: _1!, popularInvites: _2!, retryContacts: _3!, users: _4!)) + } + else { + return nil + } + } + } +} +public extension Api.contacts { + enum ResolvedPeer: TypeConstructorDescription { + public class Cons_resolvedPeer: TypeConstructorDescription { + public var peer: Api.Peer + public var chats: [Api.Chat] + public var users: [Api.User] + public init(peer: Api.Peer, chats: [Api.Chat], users: [Api.User]) { + self.peer = peer + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("resolvedPeer", [("peer", ConstructorParameterDescription(self.peer)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case resolvedPeer(Cons_resolvedPeer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .resolvedPeer(let _data): + if boxed { + buffer.appendInt32(2131196633) + } + _data.peer.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .resolvedPeer(let _data): + return ("resolvedPeer", [("peer", ConstructorParameterDescription(_data.peer)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_resolvedPeer(_ reader: BufferReader) -> ResolvedPeer? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.contacts.ResolvedPeer.resolvedPeer(Cons_resolvedPeer(peer: _1!, chats: _2!, users: _3!)) + } + else { + return nil + } + } + } +} +public extension Api.contacts { + enum SponsoredPeers: TypeConstructorDescription { + public class Cons_sponsoredPeers: TypeConstructorDescription { + public var peers: [Api.SponsoredPeer] + public var chats: [Api.Chat] + public var users: [Api.User] + public init(peers: [Api.SponsoredPeer], chats: [Api.Chat], users: [Api.User]) { + self.peers = peers + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("sponsoredPeers", [("peers", ConstructorParameterDescription(self.peers)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case sponsoredPeers(Cons_sponsoredPeers) + case sponsoredPeersEmpty + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sponsoredPeers(let _data): + if boxed { + buffer.appendInt32(-352114556) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.peers.count)) + for item in _data.peers { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + case .sponsoredPeersEmpty: + if boxed { + buffer.appendInt32(-365775695) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .sponsoredPeers(let _data): + return ("sponsoredPeers", [("peers", ConstructorParameterDescription(_data.peers)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + case .sponsoredPeersEmpty: + return ("sponsoredPeersEmpty", []) + } + } + + public static func parse_sponsoredPeers(_ reader: BufferReader) -> SponsoredPeers? { + var _1: [Api.SponsoredPeer]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SponsoredPeer.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.contacts.SponsoredPeers.sponsoredPeers(Cons_sponsoredPeers(peers: _1!, chats: _2!, users: _3!)) + } + else { + return nil + } + } + public static func parse_sponsoredPeersEmpty(_ reader: BufferReader) -> SponsoredPeers? { + return Api.contacts.SponsoredPeers.sponsoredPeersEmpty + } + } +} +public extension Api.contacts { + enum TopPeers: TypeConstructorDescription { + public class Cons_topPeers: TypeConstructorDescription { + public var categories: [Api.TopPeerCategoryPeers] + public var chats: [Api.Chat] + public var users: [Api.User] + public init(categories: [Api.TopPeerCategoryPeers], chats: [Api.Chat], users: [Api.User]) { + self.categories = categories + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("topPeers", [("categories", ConstructorParameterDescription(self.categories)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case topPeers(Cons_topPeers) + case topPeersDisabled + case topPeersNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .topPeers(let _data): + if boxed { + buffer.appendInt32(1891070632) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.categories.count)) + for item in _data.categories { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + case .topPeersDisabled: + if boxed { + buffer.appendInt32(-1255369827) + } + break + case .topPeersNotModified: + if boxed { + buffer.appendInt32(-567906571) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .topPeers(let _data): + return ("topPeers", [("categories", ConstructorParameterDescription(_data.categories)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + case .topPeersDisabled: + return ("topPeersDisabled", []) + case .topPeersNotModified: + return ("topPeersNotModified", []) + } + } + + public static func parse_topPeers(_ reader: BufferReader) -> TopPeers? { + var _1: [Api.TopPeerCategoryPeers]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.TopPeerCategoryPeers.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.contacts.TopPeers.topPeers(Cons_topPeers(categories: _1!, chats: _2!, users: _3!)) + } + else { + return nil + } + } + public static func parse_topPeersDisabled(_ reader: BufferReader) -> TopPeers? { + return Api.contacts.TopPeers.topPeersDisabled + } + public static func parse_topPeersNotModified(_ reader: BufferReader) -> TopPeers? { + return Api.contacts.TopPeers.topPeersNotModified + } + } +} +public extension Api.fragment { + enum CollectibleInfo: TypeConstructorDescription { + public class Cons_collectibleInfo: TypeConstructorDescription { + public var purchaseDate: Int32 + public var currency: String + public var amount: Int64 + public var cryptoCurrency: String + public var cryptoAmount: Int64 + public var url: String + public init(purchaseDate: Int32, currency: String, amount: Int64, cryptoCurrency: String, cryptoAmount: Int64, url: String) { + self.purchaseDate = purchaseDate + self.currency = currency + self.amount = amount + self.cryptoCurrency = cryptoCurrency + self.cryptoAmount = cryptoAmount + self.url = url + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("collectibleInfo", [("purchaseDate", ConstructorParameterDescription(self.purchaseDate)), ("currency", ConstructorParameterDescription(self.currency)), ("amount", ConstructorParameterDescription(self.amount)), ("cryptoCurrency", ConstructorParameterDescription(self.cryptoCurrency)), ("cryptoAmount", ConstructorParameterDescription(self.cryptoAmount)), ("url", ConstructorParameterDescription(self.url))]) + } + } + case collectibleInfo(Cons_collectibleInfo) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .collectibleInfo(let _data): + if boxed { + buffer.appendInt32(1857945489) + } + serializeInt32(_data.purchaseDate, buffer: buffer, boxed: false) + serializeString(_data.currency, buffer: buffer, boxed: false) + serializeInt64(_data.amount, buffer: buffer, boxed: false) + serializeString(_data.cryptoCurrency, buffer: buffer, boxed: false) + serializeInt64(_data.cryptoAmount, buffer: buffer, boxed: false) + serializeString(_data.url, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .collectibleInfo(let _data): + return ("collectibleInfo", [("purchaseDate", ConstructorParameterDescription(_data.purchaseDate)), ("currency", ConstructorParameterDescription(_data.currency)), ("amount", ConstructorParameterDescription(_data.amount)), ("cryptoCurrency", ConstructorParameterDescription(_data.cryptoCurrency)), ("cryptoAmount", ConstructorParameterDescription(_data.cryptoAmount)), ("url", ConstructorParameterDescription(_data.url))]) + } + } + + public static func parse_collectibleInfo(_ reader: BufferReader) -> CollectibleInfo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() + var _4: String? + _4 = parseString(reader) + var _5: Int64? + _5 = reader.readInt64() + var _6: String? + _6 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.fragment.CollectibleInfo.collectibleInfo(Cons_collectibleInfo(purchaseDate: _1!, currency: _2!, amount: _3!, cryptoCurrency: _4!, cryptoAmount: _5!, url: _6!)) + } + else { + return nil + } + } + } +} public extension Api.help { enum AppConfig: TypeConstructorDescription { public class Cons_appConfig: TypeConstructorDescription { @@ -145,17 +783,17 @@ public extension Api.help { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } var _6: Api.Document? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.Document } } var _7: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _7 = parseString(reader) } var _8: Api.Document? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.Document } @@ -165,9 +803,9 @@ public extension Api.help { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.help.AppUpdate.appUpdate(Cons_appUpdate(flags: _1!, id: _2!, version: _3!, text: _4!, entities: _5!, document: _6, url: _7, sticker: _8)) } @@ -304,7 +942,7 @@ public extension Api.help { var _3: String? _3 = parseString(reader) var _4: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = parseString(reader) } var _5: [Api.help.CountryCode]? @@ -314,7 +952,7 @@ public extension Api.help { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.help.Country.country(Cons_country(flags: _1!, iso2: _2!, defaultName: _3!, name: _4, countryCodes: _5!)) @@ -383,21 +1021,21 @@ public extension Api.help { var _2: String? _2 = parseString(reader) var _3: [String]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) } } var _4: [String]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.help.CountryCode.countryCode(Cons_countryCode(flags: _1!, countryCode: _2!, prefixes: _3, patterns: _4)) } @@ -464,14 +1102,14 @@ public extension Api.help { var _2: String? _2 = parseString(reader) var _3: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.help.DeepLinkInfo.deepLinkInfo(Cons_deepLinkInfo(flags: _1!, message: _2!, entities: _3)) } @@ -651,31 +1289,31 @@ public extension Api.help { var _2: Int32? _2 = reader.readInt32() var _3: Api.help.PeerColorSet? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.help.PeerColorSet } } var _4: Api.help.PeerColorSet? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.help.PeerColorSet } } var _5: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _5 = reader.readInt32() } var _6: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _6 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.help.PeerColorOption.peerColorOption(Cons_peerColorOption(flags: _1!, colorId: _2!, colors: _3, darkColors: _4, channelMinLevel: _5, groupMinLevel: _6)) } @@ -1071,17 +1709,17 @@ public extension Api.help { var _2: Int32? _2 = reader.readInt32() var _3: Api.Peer? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Peer } } var _4: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = parseString(reader) } var _5: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = parseString(reader) } var _6: [String]? @@ -1093,7 +1731,7 @@ public extension Api.help { _7 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) } var _8: Api.PendingSuggestion? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.PendingSuggestion } @@ -1108,12 +1746,12 @@ public extension Api.help { } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 4) == 0) || _8 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _8 != nil let _c9 = _9 != nil let _c10 = _10 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { @@ -1136,548 +1774,3 @@ public extension Api.help { } } } -public extension Api.help { - enum RecentMeUrls: TypeConstructorDescription { - public class Cons_recentMeUrls: TypeConstructorDescription { - public var urls: [Api.RecentMeUrl] - public var chats: [Api.Chat] - public var users: [Api.User] - public init(urls: [Api.RecentMeUrl], chats: [Api.Chat], users: [Api.User]) { - self.urls = urls - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("recentMeUrls", [("urls", ConstructorParameterDescription(self.urls)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case recentMeUrls(Cons_recentMeUrls) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .recentMeUrls(let _data): - if boxed { - buffer.appendInt32(235081943) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.urls.count)) - for item in _data.urls { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .recentMeUrls(let _data): - return ("recentMeUrls", [("urls", ConstructorParameterDescription(_data.urls)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_recentMeUrls(_ reader: BufferReader) -> RecentMeUrls? { - var _1: [Api.RecentMeUrl]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RecentMeUrl.self) - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.help.RecentMeUrls.recentMeUrls(Cons_recentMeUrls(urls: _1!, chats: _2!, users: _3!)) - } - else { - return nil - } - } - } -} -public extension Api.help { - enum Support: TypeConstructorDescription { - public class Cons_support: TypeConstructorDescription { - public var phoneNumber: String - public var user: Api.User - public init(phoneNumber: String, user: Api.User) { - self.phoneNumber = phoneNumber - self.user = user - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("support", [("phoneNumber", ConstructorParameterDescription(self.phoneNumber)), ("user", ConstructorParameterDescription(self.user))]) - } - } - case support(Cons_support) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .support(let _data): - if boxed { - buffer.appendInt32(398898678) - } - serializeString(_data.phoneNumber, buffer: buffer, boxed: false) - _data.user.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .support(let _data): - return ("support", [("phoneNumber", ConstructorParameterDescription(_data.phoneNumber)), ("user", ConstructorParameterDescription(_data.user))]) - } - } - - public static func parse_support(_ reader: BufferReader) -> Support? { - var _1: String? - _1 = parseString(reader) - var _2: Api.User? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.User - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.help.Support.support(Cons_support(phoneNumber: _1!, user: _2!)) - } - else { - return nil - } - } - } -} -public extension Api.help { - enum SupportName: TypeConstructorDescription { - public class Cons_supportName: TypeConstructorDescription { - public var name: String - public init(name: String) { - self.name = name - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("supportName", [("name", ConstructorParameterDescription(self.name))]) - } - } - case supportName(Cons_supportName) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .supportName(let _data): - if boxed { - buffer.appendInt32(-1945767479) - } - serializeString(_data.name, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .supportName(let _data): - return ("supportName", [("name", ConstructorParameterDescription(_data.name))]) - } - } - - public static func parse_supportName(_ reader: BufferReader) -> SupportName? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.help.SupportName.supportName(Cons_supportName(name: _1!)) - } - else { - return nil - } - } - } -} -public extension Api.help { - enum TermsOfService: TypeConstructorDescription { - public class Cons_termsOfService: TypeConstructorDescription { - public var flags: Int32 - public var id: Api.DataJSON - public var text: String - public var entities: [Api.MessageEntity] - public var minAgeConfirm: Int32? - public init(flags: Int32, id: Api.DataJSON, text: String, entities: [Api.MessageEntity], minAgeConfirm: Int32?) { - self.flags = flags - self.id = id - self.text = text - self.entities = entities - self.minAgeConfirm = minAgeConfirm - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("termsOfService", [("flags", ConstructorParameterDescription(self.flags)), ("id", ConstructorParameterDescription(self.id)), ("text", ConstructorParameterDescription(self.text)), ("entities", ConstructorParameterDescription(self.entities)), ("minAgeConfirm", ConstructorParameterDescription(self.minAgeConfirm))]) - } - } - case termsOfService(Cons_termsOfService) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .termsOfService(let _data): - if boxed { - buffer.appendInt32(2013922064) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - _data.id.serialize(buffer, true) - serializeString(_data.text, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.entities.count)) - for item in _data.entities { - item.serialize(buffer, true) - } - if Int(_data.flags) & Int(1 << 1) != 0 { - serializeInt32(_data.minAgeConfirm!, buffer: buffer, boxed: false) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .termsOfService(let _data): - return ("termsOfService", [("flags", ConstructorParameterDescription(_data.flags)), ("id", ConstructorParameterDescription(_data.id)), ("text", ConstructorParameterDescription(_data.text)), ("entities", ConstructorParameterDescription(_data.entities)), ("minAgeConfirm", ConstructorParameterDescription(_data.minAgeConfirm))]) - } - } - - public static func parse_termsOfService(_ reader: BufferReader) -> TermsOfService? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.DataJSON? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.DataJSON - } - var _3: String? - _3 = parseString(reader) - var _4: [Api.MessageEntity]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } - var _5: Int32? - if Int(_1!) & Int(1 << 1) != 0 { - _5 = reader.readInt32() - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.help.TermsOfService.termsOfService(Cons_termsOfService(flags: _1!, id: _2!, text: _3!, entities: _4!, minAgeConfirm: _5)) - } - else { - return nil - } - } - } -} -public extension Api.help { - enum TermsOfServiceUpdate: TypeConstructorDescription { - public class Cons_termsOfServiceUpdate: TypeConstructorDescription { - public var expires: Int32 - public var termsOfService: Api.help.TermsOfService - public init(expires: Int32, termsOfService: Api.help.TermsOfService) { - self.expires = expires - self.termsOfService = termsOfService - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("termsOfServiceUpdate", [("expires", ConstructorParameterDescription(self.expires)), ("termsOfService", ConstructorParameterDescription(self.termsOfService))]) - } - } - public class Cons_termsOfServiceUpdateEmpty: TypeConstructorDescription { - public var expires: Int32 - public init(expires: Int32) { - self.expires = expires - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("termsOfServiceUpdateEmpty", [("expires", ConstructorParameterDescription(self.expires))]) - } - } - case termsOfServiceUpdate(Cons_termsOfServiceUpdate) - case termsOfServiceUpdateEmpty(Cons_termsOfServiceUpdateEmpty) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .termsOfServiceUpdate(let _data): - if boxed { - buffer.appendInt32(686618977) - } - serializeInt32(_data.expires, buffer: buffer, boxed: false) - _data.termsOfService.serialize(buffer, true) - break - case .termsOfServiceUpdateEmpty(let _data): - if boxed { - buffer.appendInt32(-483352705) - } - serializeInt32(_data.expires, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .termsOfServiceUpdate(let _data): - return ("termsOfServiceUpdate", [("expires", ConstructorParameterDescription(_data.expires)), ("termsOfService", ConstructorParameterDescription(_data.termsOfService))]) - case .termsOfServiceUpdateEmpty(let _data): - return ("termsOfServiceUpdateEmpty", [("expires", ConstructorParameterDescription(_data.expires))]) - } - } - - public static func parse_termsOfServiceUpdate(_ reader: BufferReader) -> TermsOfServiceUpdate? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.help.TermsOfService? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.help.TermsOfService - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.help.TermsOfServiceUpdate.termsOfServiceUpdate(Cons_termsOfServiceUpdate(expires: _1!, termsOfService: _2!)) - } - else { - return nil - } - } - public static func parse_termsOfServiceUpdateEmpty(_ reader: BufferReader) -> TermsOfServiceUpdate? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.help.TermsOfServiceUpdate.termsOfServiceUpdateEmpty(Cons_termsOfServiceUpdateEmpty(expires: _1!)) - } - else { - return nil - } - } - } -} -public extension Api.help { - enum TimezonesList: TypeConstructorDescription { - public class Cons_timezonesList: TypeConstructorDescription { - public var timezones: [Api.Timezone] - public var hash: Int32 - public init(timezones: [Api.Timezone], hash: Int32) { - self.timezones = timezones - self.hash = hash - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("timezonesList", [("timezones", ConstructorParameterDescription(self.timezones)), ("hash", ConstructorParameterDescription(self.hash))]) - } - } - case timezonesList(Cons_timezonesList) - case timezonesListNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .timezonesList(let _data): - if boxed { - buffer.appendInt32(2071260529) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.timezones.count)) - for item in _data.timezones { - item.serialize(buffer, true) - } - serializeInt32(_data.hash, buffer: buffer, boxed: false) - break - case .timezonesListNotModified: - if boxed { - buffer.appendInt32(-1761146676) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .timezonesList(let _data): - return ("timezonesList", [("timezones", ConstructorParameterDescription(_data.timezones)), ("hash", ConstructorParameterDescription(_data.hash))]) - case .timezonesListNotModified: - return ("timezonesListNotModified", []) - } - } - - public static func parse_timezonesList(_ reader: BufferReader) -> TimezonesList? { - var _1: [Api.Timezone]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Timezone.self) - } - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.help.TimezonesList.timezonesList(Cons_timezonesList(timezones: _1!, hash: _2!)) - } - else { - return nil - } - } - public static func parse_timezonesListNotModified(_ reader: BufferReader) -> TimezonesList? { - return Api.help.TimezonesList.timezonesListNotModified - } - } -} -public extension Api.help { - enum UserInfo: TypeConstructorDescription { - public class Cons_userInfo: TypeConstructorDescription { - public var message: String - public var entities: [Api.MessageEntity] - public var author: String - public var date: Int32 - public init(message: String, entities: [Api.MessageEntity], author: String, date: Int32) { - self.message = message - self.entities = entities - self.author = author - self.date = date - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("userInfo", [("message", ConstructorParameterDescription(self.message)), ("entities", ConstructorParameterDescription(self.entities)), ("author", ConstructorParameterDescription(self.author)), ("date", ConstructorParameterDescription(self.date))]) - } - } - case userInfo(Cons_userInfo) - case userInfoEmpty - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .userInfo(let _data): - if boxed { - buffer.appendInt32(32192344) - } - serializeString(_data.message, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.entities.count)) - for item in _data.entities { - item.serialize(buffer, true) - } - serializeString(_data.author, buffer: buffer, boxed: false) - serializeInt32(_data.date, buffer: buffer, boxed: false) - break - case .userInfoEmpty: - if boxed { - buffer.appendInt32(-206688531) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .userInfo(let _data): - return ("userInfo", [("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("author", ConstructorParameterDescription(_data.author)), ("date", ConstructorParameterDescription(_data.date))]) - case .userInfoEmpty: - return ("userInfoEmpty", []) - } - } - - public static func parse_userInfo(_ reader: BufferReader) -> UserInfo? { - var _1: String? - _1 = parseString(reader) - var _2: [Api.MessageEntity]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } - var _3: String? - _3 = parseString(reader) - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.help.UserInfo.userInfo(Cons_userInfo(message: _1!, entities: _2!, author: _3!, date: _4!)) - } - else { - return nil - } - } - public static func parse_userInfoEmpty(_ reader: BufferReader) -> UserInfo? { - return Api.help.UserInfo.userInfoEmpty - } - } -} -public extension Api.messages { - enum AffectedFoundMessages: TypeConstructorDescription { - public class Cons_affectedFoundMessages: TypeConstructorDescription { - public var pts: Int32 - public var ptsCount: Int32 - public var offset: Int32 - public var messages: [Int32] - public init(pts: Int32, ptsCount: Int32, offset: Int32, messages: [Int32]) { - self.pts = pts - self.ptsCount = ptsCount - self.offset = offset - self.messages = messages - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("affectedFoundMessages", [("pts", ConstructorParameterDescription(self.pts)), ("ptsCount", ConstructorParameterDescription(self.ptsCount)), ("offset", ConstructorParameterDescription(self.offset)), ("messages", ConstructorParameterDescription(self.messages))]) - } - } - case affectedFoundMessages(Cons_affectedFoundMessages) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .affectedFoundMessages(let _data): - if boxed { - buffer.appendInt32(-275956116) - } - serializeInt32(_data.pts, buffer: buffer, boxed: false) - serializeInt32(_data.ptsCount, buffer: buffer, boxed: false) - serializeInt32(_data.offset, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.messages.count)) - for item in _data.messages { - serializeInt32(item, buffer: buffer, boxed: false) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .affectedFoundMessages(let _data): - return ("affectedFoundMessages", [("pts", ConstructorParameterDescription(_data.pts)), ("ptsCount", ConstructorParameterDescription(_data.ptsCount)), ("offset", ConstructorParameterDescription(_data.offset)), ("messages", ConstructorParameterDescription(_data.messages))]) - } - } - - public static func parse_affectedFoundMessages(_ reader: BufferReader) -> AffectedFoundMessages? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: [Int32]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.AffectedFoundMessages.affectedFoundMessages(Cons_affectedFoundMessages(pts: _1!, ptsCount: _2!, offset: _3!, messages: _4!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api34.swift b/submodules/TelegramApi/Sources/Api34.swift index f03ad72730..8423bd6add 100644 --- a/submodules/TelegramApi/Sources/Api34.swift +++ b/submodules/TelegramApi/Sources/Api34.swift @@ -1,3 +1,548 @@ +public extension Api.help { + enum RecentMeUrls: TypeConstructorDescription { + public class Cons_recentMeUrls: TypeConstructorDescription { + public var urls: [Api.RecentMeUrl] + public var chats: [Api.Chat] + public var users: [Api.User] + public init(urls: [Api.RecentMeUrl], chats: [Api.Chat], users: [Api.User]) { + self.urls = urls + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("recentMeUrls", [("urls", ConstructorParameterDescription(self.urls)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case recentMeUrls(Cons_recentMeUrls) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .recentMeUrls(let _data): + if boxed { + buffer.appendInt32(235081943) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.urls.count)) + for item in _data.urls { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .recentMeUrls(let _data): + return ("recentMeUrls", [("urls", ConstructorParameterDescription(_data.urls)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_recentMeUrls(_ reader: BufferReader) -> RecentMeUrls? { + var _1: [Api.RecentMeUrl]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RecentMeUrl.self) + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.help.RecentMeUrls.recentMeUrls(Cons_recentMeUrls(urls: _1!, chats: _2!, users: _3!)) + } + else { + return nil + } + } + } +} +public extension Api.help { + enum Support: TypeConstructorDescription { + public class Cons_support: TypeConstructorDescription { + public var phoneNumber: String + public var user: Api.User + public init(phoneNumber: String, user: Api.User) { + self.phoneNumber = phoneNumber + self.user = user + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("support", [("phoneNumber", ConstructorParameterDescription(self.phoneNumber)), ("user", ConstructorParameterDescription(self.user))]) + } + } + case support(Cons_support) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .support(let _data): + if boxed { + buffer.appendInt32(398898678) + } + serializeString(_data.phoneNumber, buffer: buffer, boxed: false) + _data.user.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .support(let _data): + return ("support", [("phoneNumber", ConstructorParameterDescription(_data.phoneNumber)), ("user", ConstructorParameterDescription(_data.user))]) + } + } + + public static func parse_support(_ reader: BufferReader) -> Support? { + var _1: String? + _1 = parseString(reader) + var _2: Api.User? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.User + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.help.Support.support(Cons_support(phoneNumber: _1!, user: _2!)) + } + else { + return nil + } + } + } +} +public extension Api.help { + enum SupportName: TypeConstructorDescription { + public class Cons_supportName: TypeConstructorDescription { + public var name: String + public init(name: String) { + self.name = name + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("supportName", [("name", ConstructorParameterDescription(self.name))]) + } + } + case supportName(Cons_supportName) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .supportName(let _data): + if boxed { + buffer.appendInt32(-1945767479) + } + serializeString(_data.name, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .supportName(let _data): + return ("supportName", [("name", ConstructorParameterDescription(_data.name))]) + } + } + + public static func parse_supportName(_ reader: BufferReader) -> SupportName? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.help.SupportName.supportName(Cons_supportName(name: _1!)) + } + else { + return nil + } + } + } +} +public extension Api.help { + enum TermsOfService: TypeConstructorDescription { + public class Cons_termsOfService: TypeConstructorDescription { + public var flags: Int32 + public var id: Api.DataJSON + public var text: String + public var entities: [Api.MessageEntity] + public var minAgeConfirm: Int32? + public init(flags: Int32, id: Api.DataJSON, text: String, entities: [Api.MessageEntity], minAgeConfirm: Int32?) { + self.flags = flags + self.id = id + self.text = text + self.entities = entities + self.minAgeConfirm = minAgeConfirm + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("termsOfService", [("flags", ConstructorParameterDescription(self.flags)), ("id", ConstructorParameterDescription(self.id)), ("text", ConstructorParameterDescription(self.text)), ("entities", ConstructorParameterDescription(self.entities)), ("minAgeConfirm", ConstructorParameterDescription(self.minAgeConfirm))]) + } + } + case termsOfService(Cons_termsOfService) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .termsOfService(let _data): + if boxed { + buffer.appendInt32(2013922064) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + _data.id.serialize(buffer, true) + serializeString(_data.text, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.entities.count)) + for item in _data.entities { + item.serialize(buffer, true) + } + if Int(_data.flags) & Int(1 << 1) != 0 { + serializeInt32(_data.minAgeConfirm!, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .termsOfService(let _data): + return ("termsOfService", [("flags", ConstructorParameterDescription(_data.flags)), ("id", ConstructorParameterDescription(_data.id)), ("text", ConstructorParameterDescription(_data.text)), ("entities", ConstructorParameterDescription(_data.entities)), ("minAgeConfirm", ConstructorParameterDescription(_data.minAgeConfirm))]) + } + } + + public static func parse_termsOfService(_ reader: BufferReader) -> TermsOfService? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.DataJSON? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.DataJSON + } + var _3: String? + _3 = parseString(reader) + var _4: [Api.MessageEntity]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } + var _5: Int32? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _5 = reader.readInt32() + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.help.TermsOfService.termsOfService(Cons_termsOfService(flags: _1!, id: _2!, text: _3!, entities: _4!, minAgeConfirm: _5)) + } + else { + return nil + } + } + } +} +public extension Api.help { + enum TermsOfServiceUpdate: TypeConstructorDescription { + public class Cons_termsOfServiceUpdate: TypeConstructorDescription { + public var expires: Int32 + public var termsOfService: Api.help.TermsOfService + public init(expires: Int32, termsOfService: Api.help.TermsOfService) { + self.expires = expires + self.termsOfService = termsOfService + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("termsOfServiceUpdate", [("expires", ConstructorParameterDescription(self.expires)), ("termsOfService", ConstructorParameterDescription(self.termsOfService))]) + } + } + public class Cons_termsOfServiceUpdateEmpty: TypeConstructorDescription { + public var expires: Int32 + public init(expires: Int32) { + self.expires = expires + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("termsOfServiceUpdateEmpty", [("expires", ConstructorParameterDescription(self.expires))]) + } + } + case termsOfServiceUpdate(Cons_termsOfServiceUpdate) + case termsOfServiceUpdateEmpty(Cons_termsOfServiceUpdateEmpty) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .termsOfServiceUpdate(let _data): + if boxed { + buffer.appendInt32(686618977) + } + serializeInt32(_data.expires, buffer: buffer, boxed: false) + _data.termsOfService.serialize(buffer, true) + break + case .termsOfServiceUpdateEmpty(let _data): + if boxed { + buffer.appendInt32(-483352705) + } + serializeInt32(_data.expires, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .termsOfServiceUpdate(let _data): + return ("termsOfServiceUpdate", [("expires", ConstructorParameterDescription(_data.expires)), ("termsOfService", ConstructorParameterDescription(_data.termsOfService))]) + case .termsOfServiceUpdateEmpty(let _data): + return ("termsOfServiceUpdateEmpty", [("expires", ConstructorParameterDescription(_data.expires))]) + } + } + + public static func parse_termsOfServiceUpdate(_ reader: BufferReader) -> TermsOfServiceUpdate? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.help.TermsOfService? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.help.TermsOfService + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.help.TermsOfServiceUpdate.termsOfServiceUpdate(Cons_termsOfServiceUpdate(expires: _1!, termsOfService: _2!)) + } + else { + return nil + } + } + public static func parse_termsOfServiceUpdateEmpty(_ reader: BufferReader) -> TermsOfServiceUpdate? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.help.TermsOfServiceUpdate.termsOfServiceUpdateEmpty(Cons_termsOfServiceUpdateEmpty(expires: _1!)) + } + else { + return nil + } + } + } +} +public extension Api.help { + enum TimezonesList: TypeConstructorDescription { + public class Cons_timezonesList: TypeConstructorDescription { + public var timezones: [Api.Timezone] + public var hash: Int32 + public init(timezones: [Api.Timezone], hash: Int32) { + self.timezones = timezones + self.hash = hash + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("timezonesList", [("timezones", ConstructorParameterDescription(self.timezones)), ("hash", ConstructorParameterDescription(self.hash))]) + } + } + case timezonesList(Cons_timezonesList) + case timezonesListNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .timezonesList(let _data): + if boxed { + buffer.appendInt32(2071260529) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.timezones.count)) + for item in _data.timezones { + item.serialize(buffer, true) + } + serializeInt32(_data.hash, buffer: buffer, boxed: false) + break + case .timezonesListNotModified: + if boxed { + buffer.appendInt32(-1761146676) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .timezonesList(let _data): + return ("timezonesList", [("timezones", ConstructorParameterDescription(_data.timezones)), ("hash", ConstructorParameterDescription(_data.hash))]) + case .timezonesListNotModified: + return ("timezonesListNotModified", []) + } + } + + public static func parse_timezonesList(_ reader: BufferReader) -> TimezonesList? { + var _1: [Api.Timezone]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Timezone.self) + } + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.help.TimezonesList.timezonesList(Cons_timezonesList(timezones: _1!, hash: _2!)) + } + else { + return nil + } + } + public static func parse_timezonesListNotModified(_ reader: BufferReader) -> TimezonesList? { + return Api.help.TimezonesList.timezonesListNotModified + } + } +} +public extension Api.help { + enum UserInfo: TypeConstructorDescription { + public class Cons_userInfo: TypeConstructorDescription { + public var message: String + public var entities: [Api.MessageEntity] + public var author: String + public var date: Int32 + public init(message: String, entities: [Api.MessageEntity], author: String, date: Int32) { + self.message = message + self.entities = entities + self.author = author + self.date = date + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("userInfo", [("message", ConstructorParameterDescription(self.message)), ("entities", ConstructorParameterDescription(self.entities)), ("author", ConstructorParameterDescription(self.author)), ("date", ConstructorParameterDescription(self.date))]) + } + } + case userInfo(Cons_userInfo) + case userInfoEmpty + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .userInfo(let _data): + if boxed { + buffer.appendInt32(32192344) + } + serializeString(_data.message, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.entities.count)) + for item in _data.entities { + item.serialize(buffer, true) + } + serializeString(_data.author, buffer: buffer, boxed: false) + serializeInt32(_data.date, buffer: buffer, boxed: false) + break + case .userInfoEmpty: + if boxed { + buffer.appendInt32(-206688531) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .userInfo(let _data): + return ("userInfo", [("message", ConstructorParameterDescription(_data.message)), ("entities", ConstructorParameterDescription(_data.entities)), ("author", ConstructorParameterDescription(_data.author)), ("date", ConstructorParameterDescription(_data.date))]) + case .userInfoEmpty: + return ("userInfoEmpty", []) + } + } + + public static func parse_userInfo(_ reader: BufferReader) -> UserInfo? { + var _1: String? + _1 = parseString(reader) + var _2: [Api.MessageEntity]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } + var _3: String? + _3 = parseString(reader) + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.help.UserInfo.userInfo(Cons_userInfo(message: _1!, entities: _2!, author: _3!, date: _4!)) + } + else { + return nil + } + } + public static func parse_userInfoEmpty(_ reader: BufferReader) -> UserInfo? { + return Api.help.UserInfo.userInfoEmpty + } + } +} +public extension Api.messages { + enum AffectedFoundMessages: TypeConstructorDescription { + public class Cons_affectedFoundMessages: TypeConstructorDescription { + public var pts: Int32 + public var ptsCount: Int32 + public var offset: Int32 + public var messages: [Int32] + public init(pts: Int32, ptsCount: Int32, offset: Int32, messages: [Int32]) { + self.pts = pts + self.ptsCount = ptsCount + self.offset = offset + self.messages = messages + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("affectedFoundMessages", [("pts", ConstructorParameterDescription(self.pts)), ("ptsCount", ConstructorParameterDescription(self.ptsCount)), ("offset", ConstructorParameterDescription(self.offset)), ("messages", ConstructorParameterDescription(self.messages))]) + } + } + case affectedFoundMessages(Cons_affectedFoundMessages) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .affectedFoundMessages(let _data): + if boxed { + buffer.appendInt32(-275956116) + } + serializeInt32(_data.pts, buffer: buffer, boxed: false) + serializeInt32(_data.ptsCount, buffer: buffer, boxed: false) + serializeInt32(_data.offset, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.messages.count)) + for item in _data.messages { + serializeInt32(item, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .affectedFoundMessages(let _data): + return ("affectedFoundMessages", [("pts", ConstructorParameterDescription(_data.pts)), ("ptsCount", ConstructorParameterDescription(_data.ptsCount)), ("offset", ConstructorParameterDescription(_data.offset)), ("messages", ConstructorParameterDescription(_data.messages))]) + } + } + + public static func parse_affectedFoundMessages(_ reader: BufferReader) -> AffectedFoundMessages? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: [Int32]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.AffectedFoundMessages.affectedFoundMessages(Cons_affectedFoundMessages(pts: _1!, ptsCount: _2!, offset: _3!, messages: _4!)) + } + else { + return nil + } + } + } +} public extension Api.messages { enum AffectedHistory: TypeConstructorDescription { public class Cons_affectedHistory: TypeConstructorDescription { @@ -473,18 +1018,18 @@ public extension Api.messages { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = parseString(reader) } var _3: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _3 = parseString(reader) } var _4: Int32? _4 = reader.readInt32() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 2) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.messages.BotCallbackAnswer.botCallbackAnswer(Cons_botCallbackAnswer(flags: _1!, message: _2, url: _3, cacheTime: _4!)) @@ -617,17 +1162,17 @@ public extension Api.messages { var _2: Int64? _2 = reader.readInt64() var _3: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = parseString(reader) } var _4: Api.InlineBotSwitchPM? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.InlineBotSwitchPM } } var _5: Api.InlineBotWebView? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.InlineBotWebView } @@ -644,9 +1189,9 @@ public extension Api.messages { } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil @@ -1041,14 +1586,14 @@ public extension Api.messages { _2 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } var _3: Api.TextWithEntities? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.messages.ComposedMessageWithAI.composedMessageWithAI(Cons_composedMessageWithAI(flags: _1!, resultText: _2!, diffText: _3)) } @@ -1149,585 +1694,3 @@ public extension Api.messages { } } } -public extension Api.messages { - enum DialogFilters: TypeConstructorDescription { - public class Cons_dialogFilters: TypeConstructorDescription { - public var flags: Int32 - public var filters: [Api.DialogFilter] - public init(flags: Int32, filters: [Api.DialogFilter]) { - self.flags = flags - self.filters = filters - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("dialogFilters", [("flags", ConstructorParameterDescription(self.flags)), ("filters", ConstructorParameterDescription(self.filters))]) - } - } - case dialogFilters(Cons_dialogFilters) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .dialogFilters(let _data): - if boxed { - buffer.appendInt32(718878489) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.filters.count)) - for item in _data.filters { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .dialogFilters(let _data): - return ("dialogFilters", [("flags", ConstructorParameterDescription(_data.flags)), ("filters", ConstructorParameterDescription(_data.filters))]) - } - } - - public static func parse_dialogFilters(_ reader: BufferReader) -> DialogFilters? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.DialogFilter]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DialogFilter.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.DialogFilters.dialogFilters(Cons_dialogFilters(flags: _1!, filters: _2!)) - } - else { - return nil - } - } - } -} -public extension Api.messages { - enum Dialogs: TypeConstructorDescription { - public class Cons_dialogs: TypeConstructorDescription { - public var dialogs: [Api.Dialog] - public var messages: [Api.Message] - public var chats: [Api.Chat] - public var users: [Api.User] - public init(dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) { - self.dialogs = dialogs - self.messages = messages - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("dialogs", [("dialogs", ConstructorParameterDescription(self.dialogs)), ("messages", ConstructorParameterDescription(self.messages)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - public class Cons_dialogsNotModified: TypeConstructorDescription { - public var count: Int32 - public init(count: Int32) { - self.count = count - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("dialogsNotModified", [("count", ConstructorParameterDescription(self.count))]) - } - } - public class Cons_dialogsSlice: TypeConstructorDescription { - public var count: Int32 - public var dialogs: [Api.Dialog] - public var messages: [Api.Message] - public var chats: [Api.Chat] - public var users: [Api.User] - public init(count: Int32, dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) { - self.count = count - self.dialogs = dialogs - self.messages = messages - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("dialogsSlice", [("count", ConstructorParameterDescription(self.count)), ("dialogs", ConstructorParameterDescription(self.dialogs)), ("messages", ConstructorParameterDescription(self.messages)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case dialogs(Cons_dialogs) - case dialogsNotModified(Cons_dialogsNotModified) - case dialogsSlice(Cons_dialogsSlice) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .dialogs(let _data): - if boxed { - buffer.appendInt32(364538944) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.dialogs.count)) - for item in _data.dialogs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.messages.count)) - for item in _data.messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - case .dialogsNotModified(let _data): - if boxed { - buffer.appendInt32(-253500010) - } - serializeInt32(_data.count, buffer: buffer, boxed: false) - break - case .dialogsSlice(let _data): - if boxed { - buffer.appendInt32(1910543603) - } - serializeInt32(_data.count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.dialogs.count)) - for item in _data.dialogs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.messages.count)) - for item in _data.messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .dialogs(let _data): - return ("dialogs", [("dialogs", ConstructorParameterDescription(_data.dialogs)), ("messages", ConstructorParameterDescription(_data.messages)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - case .dialogsNotModified(let _data): - return ("dialogsNotModified", [("count", ConstructorParameterDescription(_data.count))]) - case .dialogsSlice(let _data): - return ("dialogsSlice", [("count", ConstructorParameterDescription(_data.count)), ("dialogs", ConstructorParameterDescription(_data.dialogs)), ("messages", ConstructorParameterDescription(_data.messages)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_dialogs(_ reader: BufferReader) -> Dialogs? { - var _1: [Api.Dialog]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) - } - var _2: [Api.Message]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _3: [Api.Chat]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _4: [Api.User]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.Dialogs.dialogs(Cons_dialogs(dialogs: _1!, messages: _2!, chats: _3!, users: _4!)) - } - else { - return nil - } - } - public static func parse_dialogsNotModified(_ reader: BufferReader) -> Dialogs? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.messages.Dialogs.dialogsNotModified(Cons_dialogsNotModified(count: _1!)) - } - else { - return nil - } - } - public static func parse_dialogsSlice(_ reader: BufferReader) -> Dialogs? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.Dialog]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) - } - var _3: [Api.Message]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.messages.Dialogs.dialogsSlice(Cons_dialogsSlice(count: _1!, dialogs: _2!, messages: _3!, chats: _4!, users: _5!)) - } - else { - return nil - } - } - } -} -public extension Api.messages { - enum DiscussionMessage: TypeConstructorDescription { - public class Cons_discussionMessage: TypeConstructorDescription { - public var flags: Int32 - public var messages: [Api.Message] - public var maxId: Int32? - public var readInboxMaxId: Int32? - public var readOutboxMaxId: Int32? - public var unreadCount: Int32 - public var chats: [Api.Chat] - public var users: [Api.User] - public init(flags: Int32, messages: [Api.Message], maxId: Int32?, readInboxMaxId: Int32?, readOutboxMaxId: Int32?, unreadCount: Int32, chats: [Api.Chat], users: [Api.User]) { - self.flags = flags - self.messages = messages - self.maxId = maxId - self.readInboxMaxId = readInboxMaxId - self.readOutboxMaxId = readOutboxMaxId - self.unreadCount = unreadCount - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("discussionMessage", [("flags", ConstructorParameterDescription(self.flags)), ("messages", ConstructorParameterDescription(self.messages)), ("maxId", ConstructorParameterDescription(self.maxId)), ("readInboxMaxId", ConstructorParameterDescription(self.readInboxMaxId)), ("readOutboxMaxId", ConstructorParameterDescription(self.readOutboxMaxId)), ("unreadCount", ConstructorParameterDescription(self.unreadCount)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case discussionMessage(Cons_discussionMessage) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .discussionMessage(let _data): - if boxed { - buffer.appendInt32(-1506535550) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.messages.count)) - for item in _data.messages { - item.serialize(buffer, true) - } - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeInt32(_data.maxId!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 1) != 0 { - serializeInt32(_data.readInboxMaxId!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 2) != 0 { - serializeInt32(_data.readOutboxMaxId!, buffer: buffer, boxed: false) - } - serializeInt32(_data.unreadCount, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .discussionMessage(let _data): - return ("discussionMessage", [("flags", ConstructorParameterDescription(_data.flags)), ("messages", ConstructorParameterDescription(_data.messages)), ("maxId", ConstructorParameterDescription(_data.maxId)), ("readInboxMaxId", ConstructorParameterDescription(_data.readInboxMaxId)), ("readOutboxMaxId", ConstructorParameterDescription(_data.readOutboxMaxId)), ("unreadCount", ConstructorParameterDescription(_data.unreadCount)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_discussionMessage(_ reader: BufferReader) -> DiscussionMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.Message]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { - _3 = reader.readInt32() - } - var _4: Int32? - if Int(_1!) & Int(1 << 1) != 0 { - _4 = reader.readInt32() - } - var _5: Int32? - if Int(_1!) & Int(1 << 2) != 0 { - _5 = reader.readInt32() - } - var _6: Int32? - _6 = reader.readInt32() - var _7: [Api.Chat]? - if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _8: [Api.User]? - if let _ = reader.readInt32() { - _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.messages.DiscussionMessage.discussionMessage(Cons_discussionMessage(flags: _1!, messages: _2!, maxId: _3, readInboxMaxId: _4, readOutboxMaxId: _5, unreadCount: _6!, chats: _7!, users: _8!)) - } - else { - return nil - } - } - } -} -public extension Api.messages { - enum EmojiGameInfo: TypeConstructorDescription { - public class Cons_emojiGameDiceInfo: TypeConstructorDescription { - public var flags: Int32 - public var gameHash: String - public var prevStake: Int64 - public var currentStreak: Int32 - public var params: [Int32] - public var playsLeft: Int32? - public init(flags: Int32, gameHash: String, prevStake: Int64, currentStreak: Int32, params: [Int32], playsLeft: Int32?) { - self.flags = flags - self.gameHash = gameHash - self.prevStake = prevStake - self.currentStreak = currentStreak - self.params = params - self.playsLeft = playsLeft - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("emojiGameDiceInfo", [("flags", ConstructorParameterDescription(self.flags)), ("gameHash", ConstructorParameterDescription(self.gameHash)), ("prevStake", ConstructorParameterDescription(self.prevStake)), ("currentStreak", ConstructorParameterDescription(self.currentStreak)), ("params", ConstructorParameterDescription(self.params)), ("playsLeft", ConstructorParameterDescription(self.playsLeft))]) - } - } - case emojiGameDiceInfo(Cons_emojiGameDiceInfo) - case emojiGameUnavailable - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .emojiGameDiceInfo(let _data): - if boxed { - buffer.appendInt32(1155883043) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeString(_data.gameHash, buffer: buffer, boxed: false) - serializeInt64(_data.prevStake, buffer: buffer, boxed: false) - serializeInt32(_data.currentStreak, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.params.count)) - for item in _data.params { - serializeInt32(item, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeInt32(_data.playsLeft!, buffer: buffer, boxed: false) - } - break - case .emojiGameUnavailable: - if boxed { - buffer.appendInt32(1508266805) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .emojiGameDiceInfo(let _data): - return ("emojiGameDiceInfo", [("flags", ConstructorParameterDescription(_data.flags)), ("gameHash", ConstructorParameterDescription(_data.gameHash)), ("prevStake", ConstructorParameterDescription(_data.prevStake)), ("currentStreak", ConstructorParameterDescription(_data.currentStreak)), ("params", ConstructorParameterDescription(_data.params)), ("playsLeft", ConstructorParameterDescription(_data.playsLeft))]) - case .emojiGameUnavailable: - return ("emojiGameUnavailable", []) - } - } - - public static func parse_emojiGameDiceInfo(_ reader: BufferReader) -> EmojiGameInfo? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: Int64? - _3 = reader.readInt64() - var _4: Int32? - _4 = reader.readInt32() - var _5: [Int32]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } - var _6: Int32? - if Int(_1!) & Int(1 << 0) != 0 { - _6 = reader.readInt32() - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.messages.EmojiGameInfo.emojiGameDiceInfo(Cons_emojiGameDiceInfo(flags: _1!, gameHash: _2!, prevStake: _3!, currentStreak: _4!, params: _5!, playsLeft: _6)) - } - else { - return nil - } - } - public static func parse_emojiGameUnavailable(_ reader: BufferReader) -> EmojiGameInfo? { - return Api.messages.EmojiGameInfo.emojiGameUnavailable - } - } -} -public extension Api.messages { - enum EmojiGameOutcome: TypeConstructorDescription { - public class Cons_emojiGameOutcome: TypeConstructorDescription { - public var seed: Buffer - public var stakeTonAmount: Int64 - public var tonAmount: Int64 - public init(seed: Buffer, stakeTonAmount: Int64, tonAmount: Int64) { - self.seed = seed - self.stakeTonAmount = stakeTonAmount - self.tonAmount = tonAmount - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("emojiGameOutcome", [("seed", ConstructorParameterDescription(self.seed)), ("stakeTonAmount", ConstructorParameterDescription(self.stakeTonAmount)), ("tonAmount", ConstructorParameterDescription(self.tonAmount))]) - } - } - case emojiGameOutcome(Cons_emojiGameOutcome) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .emojiGameOutcome(let _data): - if boxed { - buffer.appendInt32(-634726841) - } - serializeBytes(_data.seed, buffer: buffer, boxed: false) - serializeInt64(_data.stakeTonAmount, buffer: buffer, boxed: false) - serializeInt64(_data.tonAmount, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .emojiGameOutcome(let _data): - return ("emojiGameOutcome", [("seed", ConstructorParameterDescription(_data.seed)), ("stakeTonAmount", ConstructorParameterDescription(_data.stakeTonAmount)), ("tonAmount", ConstructorParameterDescription(_data.tonAmount))]) - } - } - - public static func parse_emojiGameOutcome(_ reader: BufferReader) -> EmojiGameOutcome? { - var _1: Buffer? - _1 = parseBytes(reader) - var _2: Int64? - _2 = reader.readInt64() - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.EmojiGameOutcome.emojiGameOutcome(Cons_emojiGameOutcome(seed: _1!, stakeTonAmount: _2!, tonAmount: _3!)) - } - else { - return nil - } - } - } -} -public extension Api.messages { - enum EmojiGroups: TypeConstructorDescription { - public class Cons_emojiGroups: TypeConstructorDescription { - public var hash: Int32 - public var groups: [Api.EmojiGroup] - public init(hash: Int32, groups: [Api.EmojiGroup]) { - self.hash = hash - self.groups = groups - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("emojiGroups", [("hash", ConstructorParameterDescription(self.hash)), ("groups", ConstructorParameterDescription(self.groups))]) - } - } - case emojiGroups(Cons_emojiGroups) - case emojiGroupsNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .emojiGroups(let _data): - if boxed { - buffer.appendInt32(-2011186869) - } - serializeInt32(_data.hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.groups.count)) - for item in _data.groups { - item.serialize(buffer, true) - } - break - case .emojiGroupsNotModified: - if boxed { - buffer.appendInt32(1874111879) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .emojiGroups(let _data): - return ("emojiGroups", [("hash", ConstructorParameterDescription(_data.hash)), ("groups", ConstructorParameterDescription(_data.groups))]) - case .emojiGroupsNotModified: - return ("emojiGroupsNotModified", []) - } - } - - public static func parse_emojiGroups(_ reader: BufferReader) -> EmojiGroups? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.EmojiGroup]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.EmojiGroup.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.EmojiGroups.emojiGroups(Cons_emojiGroups(hash: _1!, groups: _2!)) - } - else { - return nil - } - } - public static func parse_emojiGroupsNotModified(_ reader: BufferReader) -> EmojiGroups? { - return Api.messages.EmojiGroups.emojiGroupsNotModified - } - } -} diff --git a/submodules/TelegramApi/Sources/Api35.swift b/submodules/TelegramApi/Sources/Api35.swift index 15431344d4..202d0b3a10 100644 --- a/submodules/TelegramApi/Sources/Api35.swift +++ b/submodules/TelegramApi/Sources/Api35.swift @@ -1,3 +1,585 @@ +public extension Api.messages { + enum DialogFilters: TypeConstructorDescription { + public class Cons_dialogFilters: TypeConstructorDescription { + public var flags: Int32 + public var filters: [Api.DialogFilter] + public init(flags: Int32, filters: [Api.DialogFilter]) { + self.flags = flags + self.filters = filters + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("dialogFilters", [("flags", ConstructorParameterDescription(self.flags)), ("filters", ConstructorParameterDescription(self.filters))]) + } + } + case dialogFilters(Cons_dialogFilters) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .dialogFilters(let _data): + if boxed { + buffer.appendInt32(718878489) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.filters.count)) + for item in _data.filters { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .dialogFilters(let _data): + return ("dialogFilters", [("flags", ConstructorParameterDescription(_data.flags)), ("filters", ConstructorParameterDescription(_data.filters))]) + } + } + + public static func parse_dialogFilters(_ reader: BufferReader) -> DialogFilters? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.DialogFilter]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.DialogFilter.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.DialogFilters.dialogFilters(Cons_dialogFilters(flags: _1!, filters: _2!)) + } + else { + return nil + } + } + } +} +public extension Api.messages { + enum Dialogs: TypeConstructorDescription { + public class Cons_dialogs: TypeConstructorDescription { + public var dialogs: [Api.Dialog] + public var messages: [Api.Message] + public var chats: [Api.Chat] + public var users: [Api.User] + public init(dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) { + self.dialogs = dialogs + self.messages = messages + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("dialogs", [("dialogs", ConstructorParameterDescription(self.dialogs)), ("messages", ConstructorParameterDescription(self.messages)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + public class Cons_dialogsNotModified: TypeConstructorDescription { + public var count: Int32 + public init(count: Int32) { + self.count = count + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("dialogsNotModified", [("count", ConstructorParameterDescription(self.count))]) + } + } + public class Cons_dialogsSlice: TypeConstructorDescription { + public var count: Int32 + public var dialogs: [Api.Dialog] + public var messages: [Api.Message] + public var chats: [Api.Chat] + public var users: [Api.User] + public init(count: Int32, dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) { + self.count = count + self.dialogs = dialogs + self.messages = messages + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("dialogsSlice", [("count", ConstructorParameterDescription(self.count)), ("dialogs", ConstructorParameterDescription(self.dialogs)), ("messages", ConstructorParameterDescription(self.messages)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case dialogs(Cons_dialogs) + case dialogsNotModified(Cons_dialogsNotModified) + case dialogsSlice(Cons_dialogsSlice) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .dialogs(let _data): + if boxed { + buffer.appendInt32(364538944) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.dialogs.count)) + for item in _data.dialogs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.messages.count)) + for item in _data.messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + case .dialogsNotModified(let _data): + if boxed { + buffer.appendInt32(-253500010) + } + serializeInt32(_data.count, buffer: buffer, boxed: false) + break + case .dialogsSlice(let _data): + if boxed { + buffer.appendInt32(1910543603) + } + serializeInt32(_data.count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.dialogs.count)) + for item in _data.dialogs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.messages.count)) + for item in _data.messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .dialogs(let _data): + return ("dialogs", [("dialogs", ConstructorParameterDescription(_data.dialogs)), ("messages", ConstructorParameterDescription(_data.messages)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + case .dialogsNotModified(let _data): + return ("dialogsNotModified", [("count", ConstructorParameterDescription(_data.count))]) + case .dialogsSlice(let _data): + return ("dialogsSlice", [("count", ConstructorParameterDescription(_data.count)), ("dialogs", ConstructorParameterDescription(_data.dialogs)), ("messages", ConstructorParameterDescription(_data.messages)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_dialogs(_ reader: BufferReader) -> Dialogs? { + var _1: [Api.Dialog]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) + } + var _2: [Api.Message]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _3: [Api.Chat]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.Dialogs.dialogs(Cons_dialogs(dialogs: _1!, messages: _2!, chats: _3!, users: _4!)) + } + else { + return nil + } + } + public static func parse_dialogsNotModified(_ reader: BufferReader) -> Dialogs? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.messages.Dialogs.dialogsNotModified(Cons_dialogsNotModified(count: _1!)) + } + else { + return nil + } + } + public static func parse_dialogsSlice(_ reader: BufferReader) -> Dialogs? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.Dialog]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) + } + var _3: [Api.Message]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.messages.Dialogs.dialogsSlice(Cons_dialogsSlice(count: _1!, dialogs: _2!, messages: _3!, chats: _4!, users: _5!)) + } + else { + return nil + } + } + } +} +public extension Api.messages { + enum DiscussionMessage: TypeConstructorDescription { + public class Cons_discussionMessage: TypeConstructorDescription { + public var flags: Int32 + public var messages: [Api.Message] + public var maxId: Int32? + public var readInboxMaxId: Int32? + public var readOutboxMaxId: Int32? + public var unreadCount: Int32 + public var chats: [Api.Chat] + public var users: [Api.User] + public init(flags: Int32, messages: [Api.Message], maxId: Int32?, readInboxMaxId: Int32?, readOutboxMaxId: Int32?, unreadCount: Int32, chats: [Api.Chat], users: [Api.User]) { + self.flags = flags + self.messages = messages + self.maxId = maxId + self.readInboxMaxId = readInboxMaxId + self.readOutboxMaxId = readOutboxMaxId + self.unreadCount = unreadCount + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("discussionMessage", [("flags", ConstructorParameterDescription(self.flags)), ("messages", ConstructorParameterDescription(self.messages)), ("maxId", ConstructorParameterDescription(self.maxId)), ("readInboxMaxId", ConstructorParameterDescription(self.readInboxMaxId)), ("readOutboxMaxId", ConstructorParameterDescription(self.readOutboxMaxId)), ("unreadCount", ConstructorParameterDescription(self.unreadCount)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case discussionMessage(Cons_discussionMessage) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .discussionMessage(let _data): + if boxed { + buffer.appendInt32(-1506535550) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.messages.count)) + for item in _data.messages { + item.serialize(buffer, true) + } + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeInt32(_data.maxId!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 1) != 0 { + serializeInt32(_data.readInboxMaxId!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 2) != 0 { + serializeInt32(_data.readOutboxMaxId!, buffer: buffer, boxed: false) + } + serializeInt32(_data.unreadCount, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .discussionMessage(let _data): + return ("discussionMessage", [("flags", ConstructorParameterDescription(_data.flags)), ("messages", ConstructorParameterDescription(_data.messages)), ("maxId", ConstructorParameterDescription(_data.maxId)), ("readInboxMaxId", ConstructorParameterDescription(_data.readInboxMaxId)), ("readOutboxMaxId", ConstructorParameterDescription(_data.readOutboxMaxId)), ("unreadCount", ConstructorParameterDescription(_data.unreadCount)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_discussionMessage(_ reader: BufferReader) -> DiscussionMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.Message]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _3: Int32? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _3 = reader.readInt32() + } + var _4: Int32? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _4 = reader.readInt32() + } + var _5: Int32? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _5 = reader.readInt32() + } + var _6: Int32? + _6 = reader.readInt32() + var _7: [Api.Chat]? + if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _8: [Api.User]? + if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.messages.DiscussionMessage.discussionMessage(Cons_discussionMessage(flags: _1!, messages: _2!, maxId: _3, readInboxMaxId: _4, readOutboxMaxId: _5, unreadCount: _6!, chats: _7!, users: _8!)) + } + else { + return nil + } + } + } +} +public extension Api.messages { + enum EmojiGameInfo: TypeConstructorDescription { + public class Cons_emojiGameDiceInfo: TypeConstructorDescription { + public var flags: Int32 + public var gameHash: String + public var prevStake: Int64 + public var currentStreak: Int32 + public var params: [Int32] + public var playsLeft: Int32? + public init(flags: Int32, gameHash: String, prevStake: Int64, currentStreak: Int32, params: [Int32], playsLeft: Int32?) { + self.flags = flags + self.gameHash = gameHash + self.prevStake = prevStake + self.currentStreak = currentStreak + self.params = params + self.playsLeft = playsLeft + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("emojiGameDiceInfo", [("flags", ConstructorParameterDescription(self.flags)), ("gameHash", ConstructorParameterDescription(self.gameHash)), ("prevStake", ConstructorParameterDescription(self.prevStake)), ("currentStreak", ConstructorParameterDescription(self.currentStreak)), ("params", ConstructorParameterDescription(self.params)), ("playsLeft", ConstructorParameterDescription(self.playsLeft))]) + } + } + case emojiGameDiceInfo(Cons_emojiGameDiceInfo) + case emojiGameUnavailable + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .emojiGameDiceInfo(let _data): + if boxed { + buffer.appendInt32(1155883043) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeString(_data.gameHash, buffer: buffer, boxed: false) + serializeInt64(_data.prevStake, buffer: buffer, boxed: false) + serializeInt32(_data.currentStreak, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.params.count)) + for item in _data.params { + serializeInt32(item, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeInt32(_data.playsLeft!, buffer: buffer, boxed: false) + } + break + case .emojiGameUnavailable: + if boxed { + buffer.appendInt32(1508266805) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .emojiGameDiceInfo(let _data): + return ("emojiGameDiceInfo", [("flags", ConstructorParameterDescription(_data.flags)), ("gameHash", ConstructorParameterDescription(_data.gameHash)), ("prevStake", ConstructorParameterDescription(_data.prevStake)), ("currentStreak", ConstructorParameterDescription(_data.currentStreak)), ("params", ConstructorParameterDescription(_data.params)), ("playsLeft", ConstructorParameterDescription(_data.playsLeft))]) + case .emojiGameUnavailable: + return ("emojiGameUnavailable", []) + } + } + + public static func parse_emojiGameDiceInfo(_ reader: BufferReader) -> EmojiGameInfo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() + var _4: Int32? + _4 = reader.readInt32() + var _5: [Int32]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } + var _6: Int32? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _6 = reader.readInt32() + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.messages.EmojiGameInfo.emojiGameDiceInfo(Cons_emojiGameDiceInfo(flags: _1!, gameHash: _2!, prevStake: _3!, currentStreak: _4!, params: _5!, playsLeft: _6)) + } + else { + return nil + } + } + public static func parse_emojiGameUnavailable(_ reader: BufferReader) -> EmojiGameInfo? { + return Api.messages.EmojiGameInfo.emojiGameUnavailable + } + } +} +public extension Api.messages { + enum EmojiGameOutcome: TypeConstructorDescription { + public class Cons_emojiGameOutcome: TypeConstructorDescription { + public var seed: Buffer + public var stakeTonAmount: Int64 + public var tonAmount: Int64 + public init(seed: Buffer, stakeTonAmount: Int64, tonAmount: Int64) { + self.seed = seed + self.stakeTonAmount = stakeTonAmount + self.tonAmount = tonAmount + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("emojiGameOutcome", [("seed", ConstructorParameterDescription(self.seed)), ("stakeTonAmount", ConstructorParameterDescription(self.stakeTonAmount)), ("tonAmount", ConstructorParameterDescription(self.tonAmount))]) + } + } + case emojiGameOutcome(Cons_emojiGameOutcome) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .emojiGameOutcome(let _data): + if boxed { + buffer.appendInt32(-634726841) + } + serializeBytes(_data.seed, buffer: buffer, boxed: false) + serializeInt64(_data.stakeTonAmount, buffer: buffer, boxed: false) + serializeInt64(_data.tonAmount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .emojiGameOutcome(let _data): + return ("emojiGameOutcome", [("seed", ConstructorParameterDescription(_data.seed)), ("stakeTonAmount", ConstructorParameterDescription(_data.stakeTonAmount)), ("tonAmount", ConstructorParameterDescription(_data.tonAmount))]) + } + } + + public static func parse_emojiGameOutcome(_ reader: BufferReader) -> EmojiGameOutcome? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int64? + _2 = reader.readInt64() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.EmojiGameOutcome.emojiGameOutcome(Cons_emojiGameOutcome(seed: _1!, stakeTonAmount: _2!, tonAmount: _3!)) + } + else { + return nil + } + } + } +} +public extension Api.messages { + enum EmojiGroups: TypeConstructorDescription { + public class Cons_emojiGroups: TypeConstructorDescription { + public var hash: Int32 + public var groups: [Api.EmojiGroup] + public init(hash: Int32, groups: [Api.EmojiGroup]) { + self.hash = hash + self.groups = groups + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("emojiGroups", [("hash", ConstructorParameterDescription(self.hash)), ("groups", ConstructorParameterDescription(self.groups))]) + } + } + case emojiGroups(Cons_emojiGroups) + case emojiGroupsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .emojiGroups(let _data): + if boxed { + buffer.appendInt32(-2011186869) + } + serializeInt32(_data.hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.groups.count)) + for item in _data.groups { + item.serialize(buffer, true) + } + break + case .emojiGroupsNotModified: + if boxed { + buffer.appendInt32(1874111879) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .emojiGroups(let _data): + return ("emojiGroups", [("hash", ConstructorParameterDescription(_data.hash)), ("groups", ConstructorParameterDescription(_data.groups))]) + case .emojiGroupsNotModified: + return ("emojiGroupsNotModified", []) + } + } + + public static func parse_emojiGroups(_ reader: BufferReader) -> EmojiGroups? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.EmojiGroup]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.EmojiGroup.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.EmojiGroups.emojiGroups(Cons_emojiGroups(hash: _1!, groups: _2!)) + } + else { + return nil + } + } + public static func parse_emojiGroupsNotModified(_ reader: BufferReader) -> EmojiGroups? { + return Api.messages.EmojiGroups.emojiGroupsNotModified + } + } +} public extension Api.messages { enum ExportedChatInvite: TypeConstructorDescription { public class Cons_exportedChatInvite: TypeConstructorDescription { @@ -607,7 +1189,7 @@ public extension Api.messages { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } var _3: Int64? @@ -617,7 +1199,7 @@ public extension Api.messages { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { @@ -631,11 +1213,11 @@ public extension Api.messages { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil if _c1 && _c2 { return Api.messages.FoundStickers.foundStickersNotModified(Cons_foundStickersNotModified(flags: _1!, nextOffset: _2)) } @@ -791,11 +1373,11 @@ public extension Api.messages { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _2 = parseString(reader) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _2 != nil if _c1 && _c2 { return Api.messages.HistoryImportParsed.historyImportParsed(Cons_historyImportParsed(flags: _1!, title: _2)) } @@ -1059,7 +1641,7 @@ public extension Api.messages { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } var _6: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = parseString(reader) } let _c1 = _1 != nil @@ -1067,7 +1649,7 @@ public extension Api.messages { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.messages.MessageReactionsList.messageReactionsList(Cons_messageReactionsList(flags: _1!, count: _2!, reactions: _3!, chats: _4!, users: _5!, nextOffset: _6)) } @@ -1354,7 +1936,7 @@ public extension Api.messages { var _3: Int32? _3 = reader.readInt32() var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = reader.readInt32() } var _5: [Api.Message]? @@ -1376,7 +1958,7 @@ public extension Api.messages { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil @@ -1433,15 +2015,15 @@ public extension Api.messages { var _2: Int32? _2 = reader.readInt32() var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = reader.readInt32() } var _5: Api.SearchPostsFlood? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.SearchPostsFlood } @@ -1464,9 +2046,9 @@ public extension Api.messages { } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil @@ -1480,469 +2062,3 @@ public extension Api.messages { } } } -public extension Api.messages { - enum MyStickers: TypeConstructorDescription { - public class Cons_myStickers: TypeConstructorDescription { - public var count: Int32 - public var sets: [Api.StickerSetCovered] - public init(count: Int32, sets: [Api.StickerSetCovered]) { - self.count = count - self.sets = sets - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("myStickers", [("count", ConstructorParameterDescription(self.count)), ("sets", ConstructorParameterDescription(self.sets))]) - } - } - case myStickers(Cons_myStickers) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .myStickers(let _data): - if boxed { - buffer.appendInt32(-83926371) - } - serializeInt32(_data.count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.sets.count)) - for item in _data.sets { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .myStickers(let _data): - return ("myStickers", [("count", ConstructorParameterDescription(_data.count)), ("sets", ConstructorParameterDescription(_data.sets))]) - } - } - - public static func parse_myStickers(_ reader: BufferReader) -> MyStickers? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.StickerSetCovered]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.MyStickers.myStickers(Cons_myStickers(count: _1!, sets: _2!)) - } - else { - return nil - } - } - } -} -public extension Api.messages { - enum PeerDialogs: TypeConstructorDescription { - public class Cons_peerDialogs: TypeConstructorDescription { - public var dialogs: [Api.Dialog] - public var messages: [Api.Message] - public var chats: [Api.Chat] - public var users: [Api.User] - public var state: Api.updates.State - public init(dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User], state: Api.updates.State) { - self.dialogs = dialogs - self.messages = messages - self.chats = chats - self.users = users - self.state = state - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("peerDialogs", [("dialogs", ConstructorParameterDescription(self.dialogs)), ("messages", ConstructorParameterDescription(self.messages)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users)), ("state", ConstructorParameterDescription(self.state))]) - } - } - case peerDialogs(Cons_peerDialogs) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .peerDialogs(let _data): - if boxed { - buffer.appendInt32(863093588) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.dialogs.count)) - for item in _data.dialogs { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.messages.count)) - for item in _data.messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - _data.state.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .peerDialogs(let _data): - return ("peerDialogs", [("dialogs", ConstructorParameterDescription(_data.dialogs)), ("messages", ConstructorParameterDescription(_data.messages)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users)), ("state", ConstructorParameterDescription(_data.state))]) - } - } - - public static func parse_peerDialogs(_ reader: BufferReader) -> PeerDialogs? { - var _1: [Api.Dialog]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) - } - var _2: [Api.Message]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _3: [Api.Chat]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _4: [Api.User]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - var _5: Api.updates.State? - if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.updates.State - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.messages.PeerDialogs.peerDialogs(Cons_peerDialogs(dialogs: _1!, messages: _2!, chats: _3!, users: _4!, state: _5!)) - } - else { - return nil - } - } - } -} -public extension Api.messages { - enum PeerSettings: TypeConstructorDescription { - public class Cons_peerSettings: TypeConstructorDescription { - public var settings: Api.PeerSettings - public var chats: [Api.Chat] - public var users: [Api.User] - public init(settings: Api.PeerSettings, chats: [Api.Chat], users: [Api.User]) { - self.settings = settings - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("peerSettings", [("settings", ConstructorParameterDescription(self.settings)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case peerSettings(Cons_peerSettings) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .peerSettings(let _data): - if boxed { - buffer.appendInt32(1753266509) - } - _data.settings.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .peerSettings(let _data): - return ("peerSettings", [("settings", ConstructorParameterDescription(_data.settings)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_peerSettings(_ reader: BufferReader) -> PeerSettings? { - var _1: Api.PeerSettings? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.PeerSettings - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.PeerSettings.peerSettings(Cons_peerSettings(settings: _1!, chats: _2!, users: _3!)) - } - else { - return nil - } - } - } -} -public extension Api.messages { - enum PreparedInlineMessage: TypeConstructorDescription { - public class Cons_preparedInlineMessage: TypeConstructorDescription { - public var queryId: Int64 - public var result: Api.BotInlineResult - public var peerTypes: [Api.InlineQueryPeerType] - public var cacheTime: Int32 - public var users: [Api.User] - public init(queryId: Int64, result: Api.BotInlineResult, peerTypes: [Api.InlineQueryPeerType], cacheTime: Int32, users: [Api.User]) { - self.queryId = queryId - self.result = result - self.peerTypes = peerTypes - self.cacheTime = cacheTime - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("preparedInlineMessage", [("queryId", ConstructorParameterDescription(self.queryId)), ("result", ConstructorParameterDescription(self.result)), ("peerTypes", ConstructorParameterDescription(self.peerTypes)), ("cacheTime", ConstructorParameterDescription(self.cacheTime)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case preparedInlineMessage(Cons_preparedInlineMessage) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .preparedInlineMessage(let _data): - if boxed { - buffer.appendInt32(-11046771) - } - serializeInt64(_data.queryId, buffer: buffer, boxed: false) - _data.result.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.peerTypes.count)) - for item in _data.peerTypes { - item.serialize(buffer, true) - } - serializeInt32(_data.cacheTime, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .preparedInlineMessage(let _data): - return ("preparedInlineMessage", [("queryId", ConstructorParameterDescription(_data.queryId)), ("result", ConstructorParameterDescription(_data.result)), ("peerTypes", ConstructorParameterDescription(_data.peerTypes)), ("cacheTime", ConstructorParameterDescription(_data.cacheTime)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_preparedInlineMessage(_ reader: BufferReader) -> PreparedInlineMessage? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Api.BotInlineResult? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.BotInlineResult - } - var _3: [Api.InlineQueryPeerType]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InlineQueryPeerType.self) - } - var _4: Int32? - _4 = reader.readInt32() - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.messages.PreparedInlineMessage.preparedInlineMessage(Cons_preparedInlineMessage(queryId: _1!, result: _2!, peerTypes: _3!, cacheTime: _4!, users: _5!)) - } - else { - return nil - } - } - } -} -public extension Api.messages { - enum QuickReplies: TypeConstructorDescription { - public class Cons_quickReplies: TypeConstructorDescription { - public var quickReplies: [Api.QuickReply] - public var messages: [Api.Message] - public var chats: [Api.Chat] - public var users: [Api.User] - public init(quickReplies: [Api.QuickReply], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) { - self.quickReplies = quickReplies - self.messages = messages - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("quickReplies", [("quickReplies", ConstructorParameterDescription(self.quickReplies)), ("messages", ConstructorParameterDescription(self.messages)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case quickReplies(Cons_quickReplies) - case quickRepliesNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .quickReplies(let _data): - if boxed { - buffer.appendInt32(-963811691) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.quickReplies.count)) - for item in _data.quickReplies { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.messages.count)) - for item in _data.messages { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - case .quickRepliesNotModified: - if boxed { - buffer.appendInt32(1603398491) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .quickReplies(let _data): - return ("quickReplies", [("quickReplies", ConstructorParameterDescription(_data.quickReplies)), ("messages", ConstructorParameterDescription(_data.messages)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - case .quickRepliesNotModified: - return ("quickRepliesNotModified", []) - } - } - - public static func parse_quickReplies(_ reader: BufferReader) -> QuickReplies? { - var _1: [Api.QuickReply]? - if let _ = reader.readInt32() { - _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.QuickReply.self) - } - var _2: [Api.Message]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) - } - var _3: [Api.Chat]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _4: [Api.User]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.messages.QuickReplies.quickReplies(Cons_quickReplies(quickReplies: _1!, messages: _2!, chats: _3!, users: _4!)) - } - else { - return nil - } - } - public static func parse_quickRepliesNotModified(_ reader: BufferReader) -> QuickReplies? { - return Api.messages.QuickReplies.quickRepliesNotModified - } - } -} -public extension Api.messages { - enum Reactions: TypeConstructorDescription { - public class Cons_reactions: TypeConstructorDescription { - public var hash: Int64 - public var reactions: [Api.Reaction] - public init(hash: Int64, reactions: [Api.Reaction]) { - self.hash = hash - self.reactions = reactions - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("reactions", [("hash", ConstructorParameterDescription(self.hash)), ("reactions", ConstructorParameterDescription(self.reactions))]) - } - } - case reactions(Cons_reactions) - case reactionsNotModified - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .reactions(let _data): - if boxed { - buffer.appendInt32(-352454890) - } - serializeInt64(_data.hash, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.reactions.count)) - for item in _data.reactions { - item.serialize(buffer, true) - } - break - case .reactionsNotModified: - if boxed { - buffer.appendInt32(-1334846497) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .reactions(let _data): - return ("reactions", [("hash", ConstructorParameterDescription(_data.hash)), ("reactions", ConstructorParameterDescription(_data.reactions))]) - case .reactionsNotModified: - return ("reactionsNotModified", []) - } - } - - public static func parse_reactions(_ reader: BufferReader) -> Reactions? { - var _1: Int64? - _1 = reader.readInt64() - var _2: [Api.Reaction]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Reaction.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.messages.Reactions.reactions(Cons_reactions(hash: _1!, reactions: _2!)) - } - else { - return nil - } - } - public static func parse_reactionsNotModified(_ reader: BufferReader) -> Reactions? { - return Api.messages.Reactions.reactionsNotModified - } - } -} diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 2ad050397d..1670f3226a 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -1,3 +1,469 @@ +public extension Api.messages { + enum MyStickers: TypeConstructorDescription { + public class Cons_myStickers: TypeConstructorDescription { + public var count: Int32 + public var sets: [Api.StickerSetCovered] + public init(count: Int32, sets: [Api.StickerSetCovered]) { + self.count = count + self.sets = sets + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("myStickers", [("count", ConstructorParameterDescription(self.count)), ("sets", ConstructorParameterDescription(self.sets))]) + } + } + case myStickers(Cons_myStickers) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .myStickers(let _data): + if boxed { + buffer.appendInt32(-83926371) + } + serializeInt32(_data.count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.sets.count)) + for item in _data.sets { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .myStickers(let _data): + return ("myStickers", [("count", ConstructorParameterDescription(_data.count)), ("sets", ConstructorParameterDescription(_data.sets))]) + } + } + + public static func parse_myStickers(_ reader: BufferReader) -> MyStickers? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.StickerSetCovered]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerSetCovered.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.MyStickers.myStickers(Cons_myStickers(count: _1!, sets: _2!)) + } + else { + return nil + } + } + } +} +public extension Api.messages { + enum PeerDialogs: TypeConstructorDescription { + public class Cons_peerDialogs: TypeConstructorDescription { + public var dialogs: [Api.Dialog] + public var messages: [Api.Message] + public var chats: [Api.Chat] + public var users: [Api.User] + public var state: Api.updates.State + public init(dialogs: [Api.Dialog], messages: [Api.Message], chats: [Api.Chat], users: [Api.User], state: Api.updates.State) { + self.dialogs = dialogs + self.messages = messages + self.chats = chats + self.users = users + self.state = state + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("peerDialogs", [("dialogs", ConstructorParameterDescription(self.dialogs)), ("messages", ConstructorParameterDescription(self.messages)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users)), ("state", ConstructorParameterDescription(self.state))]) + } + } + case peerDialogs(Cons_peerDialogs) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .peerDialogs(let _data): + if boxed { + buffer.appendInt32(863093588) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.dialogs.count)) + for item in _data.dialogs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.messages.count)) + for item in _data.messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + _data.state.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .peerDialogs(let _data): + return ("peerDialogs", [("dialogs", ConstructorParameterDescription(_data.dialogs)), ("messages", ConstructorParameterDescription(_data.messages)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users)), ("state", ConstructorParameterDescription(_data.state))]) + } + } + + public static func parse_peerDialogs(_ reader: BufferReader) -> PeerDialogs? { + var _1: [Api.Dialog]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Dialog.self) + } + var _2: [Api.Message]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _3: [Api.Chat]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _5: Api.updates.State? + if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.updates.State + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.messages.PeerDialogs.peerDialogs(Cons_peerDialogs(dialogs: _1!, messages: _2!, chats: _3!, users: _4!, state: _5!)) + } + else { + return nil + } + } + } +} +public extension Api.messages { + enum PeerSettings: TypeConstructorDescription { + public class Cons_peerSettings: TypeConstructorDescription { + public var settings: Api.PeerSettings + public var chats: [Api.Chat] + public var users: [Api.User] + public init(settings: Api.PeerSettings, chats: [Api.Chat], users: [Api.User]) { + self.settings = settings + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("peerSettings", [("settings", ConstructorParameterDescription(self.settings)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case peerSettings(Cons_peerSettings) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .peerSettings(let _data): + if boxed { + buffer.appendInt32(1753266509) + } + _data.settings.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .peerSettings(let _data): + return ("peerSettings", [("settings", ConstructorParameterDescription(_data.settings)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_peerSettings(_ reader: BufferReader) -> PeerSettings? { + var _1: Api.PeerSettings? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.PeerSettings + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.PeerSettings.peerSettings(Cons_peerSettings(settings: _1!, chats: _2!, users: _3!)) + } + else { + return nil + } + } + } +} +public extension Api.messages { + enum PreparedInlineMessage: TypeConstructorDescription { + public class Cons_preparedInlineMessage: TypeConstructorDescription { + public var queryId: Int64 + public var result: Api.BotInlineResult + public var peerTypes: [Api.InlineQueryPeerType] + public var cacheTime: Int32 + public var users: [Api.User] + public init(queryId: Int64, result: Api.BotInlineResult, peerTypes: [Api.InlineQueryPeerType], cacheTime: Int32, users: [Api.User]) { + self.queryId = queryId + self.result = result + self.peerTypes = peerTypes + self.cacheTime = cacheTime + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("preparedInlineMessage", [("queryId", ConstructorParameterDescription(self.queryId)), ("result", ConstructorParameterDescription(self.result)), ("peerTypes", ConstructorParameterDescription(self.peerTypes)), ("cacheTime", ConstructorParameterDescription(self.cacheTime)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case preparedInlineMessage(Cons_preparedInlineMessage) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .preparedInlineMessage(let _data): + if boxed { + buffer.appendInt32(-11046771) + } + serializeInt64(_data.queryId, buffer: buffer, boxed: false) + _data.result.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.peerTypes.count)) + for item in _data.peerTypes { + item.serialize(buffer, true) + } + serializeInt32(_data.cacheTime, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .preparedInlineMessage(let _data): + return ("preparedInlineMessage", [("queryId", ConstructorParameterDescription(_data.queryId)), ("result", ConstructorParameterDescription(_data.result)), ("peerTypes", ConstructorParameterDescription(_data.peerTypes)), ("cacheTime", ConstructorParameterDescription(_data.cacheTime)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_preparedInlineMessage(_ reader: BufferReader) -> PreparedInlineMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Api.BotInlineResult? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.BotInlineResult + } + var _3: [Api.InlineQueryPeerType]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InlineQueryPeerType.self) + } + var _4: Int32? + _4 = reader.readInt32() + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.messages.PreparedInlineMessage.preparedInlineMessage(Cons_preparedInlineMessage(queryId: _1!, result: _2!, peerTypes: _3!, cacheTime: _4!, users: _5!)) + } + else { + return nil + } + } + } +} +public extension Api.messages { + enum QuickReplies: TypeConstructorDescription { + public class Cons_quickReplies: TypeConstructorDescription { + public var quickReplies: [Api.QuickReply] + public var messages: [Api.Message] + public var chats: [Api.Chat] + public var users: [Api.User] + public init(quickReplies: [Api.QuickReply], messages: [Api.Message], chats: [Api.Chat], users: [Api.User]) { + self.quickReplies = quickReplies + self.messages = messages + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("quickReplies", [("quickReplies", ConstructorParameterDescription(self.quickReplies)), ("messages", ConstructorParameterDescription(self.messages)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case quickReplies(Cons_quickReplies) + case quickRepliesNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .quickReplies(let _data): + if boxed { + buffer.appendInt32(-963811691) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.quickReplies.count)) + for item in _data.quickReplies { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.messages.count)) + for item in _data.messages { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + case .quickRepliesNotModified: + if boxed { + buffer.appendInt32(1603398491) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .quickReplies(let _data): + return ("quickReplies", [("quickReplies", ConstructorParameterDescription(_data.quickReplies)), ("messages", ConstructorParameterDescription(_data.messages)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + case .quickRepliesNotModified: + return ("quickRepliesNotModified", []) + } + } + + public static func parse_quickReplies(_ reader: BufferReader) -> QuickReplies? { + var _1: [Api.QuickReply]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.QuickReply.self) + } + var _2: [Api.Message]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self) + } + var _3: [Api.Chat]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.messages.QuickReplies.quickReplies(Cons_quickReplies(quickReplies: _1!, messages: _2!, chats: _3!, users: _4!)) + } + else { + return nil + } + } + public static func parse_quickRepliesNotModified(_ reader: BufferReader) -> QuickReplies? { + return Api.messages.QuickReplies.quickRepliesNotModified + } + } +} +public extension Api.messages { + enum Reactions: TypeConstructorDescription { + public class Cons_reactions: TypeConstructorDescription { + public var hash: Int64 + public var reactions: [Api.Reaction] + public init(hash: Int64, reactions: [Api.Reaction]) { + self.hash = hash + self.reactions = reactions + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("reactions", [("hash", ConstructorParameterDescription(self.hash)), ("reactions", ConstructorParameterDescription(self.reactions))]) + } + } + case reactions(Cons_reactions) + case reactionsNotModified + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .reactions(let _data): + if boxed { + buffer.appendInt32(-352454890) + } + serializeInt64(_data.hash, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.reactions.count)) + for item in _data.reactions { + item.serialize(buffer, true) + } + break + case .reactionsNotModified: + if boxed { + buffer.appendInt32(-1334846497) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .reactions(let _data): + return ("reactions", [("hash", ConstructorParameterDescription(_data.hash)), ("reactions", ConstructorParameterDescription(_data.reactions))]) + case .reactionsNotModified: + return ("reactionsNotModified", []) + } + } + + public static func parse_reactions(_ reader: BufferReader) -> Reactions? { + var _1: Int64? + _1 = reader.readInt64() + var _2: [Api.Reaction]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Reaction.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.messages.Reactions.reactions(Cons_reactions(hash: _1!, reactions: _2!)) + } + else { + return nil + } + } + public static func parse_reactionsNotModified(_ reader: BufferReader) -> Reactions? { + return Api.messages.Reactions.reactionsNotModified + } + } +} public extension Api.messages { enum RecentStickers: TypeConstructorDescription { public class Cons_recentStickers: TypeConstructorDescription { @@ -556,7 +1022,7 @@ public extension Api.messages { var _4: Int32? _4 = reader.readInt32() var _5: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = reader.readInt32() } var _6: [Api.SearchResultsCalendarPeriod]? @@ -579,7 +1045,7 @@ public extension Api.messages { let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil @@ -809,15 +1275,15 @@ public extension Api.messages { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = reader.readInt32() } var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = reader.readInt32() } var _5: [Api.SponsoredMessage]? @@ -833,9 +1299,9 @@ public extension Api.messages { _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil @@ -1127,18 +1593,18 @@ public extension Api.messages { var _3: String? _3 = parseString(reader) var _4: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = reader.readInt32() } var _5: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.messages.TranscribedAudio.transcribedAudio(Cons_transcribedAudio(flags: _1!, transcriptionId: _2!, text: _3!, trialRemainsNum: _4, trialRemainsUntilDate: _5)) } @@ -1198,542 +1664,3 @@ public extension Api.messages { } } } -public extension Api.messages { - enum VotesList: TypeConstructorDescription { - public class Cons_votesList: TypeConstructorDescription { - public var flags: Int32 - public var count: Int32 - public var votes: [Api.MessagePeerVote] - public var chats: [Api.Chat] - public var users: [Api.User] - public var nextOffset: String? - public init(flags: Int32, count: Int32, votes: [Api.MessagePeerVote], chats: [Api.Chat], users: [Api.User], nextOffset: String?) { - self.flags = flags - self.count = count - self.votes = votes - self.chats = chats - self.users = users - self.nextOffset = nextOffset - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("votesList", [("flags", ConstructorParameterDescription(self.flags)), ("count", ConstructorParameterDescription(self.count)), ("votes", ConstructorParameterDescription(self.votes)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users)), ("nextOffset", ConstructorParameterDescription(self.nextOffset))]) - } - } - case votesList(Cons_votesList) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .votesList(let _data): - if boxed { - buffer.appendInt32(1218005070) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeInt32(_data.count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.votes.count)) - for item in _data.votes { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeString(_data.nextOffset!, buffer: buffer, boxed: false) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .votesList(let _data): - return ("votesList", [("flags", ConstructorParameterDescription(_data.flags)), ("count", ConstructorParameterDescription(_data.count)), ("votes", ConstructorParameterDescription(_data.votes)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users)), ("nextOffset", ConstructorParameterDescription(_data.nextOffset))]) - } - } - - public static func parse_votesList(_ reader: BufferReader) -> VotesList? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.MessagePeerVote]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessagePeerVote.self) - } - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - var _6: String? - if Int(_1!) & Int(1 << 0) != 0 { - _6 = parseString(reader) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.messages.VotesList.votesList(Cons_votesList(flags: _1!, count: _2!, votes: _3!, chats: _4!, users: _5!, nextOffset: _6)) - } - else { - return nil - } - } - } -} -public extension Api.messages { - enum WebPage: TypeConstructorDescription { - public class Cons_webPage: TypeConstructorDescription { - public var webpage: Api.WebPage - public var chats: [Api.Chat] - public var users: [Api.User] - public init(webpage: Api.WebPage, chats: [Api.Chat], users: [Api.User]) { - self.webpage = webpage - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("webPage", [("webpage", ConstructorParameterDescription(self.webpage)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case webPage(Cons_webPage) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .webPage(let _data): - if boxed { - buffer.appendInt32(-44166467) - } - _data.webpage.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .webPage(let _data): - return ("webPage", [("webpage", ConstructorParameterDescription(_data.webpage)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_webPage(_ reader: BufferReader) -> WebPage? { - var _1: Api.WebPage? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.WebPage - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.WebPage.webPage(Cons_webPage(webpage: _1!, chats: _2!, users: _3!)) - } - else { - return nil - } - } - } -} -public extension Api.messages { - indirect enum WebPagePreview: TypeConstructorDescription { - public class Cons_webPagePreview: TypeConstructorDescription { - public var media: Api.MessageMedia - public var chats: [Api.Chat] - public var users: [Api.User] - public init(media: Api.MessageMedia, chats: [Api.Chat], users: [Api.User]) { - self.media = media - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("webPagePreview", [("media", ConstructorParameterDescription(self.media)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case webPagePreview(Cons_webPagePreview) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .webPagePreview(let _data): - if boxed { - buffer.appendInt32(-1936029524) - } - _data.media.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .webPagePreview(let _data): - return ("webPagePreview", [("media", ConstructorParameterDescription(_data.media)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_webPagePreview(_ reader: BufferReader) -> WebPagePreview? { - var _1: Api.MessageMedia? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.MessageMedia - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.messages.WebPagePreview.webPagePreview(Cons_webPagePreview(media: _1!, chats: _2!, users: _3!)) - } - else { - return nil - } - } - } -} -public extension Api.payments { - enum BankCardData: TypeConstructorDescription { - public class Cons_bankCardData: TypeConstructorDescription { - public var title: String - public var openUrls: [Api.BankCardOpenUrl] - public init(title: String, openUrls: [Api.BankCardOpenUrl]) { - self.title = title - self.openUrls = openUrls - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("bankCardData", [("title", ConstructorParameterDescription(self.title)), ("openUrls", ConstructorParameterDescription(self.openUrls))]) - } - } - case bankCardData(Cons_bankCardData) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .bankCardData(let _data): - if boxed { - buffer.appendInt32(1042605427) - } - serializeString(_data.title, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.openUrls.count)) - for item in _data.openUrls { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .bankCardData(let _data): - return ("bankCardData", [("title", ConstructorParameterDescription(_data.title)), ("openUrls", ConstructorParameterDescription(_data.openUrls))]) - } - } - - public static func parse_bankCardData(_ reader: BufferReader) -> BankCardData? { - var _1: String? - _1 = parseString(reader) - var _2: [Api.BankCardOpenUrl]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BankCardOpenUrl.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.payments.BankCardData.bankCardData(Cons_bankCardData(title: _1!, openUrls: _2!)) - } - else { - return nil - } - } - } -} -public extension Api.payments { - enum CheckCanSendGiftResult: TypeConstructorDescription { - public class Cons_checkCanSendGiftResultFail: TypeConstructorDescription { - public var reason: Api.TextWithEntities - public init(reason: Api.TextWithEntities) { - self.reason = reason - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("checkCanSendGiftResultFail", [("reason", ConstructorParameterDescription(self.reason))]) - } - } - case checkCanSendGiftResultFail(Cons_checkCanSendGiftResultFail) - case checkCanSendGiftResultOk - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .checkCanSendGiftResultFail(let _data): - if boxed { - buffer.appendInt32(-706379148) - } - _data.reason.serialize(buffer, true) - break - case .checkCanSendGiftResultOk: - if boxed { - buffer.appendInt32(927967149) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .checkCanSendGiftResultFail(let _data): - return ("checkCanSendGiftResultFail", [("reason", ConstructorParameterDescription(_data.reason))]) - case .checkCanSendGiftResultOk: - return ("checkCanSendGiftResultOk", []) - } - } - - public static func parse_checkCanSendGiftResultFail(_ reader: BufferReader) -> CheckCanSendGiftResult? { - var _1: Api.TextWithEntities? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.TextWithEntities - } - let _c1 = _1 != nil - if _c1 { - return Api.payments.CheckCanSendGiftResult.checkCanSendGiftResultFail(Cons_checkCanSendGiftResultFail(reason: _1!)) - } - else { - return nil - } - } - public static func parse_checkCanSendGiftResultOk(_ reader: BufferReader) -> CheckCanSendGiftResult? { - return Api.payments.CheckCanSendGiftResult.checkCanSendGiftResultOk - } - } -} -public extension Api.payments { - enum CheckedGiftCode: TypeConstructorDescription { - public class Cons_checkedGiftCode: TypeConstructorDescription { - public var flags: Int32 - public var fromId: Api.Peer? - public var giveawayMsgId: Int32? - public var toId: Int64? - public var date: Int32 - public var days: Int32 - public var usedDate: Int32? - public var chats: [Api.Chat] - public var users: [Api.User] - public init(flags: Int32, fromId: Api.Peer?, giveawayMsgId: Int32?, toId: Int64?, date: Int32, days: Int32, usedDate: Int32?, chats: [Api.Chat], users: [Api.User]) { - self.flags = flags - self.fromId = fromId - self.giveawayMsgId = giveawayMsgId - self.toId = toId - self.date = date - self.days = days - self.usedDate = usedDate - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("checkedGiftCode", [("flags", ConstructorParameterDescription(self.flags)), ("fromId", ConstructorParameterDescription(self.fromId)), ("giveawayMsgId", ConstructorParameterDescription(self.giveawayMsgId)), ("toId", ConstructorParameterDescription(self.toId)), ("date", ConstructorParameterDescription(self.date)), ("days", ConstructorParameterDescription(self.days)), ("usedDate", ConstructorParameterDescription(self.usedDate)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case checkedGiftCode(Cons_checkedGiftCode) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .checkedGiftCode(let _data): - if boxed { - buffer.appendInt32(-342343793) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 4) != 0 { - _data.fromId!.serialize(buffer, true) - } - if Int(_data.flags) & Int(1 << 3) != 0 { - serializeInt32(_data.giveawayMsgId!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeInt64(_data.toId!, buffer: buffer, boxed: false) - } - serializeInt32(_data.date, buffer: buffer, boxed: false) - serializeInt32(_data.days, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 1) != 0 { - serializeInt32(_data.usedDate!, buffer: buffer, boxed: false) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .checkedGiftCode(let _data): - return ("checkedGiftCode", [("flags", ConstructorParameterDescription(_data.flags)), ("fromId", ConstructorParameterDescription(_data.fromId)), ("giveawayMsgId", ConstructorParameterDescription(_data.giveawayMsgId)), ("toId", ConstructorParameterDescription(_data.toId)), ("date", ConstructorParameterDescription(_data.date)), ("days", ConstructorParameterDescription(_data.days)), ("usedDate", ConstructorParameterDescription(_data.usedDate)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_checkedGiftCode(_ reader: BufferReader) -> CheckedGiftCode? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.Peer? - if Int(_1!) & Int(1 << 4) != 0 { - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.Peer - } - } - var _3: Int32? - if Int(_1!) & Int(1 << 3) != 0 { - _3 = reader.readInt32() - } - var _4: Int64? - if Int(_1!) & Int(1 << 0) != 0 { - _4 = reader.readInt64() - } - var _5: Int32? - _5 = reader.readInt32() - var _6: Int32? - _6 = reader.readInt32() - var _7: Int32? - if Int(_1!) & Int(1 << 1) != 0 { - _7 = reader.readInt32() - } - var _8: [Api.Chat]? - if let _ = reader.readInt32() { - _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _9: [Api.User]? - if let _ = reader.readInt32() { - _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - let _c8 = _8 != nil - let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.payments.CheckedGiftCode.checkedGiftCode(Cons_checkedGiftCode(flags: _1!, fromId: _2, giveawayMsgId: _3, toId: _4, date: _5!, days: _6!, usedDate: _7, chats: _8!, users: _9!)) - } - else { - return nil - } - } - } -} -public extension Api.payments { - enum ConnectedStarRefBots: TypeConstructorDescription { - public class Cons_connectedStarRefBots: TypeConstructorDescription { - public var count: Int32 - public var connectedBots: [Api.ConnectedBotStarRef] - public var users: [Api.User] - public init(count: Int32, connectedBots: [Api.ConnectedBotStarRef], users: [Api.User]) { - self.count = count - self.connectedBots = connectedBots - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("connectedStarRefBots", [("count", ConstructorParameterDescription(self.count)), ("connectedBots", ConstructorParameterDescription(self.connectedBots)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case connectedStarRefBots(Cons_connectedStarRefBots) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .connectedStarRefBots(let _data): - if boxed { - buffer.appendInt32(-1730811363) - } - serializeInt32(_data.count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.connectedBots.count)) - for item in _data.connectedBots { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .connectedStarRefBots(let _data): - return ("connectedStarRefBots", [("count", ConstructorParameterDescription(_data.count)), ("connectedBots", ConstructorParameterDescription(_data.connectedBots)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_connectedStarRefBots(_ reader: BufferReader) -> ConnectedStarRefBots? { - var _1: Int32? - _1 = reader.readInt32() - var _2: [Api.ConnectedBotStarRef]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ConnectedBotStarRef.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.payments.ConnectedStarRefBots.connectedStarRefBots(Cons_connectedStarRefBots(count: _1!, connectedBots: _2!, users: _3!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api37.swift b/submodules/TelegramApi/Sources/Api37.swift index c4a2ad6d96..1cf60883c0 100644 --- a/submodules/TelegramApi/Sources/Api37.swift +++ b/submodules/TelegramApi/Sources/Api37.swift @@ -1,3 +1,542 @@ +public extension Api.messages { + enum VotesList: TypeConstructorDescription { + public class Cons_votesList: TypeConstructorDescription { + public var flags: Int32 + public var count: Int32 + public var votes: [Api.MessagePeerVote] + public var chats: [Api.Chat] + public var users: [Api.User] + public var nextOffset: String? + public init(flags: Int32, count: Int32, votes: [Api.MessagePeerVote], chats: [Api.Chat], users: [Api.User], nextOffset: String?) { + self.flags = flags + self.count = count + self.votes = votes + self.chats = chats + self.users = users + self.nextOffset = nextOffset + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("votesList", [("flags", ConstructorParameterDescription(self.flags)), ("count", ConstructorParameterDescription(self.count)), ("votes", ConstructorParameterDescription(self.votes)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users)), ("nextOffset", ConstructorParameterDescription(self.nextOffset))]) + } + } + case votesList(Cons_votesList) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .votesList(let _data): + if boxed { + buffer.appendInt32(1218005070) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeInt32(_data.count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.votes.count)) + for item in _data.votes { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeString(_data.nextOffset!, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .votesList(let _data): + return ("votesList", [("flags", ConstructorParameterDescription(_data.flags)), ("count", ConstructorParameterDescription(_data.count)), ("votes", ConstructorParameterDescription(_data.votes)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users)), ("nextOffset", ConstructorParameterDescription(_data.nextOffset))]) + } + } + + public static func parse_votesList(_ reader: BufferReader) -> VotesList? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.MessagePeerVote]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessagePeerVote.self) + } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _6: String? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _6 = parseString(reader) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.messages.VotesList.votesList(Cons_votesList(flags: _1!, count: _2!, votes: _3!, chats: _4!, users: _5!, nextOffset: _6)) + } + else { + return nil + } + } + } +} +public extension Api.messages { + enum WebPage: TypeConstructorDescription { + public class Cons_webPage: TypeConstructorDescription { + public var webpage: Api.WebPage + public var chats: [Api.Chat] + public var users: [Api.User] + public init(webpage: Api.WebPage, chats: [Api.Chat], users: [Api.User]) { + self.webpage = webpage + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("webPage", [("webpage", ConstructorParameterDescription(self.webpage)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case webPage(Cons_webPage) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .webPage(let _data): + if boxed { + buffer.appendInt32(-44166467) + } + _data.webpage.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .webPage(let _data): + return ("webPage", [("webpage", ConstructorParameterDescription(_data.webpage)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_webPage(_ reader: BufferReader) -> WebPage? { + var _1: Api.WebPage? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.WebPage + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.WebPage.webPage(Cons_webPage(webpage: _1!, chats: _2!, users: _3!)) + } + else { + return nil + } + } + } +} +public extension Api.messages { + indirect enum WebPagePreview: TypeConstructorDescription { + public class Cons_webPagePreview: TypeConstructorDescription { + public var media: Api.MessageMedia + public var chats: [Api.Chat] + public var users: [Api.User] + public init(media: Api.MessageMedia, chats: [Api.Chat], users: [Api.User]) { + self.media = media + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("webPagePreview", [("media", ConstructorParameterDescription(self.media)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case webPagePreview(Cons_webPagePreview) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .webPagePreview(let _data): + if boxed { + buffer.appendInt32(-1936029524) + } + _data.media.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .webPagePreview(let _data): + return ("webPagePreview", [("media", ConstructorParameterDescription(_data.media)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_webPagePreview(_ reader: BufferReader) -> WebPagePreview? { + var _1: Api.MessageMedia? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.MessageMedia + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.messages.WebPagePreview.webPagePreview(Cons_webPagePreview(media: _1!, chats: _2!, users: _3!)) + } + else { + return nil + } + } + } +} +public extension Api.payments { + enum BankCardData: TypeConstructorDescription { + public class Cons_bankCardData: TypeConstructorDescription { + public var title: String + public var openUrls: [Api.BankCardOpenUrl] + public init(title: String, openUrls: [Api.BankCardOpenUrl]) { + self.title = title + self.openUrls = openUrls + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("bankCardData", [("title", ConstructorParameterDescription(self.title)), ("openUrls", ConstructorParameterDescription(self.openUrls))]) + } + } + case bankCardData(Cons_bankCardData) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .bankCardData(let _data): + if boxed { + buffer.appendInt32(1042605427) + } + serializeString(_data.title, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.openUrls.count)) + for item in _data.openUrls { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .bankCardData(let _data): + return ("bankCardData", [("title", ConstructorParameterDescription(_data.title)), ("openUrls", ConstructorParameterDescription(_data.openUrls))]) + } + } + + public static func parse_bankCardData(_ reader: BufferReader) -> BankCardData? { + var _1: String? + _1 = parseString(reader) + var _2: [Api.BankCardOpenUrl]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BankCardOpenUrl.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.payments.BankCardData.bankCardData(Cons_bankCardData(title: _1!, openUrls: _2!)) + } + else { + return nil + } + } + } +} +public extension Api.payments { + enum CheckCanSendGiftResult: TypeConstructorDescription { + public class Cons_checkCanSendGiftResultFail: TypeConstructorDescription { + public var reason: Api.TextWithEntities + public init(reason: Api.TextWithEntities) { + self.reason = reason + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("checkCanSendGiftResultFail", [("reason", ConstructorParameterDescription(self.reason))]) + } + } + case checkCanSendGiftResultFail(Cons_checkCanSendGiftResultFail) + case checkCanSendGiftResultOk + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .checkCanSendGiftResultFail(let _data): + if boxed { + buffer.appendInt32(-706379148) + } + _data.reason.serialize(buffer, true) + break + case .checkCanSendGiftResultOk: + if boxed { + buffer.appendInt32(927967149) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .checkCanSendGiftResultFail(let _data): + return ("checkCanSendGiftResultFail", [("reason", ConstructorParameterDescription(_data.reason))]) + case .checkCanSendGiftResultOk: + return ("checkCanSendGiftResultOk", []) + } + } + + public static func parse_checkCanSendGiftResultFail(_ reader: BufferReader) -> CheckCanSendGiftResult? { + var _1: Api.TextWithEntities? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.TextWithEntities + } + let _c1 = _1 != nil + if _c1 { + return Api.payments.CheckCanSendGiftResult.checkCanSendGiftResultFail(Cons_checkCanSendGiftResultFail(reason: _1!)) + } + else { + return nil + } + } + public static func parse_checkCanSendGiftResultOk(_ reader: BufferReader) -> CheckCanSendGiftResult? { + return Api.payments.CheckCanSendGiftResult.checkCanSendGiftResultOk + } + } +} +public extension Api.payments { + enum CheckedGiftCode: TypeConstructorDescription { + public class Cons_checkedGiftCode: TypeConstructorDescription { + public var flags: Int32 + public var fromId: Api.Peer? + public var giveawayMsgId: Int32? + public var toId: Int64? + public var date: Int32 + public var days: Int32 + public var usedDate: Int32? + public var chats: [Api.Chat] + public var users: [Api.User] + public init(flags: Int32, fromId: Api.Peer?, giveawayMsgId: Int32?, toId: Int64?, date: Int32, days: Int32, usedDate: Int32?, chats: [Api.Chat], users: [Api.User]) { + self.flags = flags + self.fromId = fromId + self.giveawayMsgId = giveawayMsgId + self.toId = toId + self.date = date + self.days = days + self.usedDate = usedDate + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("checkedGiftCode", [("flags", ConstructorParameterDescription(self.flags)), ("fromId", ConstructorParameterDescription(self.fromId)), ("giveawayMsgId", ConstructorParameterDescription(self.giveawayMsgId)), ("toId", ConstructorParameterDescription(self.toId)), ("date", ConstructorParameterDescription(self.date)), ("days", ConstructorParameterDescription(self.days)), ("usedDate", ConstructorParameterDescription(self.usedDate)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case checkedGiftCode(Cons_checkedGiftCode) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .checkedGiftCode(let _data): + if boxed { + buffer.appendInt32(-342343793) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 4) != 0 { + _data.fromId!.serialize(buffer, true) + } + if Int(_data.flags) & Int(1 << 3) != 0 { + serializeInt32(_data.giveawayMsgId!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeInt64(_data.toId!, buffer: buffer, boxed: false) + } + serializeInt32(_data.date, buffer: buffer, boxed: false) + serializeInt32(_data.days, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 1) != 0 { + serializeInt32(_data.usedDate!, buffer: buffer, boxed: false) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .checkedGiftCode(let _data): + return ("checkedGiftCode", [("flags", ConstructorParameterDescription(_data.flags)), ("fromId", ConstructorParameterDescription(_data.fromId)), ("giveawayMsgId", ConstructorParameterDescription(_data.giveawayMsgId)), ("toId", ConstructorParameterDescription(_data.toId)), ("date", ConstructorParameterDescription(_data.date)), ("days", ConstructorParameterDescription(_data.days)), ("usedDate", ConstructorParameterDescription(_data.usedDate)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_checkedGiftCode(_ reader: BufferReader) -> CheckedGiftCode? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? + if Int(_1 ?? 0) & Int(1 << 4) != 0 { + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Peer + } + } + var _3: Int32? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _3 = reader.readInt32() + } + var _4: Int64? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _4 = reader.readInt64() + } + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: Int32? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _7 = reader.readInt32() + } + var _8: [Api.Chat]? + if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _9: [Api.User]? + if let _ = reader.readInt32() { + _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.payments.CheckedGiftCode.checkedGiftCode(Cons_checkedGiftCode(flags: _1!, fromId: _2, giveawayMsgId: _3, toId: _4, date: _5!, days: _6!, usedDate: _7, chats: _8!, users: _9!)) + } + else { + return nil + } + } + } +} +public extension Api.payments { + enum ConnectedStarRefBots: TypeConstructorDescription { + public class Cons_connectedStarRefBots: TypeConstructorDescription { + public var count: Int32 + public var connectedBots: [Api.ConnectedBotStarRef] + public var users: [Api.User] + public init(count: Int32, connectedBots: [Api.ConnectedBotStarRef], users: [Api.User]) { + self.count = count + self.connectedBots = connectedBots + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("connectedStarRefBots", [("count", ConstructorParameterDescription(self.count)), ("connectedBots", ConstructorParameterDescription(self.connectedBots)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case connectedStarRefBots(Cons_connectedStarRefBots) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .connectedStarRefBots(let _data): + if boxed { + buffer.appendInt32(-1730811363) + } + serializeInt32(_data.count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.connectedBots.count)) + for item in _data.connectedBots { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .connectedStarRefBots(let _data): + return ("connectedStarRefBots", [("count", ConstructorParameterDescription(_data.count)), ("connectedBots", ConstructorParameterDescription(_data.connectedBots)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_connectedStarRefBots(_ reader: BufferReader) -> ConnectedStarRefBots? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.ConnectedBotStarRef]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ConnectedBotStarRef.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.payments.ConnectedStarRefBots.connectedStarRefBots(Cons_connectedStarRefBots(count: _1!, connectedBots: _2!, users: _3!)) + } + else { + return nil + } + } + } +} public extension Api.payments { enum ExportedInvoice: TypeConstructorDescription { public class Cons_exportedInvoice: TypeConstructorDescription { @@ -139,22 +678,22 @@ public extension Api.payments { var _2: Int32? _2 = reader.readInt32() var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = reader.readInt32() } var _4: Int64? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = reader.readInt64() } var _5: String? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _5 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.payments.GiveawayInfo.giveawayInfo(Cons_giveawayInfo(flags: _1!, startDate: _2!, joinedTooEarlyDate: _3, adminDisallowedChatId: _4, disallowedCountry: _5)) } @@ -168,11 +707,11 @@ public extension Api.payments { var _2: Int32? _2 = reader.readInt32() var _3: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _3 = parseString(reader) } var _4: Int64? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _4 = reader.readInt64() } var _5: Int32? @@ -180,16 +719,16 @@ public extension Api.payments { var _6: Int32? _6 = reader.readInt32() var _7: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _7 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 4) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.payments.GiveawayInfo.giveawayInfoResults(Cons_giveawayInfoResults(flags: _1!, startDate: _2!, giftCodeSlug: _3, starsPrize: _4, finishDate: _5!, winnersCount: _6!, activatedCount: _7)) } @@ -374,7 +913,7 @@ public extension Api.payments { var _5: String? _5 = parseString(reader) var _6: Api.WebDocument? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.WebDocument } @@ -388,29 +927,29 @@ public extension Api.payments { var _9: String? _9 = parseString(reader) var _10: String? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _10 = parseString(reader) } var _11: Api.DataJSON? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _11 = Api.parse(reader, signature: signature) as? Api.DataJSON } } var _12: [Api.PaymentFormMethod]? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let _ = reader.readInt32() { _12 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PaymentFormMethod.self) } } var _13: Api.PaymentRequestedInfo? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _13 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo } } var _14: [Api.PaymentSavedCredentials]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PaymentSavedCredentials.self) } @@ -424,15 +963,15 @@ public extension Api.payments { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 5) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil let _c9 = _9 != nil - let _c10 = (Int(_1!) & Int(1 << 4) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 4) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 6) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 0) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 1) == 0) || _14 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _13 != nil + let _c14 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _14 != nil let _c15 = _15 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { return Api.payments.PaymentForm.paymentForm(Cons_paymentForm(flags: _1!, formId: _2!, botId: _3!, title: _4!, description: _5!, photo: _6, invoice: _7!, providerId: _8!, url: _9!, nativeProvider: _10, nativeParams: _11, additionalMethods: _12, savedInfo: _13, savedCredentials: _14, users: _15!)) @@ -469,7 +1008,7 @@ public extension Api.payments { var _5: String? _5 = parseString(reader) var _6: Api.WebDocument? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.WebDocument } @@ -487,7 +1026,7 @@ public extension Api.payments { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 5) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { @@ -652,7 +1191,7 @@ public extension Api.payments { var _6: String? _6 = parseString(reader) var _7: Api.WebDocument? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.WebDocument } @@ -662,19 +1201,19 @@ public extension Api.payments { _8 = Api.parse(reader, signature: signature) as? Api.Invoice } var _9: Api.PaymentRequestedInfo? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo } } var _10: Api.ShippingOption? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.ShippingOption } } var _11: Int64? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _11 = reader.readInt64() } var _12: String? @@ -693,11 +1232,11 @@ public extension Api.payments { let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _7 != nil let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 1) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 3) == 0) || _11 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _11 != nil let _c12 = _12 != nil let _c13 = _13 != nil let _c14 = _14 != nil @@ -721,7 +1260,7 @@ public extension Api.payments { var _5: String? _5 = parseString(reader) var _6: Api.WebDocument? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.WebDocument } @@ -745,7 +1284,7 @@ public extension Api.payments { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil let _c9 = _9 != nil @@ -928,17 +1467,17 @@ public extension Api.payments { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGift.self) } var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } var _5: [Api.StarGiftAttribute]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAttribute.self) } } var _6: Int64? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _6 = reader.readInt64() } var _7: [Api.Chat]? @@ -946,7 +1485,7 @@ public extension Api.payments { _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) } var _8: [Api.StarGiftAttributeCounter]? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let _ = reader.readInt32() { _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAttributeCounter.self) } @@ -958,11 +1497,11 @@ public extension Api.payments { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _8 != nil let _c9 = _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.payments.ResaleStarGifts.resaleStarGifts(Cons_resaleStarGifts(flags: _1!, count: _2!, gifts: _3!, nextOffset: _4, attributes: _5, attributesHash: _6, chats: _7!, counters: _8, users: _9!)) @@ -1013,13 +1552,13 @@ public extension Api.payments { var _1: Int32? _1 = reader.readInt32() var _2: Api.PaymentRequestedInfo? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil if _c1 && _c2 { return Api.payments.SavedInfo.savedInfo(Cons_savedInfo(flags: _1!, savedInfo: _2)) } @@ -1100,7 +1639,7 @@ public extension Api.payments { var _2: Int32? _2 = reader.readInt32() var _3: Api.Bool? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.Bool } @@ -1110,7 +1649,7 @@ public extension Api.payments { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SavedStarGift.self) } var _5: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = parseString(reader) } var _6: [Api.Chat]? @@ -1123,9 +1662,9 @@ public extension Api.payments { } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { @@ -1708,675 +2247,3 @@ public extension Api.payments { } } } -public extension Api.payments { - enum StarsRevenueAdsAccountUrl: TypeConstructorDescription { - public class Cons_starsRevenueAdsAccountUrl: TypeConstructorDescription { - public var url: String - public init(url: String) { - self.url = url - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("starsRevenueAdsAccountUrl", [("url", ConstructorParameterDescription(self.url))]) - } - } - case starsRevenueAdsAccountUrl(Cons_starsRevenueAdsAccountUrl) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .starsRevenueAdsAccountUrl(let _data): - if boxed { - buffer.appendInt32(961445665) - } - serializeString(_data.url, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .starsRevenueAdsAccountUrl(let _data): - return ("starsRevenueAdsAccountUrl", [("url", ConstructorParameterDescription(_data.url))]) - } - } - - public static func parse_starsRevenueAdsAccountUrl(_ reader: BufferReader) -> StarsRevenueAdsAccountUrl? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.payments.StarsRevenueAdsAccountUrl.starsRevenueAdsAccountUrl(Cons_starsRevenueAdsAccountUrl(url: _1!)) - } - else { - return nil - } - } - } -} -public extension Api.payments { - enum StarsRevenueStats: TypeConstructorDescription { - public class Cons_starsRevenueStats: TypeConstructorDescription { - public var flags: Int32 - public var topHoursGraph: Api.StatsGraph? - public var revenueGraph: Api.StatsGraph - public var status: Api.StarsRevenueStatus - public var usdRate: Double - public init(flags: Int32, topHoursGraph: Api.StatsGraph?, revenueGraph: Api.StatsGraph, status: Api.StarsRevenueStatus, usdRate: Double) { - self.flags = flags - self.topHoursGraph = topHoursGraph - self.revenueGraph = revenueGraph - self.status = status - self.usdRate = usdRate - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("starsRevenueStats", [("flags", ConstructorParameterDescription(self.flags)), ("topHoursGraph", ConstructorParameterDescription(self.topHoursGraph)), ("revenueGraph", ConstructorParameterDescription(self.revenueGraph)), ("status", ConstructorParameterDescription(self.status)), ("usdRate", ConstructorParameterDescription(self.usdRate))]) - } - } - case starsRevenueStats(Cons_starsRevenueStats) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .starsRevenueStats(let _data): - if boxed { - buffer.appendInt32(1814066038) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 0) != 0 { - _data.topHoursGraph!.serialize(buffer, true) - } - _data.revenueGraph.serialize(buffer, true) - _data.status.serialize(buffer, true) - serializeDouble(_data.usdRate, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .starsRevenueStats(let _data): - return ("starsRevenueStats", [("flags", ConstructorParameterDescription(_data.flags)), ("topHoursGraph", ConstructorParameterDescription(_data.topHoursGraph)), ("revenueGraph", ConstructorParameterDescription(_data.revenueGraph)), ("status", ConstructorParameterDescription(_data.status)), ("usdRate", ConstructorParameterDescription(_data.usdRate))]) - } - } - - public static func parse_starsRevenueStats(_ reader: BufferReader) -> StarsRevenueStats? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.StatsGraph? - if Int(_1!) & Int(1 << 0) != 0 { - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - } - var _3: Api.StatsGraph? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.StatsGraph - } - var _4: Api.StarsRevenueStatus? - if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.StarsRevenueStatus - } - var _5: Double? - _5 = reader.readDouble() - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.payments.StarsRevenueStats.starsRevenueStats(Cons_starsRevenueStats(flags: _1!, topHoursGraph: _2, revenueGraph: _3!, status: _4!, usdRate: _5!)) - } - else { - return nil - } - } - } -} -public extension Api.payments { - enum StarsRevenueWithdrawalUrl: TypeConstructorDescription { - public class Cons_starsRevenueWithdrawalUrl: TypeConstructorDescription { - public var url: String - public init(url: String) { - self.url = url - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("starsRevenueWithdrawalUrl", [("url", ConstructorParameterDescription(self.url))]) - } - } - case starsRevenueWithdrawalUrl(Cons_starsRevenueWithdrawalUrl) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .starsRevenueWithdrawalUrl(let _data): - if boxed { - buffer.appendInt32(497778871) - } - serializeString(_data.url, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .starsRevenueWithdrawalUrl(let _data): - return ("starsRevenueWithdrawalUrl", [("url", ConstructorParameterDescription(_data.url))]) - } - } - - public static func parse_starsRevenueWithdrawalUrl(_ reader: BufferReader) -> StarsRevenueWithdrawalUrl? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return Api.payments.StarsRevenueWithdrawalUrl.starsRevenueWithdrawalUrl(Cons_starsRevenueWithdrawalUrl(url: _1!)) - } - else { - return nil - } - } - } -} -public extension Api.payments { - enum StarsStatus: TypeConstructorDescription { - public class Cons_starsStatus: TypeConstructorDescription { - public var flags: Int32 - public var balance: Api.StarsAmount - public var subscriptions: [Api.StarsSubscription]? - public var subscriptionsNextOffset: String? - public var subscriptionsMissingBalance: Int64? - public var history: [Api.StarsTransaction]? - public var nextOffset: String? - public var chats: [Api.Chat] - public var users: [Api.User] - public init(flags: Int32, balance: Api.StarsAmount, subscriptions: [Api.StarsSubscription]?, subscriptionsNextOffset: String?, subscriptionsMissingBalance: Int64?, history: [Api.StarsTransaction]?, nextOffset: String?, chats: [Api.Chat], users: [Api.User]) { - self.flags = flags - self.balance = balance - self.subscriptions = subscriptions - self.subscriptionsNextOffset = subscriptionsNextOffset - self.subscriptionsMissingBalance = subscriptionsMissingBalance - self.history = history - self.nextOffset = nextOffset - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("starsStatus", [("flags", ConstructorParameterDescription(self.flags)), ("balance", ConstructorParameterDescription(self.balance)), ("subscriptions", ConstructorParameterDescription(self.subscriptions)), ("subscriptionsNextOffset", ConstructorParameterDescription(self.subscriptionsNextOffset)), ("subscriptionsMissingBalance", ConstructorParameterDescription(self.subscriptionsMissingBalance)), ("history", ConstructorParameterDescription(self.history)), ("nextOffset", ConstructorParameterDescription(self.nextOffset)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case starsStatus(Cons_starsStatus) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .starsStatus(let _data): - if boxed { - buffer.appendInt32(1822222573) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - _data.balance.serialize(buffer, true) - if Int(_data.flags) & Int(1 << 1) != 0 { - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.subscriptions!.count)) - for item in _data.subscriptions! { - item.serialize(buffer, true) - } - } - if Int(_data.flags) & Int(1 << 2) != 0 { - serializeString(_data.subscriptionsNextOffset!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 4) != 0 { - serializeInt64(_data.subscriptionsMissingBalance!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 3) != 0 { - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.history!.count)) - for item in _data.history! { - item.serialize(buffer, true) - } - } - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeString(_data.nextOffset!, buffer: buffer, boxed: false) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .starsStatus(let _data): - return ("starsStatus", [("flags", ConstructorParameterDescription(_data.flags)), ("balance", ConstructorParameterDescription(_data.balance)), ("subscriptions", ConstructorParameterDescription(_data.subscriptions)), ("subscriptionsNextOffset", ConstructorParameterDescription(_data.subscriptionsNextOffset)), ("subscriptionsMissingBalance", ConstructorParameterDescription(_data.subscriptionsMissingBalance)), ("history", ConstructorParameterDescription(_data.history)), ("nextOffset", ConstructorParameterDescription(_data.nextOffset)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_starsStatus(_ reader: BufferReader) -> StarsStatus? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.StarsAmount? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.StarsAmount - } - var _3: [Api.StarsSubscription]? - if Int(_1!) & Int(1 << 1) != 0 { - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsSubscription.self) - } - } - var _4: String? - if Int(_1!) & Int(1 << 2) != 0 { - _4 = parseString(reader) - } - var _5: Int64? - if Int(_1!) & Int(1 << 4) != 0 { - _5 = reader.readInt64() - } - var _6: [Api.StarsTransaction]? - if Int(_1!) & Int(1 << 3) != 0 { - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsTransaction.self) - } - } - var _7: String? - if Int(_1!) & Int(1 << 0) != 0 { - _7 = parseString(reader) - } - var _8: [Api.Chat]? - if let _ = reader.readInt32() { - _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _9: [Api.User]? - if let _ = reader.readInt32() { - _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil - let _c8 = _8 != nil - let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.payments.StarsStatus.starsStatus(Cons_starsStatus(flags: _1!, balance: _2!, subscriptions: _3, subscriptionsNextOffset: _4, subscriptionsMissingBalance: _5, history: _6, nextOffset: _7, chats: _8!, users: _9!)) - } - else { - return nil - } - } - } -} -public extension Api.payments { - enum SuggestedStarRefBots: TypeConstructorDescription { - public class Cons_suggestedStarRefBots: TypeConstructorDescription { - public var flags: Int32 - public var count: Int32 - public var suggestedBots: [Api.StarRefProgram] - public var users: [Api.User] - public var nextOffset: String? - public init(flags: Int32, count: Int32, suggestedBots: [Api.StarRefProgram], users: [Api.User], nextOffset: String?) { - self.flags = flags - self.count = count - self.suggestedBots = suggestedBots - self.users = users - self.nextOffset = nextOffset - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("suggestedStarRefBots", [("flags", ConstructorParameterDescription(self.flags)), ("count", ConstructorParameterDescription(self.count)), ("suggestedBots", ConstructorParameterDescription(self.suggestedBots)), ("users", ConstructorParameterDescription(self.users)), ("nextOffset", ConstructorParameterDescription(self.nextOffset))]) - } - } - case suggestedStarRefBots(Cons_suggestedStarRefBots) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .suggestedStarRefBots(let _data): - if boxed { - buffer.appendInt32(-1261053863) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeInt32(_data.count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.suggestedBots.count)) - for item in _data.suggestedBots { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeString(_data.nextOffset!, buffer: buffer, boxed: false) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .suggestedStarRefBots(let _data): - return ("suggestedStarRefBots", [("flags", ConstructorParameterDescription(_data.flags)), ("count", ConstructorParameterDescription(_data.count)), ("suggestedBots", ConstructorParameterDescription(_data.suggestedBots)), ("users", ConstructorParameterDescription(_data.users)), ("nextOffset", ConstructorParameterDescription(_data.nextOffset))]) - } - } - - public static func parse_suggestedStarRefBots(_ reader: BufferReader) -> SuggestedStarRefBots? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.StarRefProgram]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarRefProgram.self) - } - var _4: [Api.User]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - var _5: String? - if Int(_1!) & Int(1 << 0) != 0 { - _5 = parseString(reader) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.payments.SuggestedStarRefBots.suggestedStarRefBots(Cons_suggestedStarRefBots(flags: _1!, count: _2!, suggestedBots: _3!, users: _4!, nextOffset: _5)) - } - else { - return nil - } - } - } -} -public extension Api.payments { - enum UniqueStarGift: TypeConstructorDescription { - public class Cons_uniqueStarGift: TypeConstructorDescription { - public var gift: Api.StarGift - public var chats: [Api.Chat] - public var users: [Api.User] - public init(gift: Api.StarGift, chats: [Api.Chat], users: [Api.User]) { - self.gift = gift - self.chats = chats - self.users = users - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("uniqueStarGift", [("gift", ConstructorParameterDescription(self.gift)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) - } - } - case uniqueStarGift(Cons_uniqueStarGift) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .uniqueStarGift(let _data): - if boxed { - buffer.appendInt32(1097619176) - } - _data.gift.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.chats.count)) - for item in _data.chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.users.count)) - for item in _data.users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .uniqueStarGift(let _data): - return ("uniqueStarGift", [("gift", ConstructorParameterDescription(_data.gift)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) - } - } - - public static func parse_uniqueStarGift(_ reader: BufferReader) -> UniqueStarGift? { - var _1: Api.StarGift? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.StarGift - } - var _2: [Api.Chat]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _3: [Api.User]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.payments.UniqueStarGift.uniqueStarGift(Cons_uniqueStarGift(gift: _1!, chats: _2!, users: _3!)) - } - else { - return nil - } - } - } -} -public extension Api.payments { - enum UniqueStarGiftValueInfo: TypeConstructorDescription { - public class Cons_uniqueStarGiftValueInfo: TypeConstructorDescription { - public var flags: Int32 - public var currency: String - public var value: Int64 - public var initialSaleDate: Int32 - public var initialSaleStars: Int64 - public var initialSalePrice: Int64 - public var lastSaleDate: Int32? - public var lastSalePrice: Int64? - public var floorPrice: Int64? - public var averagePrice: Int64? - public var listedCount: Int32? - public var fragmentListedCount: Int32? - public var fragmentListedUrl: String? - public init(flags: Int32, currency: String, value: Int64, initialSaleDate: Int32, initialSaleStars: Int64, initialSalePrice: Int64, lastSaleDate: Int32?, lastSalePrice: Int64?, floorPrice: Int64?, averagePrice: Int64?, listedCount: Int32?, fragmentListedCount: Int32?, fragmentListedUrl: String?) { - self.flags = flags - self.currency = currency - self.value = value - self.initialSaleDate = initialSaleDate - self.initialSaleStars = initialSaleStars - self.initialSalePrice = initialSalePrice - self.lastSaleDate = lastSaleDate - self.lastSalePrice = lastSalePrice - self.floorPrice = floorPrice - self.averagePrice = averagePrice - self.listedCount = listedCount - self.fragmentListedCount = fragmentListedCount - self.fragmentListedUrl = fragmentListedUrl - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("uniqueStarGiftValueInfo", [("flags", ConstructorParameterDescription(self.flags)), ("currency", ConstructorParameterDescription(self.currency)), ("value", ConstructorParameterDescription(self.value)), ("initialSaleDate", ConstructorParameterDescription(self.initialSaleDate)), ("initialSaleStars", ConstructorParameterDescription(self.initialSaleStars)), ("initialSalePrice", ConstructorParameterDescription(self.initialSalePrice)), ("lastSaleDate", ConstructorParameterDescription(self.lastSaleDate)), ("lastSalePrice", ConstructorParameterDescription(self.lastSalePrice)), ("floorPrice", ConstructorParameterDescription(self.floorPrice)), ("averagePrice", ConstructorParameterDescription(self.averagePrice)), ("listedCount", ConstructorParameterDescription(self.listedCount)), ("fragmentListedCount", ConstructorParameterDescription(self.fragmentListedCount)), ("fragmentListedUrl", ConstructorParameterDescription(self.fragmentListedUrl))]) - } - } - case uniqueStarGiftValueInfo(Cons_uniqueStarGiftValueInfo) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .uniqueStarGiftValueInfo(let _data): - if boxed { - buffer.appendInt32(1362093126) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeString(_data.currency, buffer: buffer, boxed: false) - serializeInt64(_data.value, buffer: buffer, boxed: false) - serializeInt32(_data.initialSaleDate, buffer: buffer, boxed: false) - serializeInt64(_data.initialSaleStars, buffer: buffer, boxed: false) - serializeInt64(_data.initialSalePrice, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeInt32(_data.lastSaleDate!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeInt64(_data.lastSalePrice!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 2) != 0 { - serializeInt64(_data.floorPrice!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 3) != 0 { - serializeInt64(_data.averagePrice!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 4) != 0 { - serializeInt32(_data.listedCount!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 5) != 0 { - serializeInt32(_data.fragmentListedCount!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 5) != 0 { - serializeString(_data.fragmentListedUrl!, buffer: buffer, boxed: false) - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .uniqueStarGiftValueInfo(let _data): - return ("uniqueStarGiftValueInfo", [("flags", ConstructorParameterDescription(_data.flags)), ("currency", ConstructorParameterDescription(_data.currency)), ("value", ConstructorParameterDescription(_data.value)), ("initialSaleDate", ConstructorParameterDescription(_data.initialSaleDate)), ("initialSaleStars", ConstructorParameterDescription(_data.initialSaleStars)), ("initialSalePrice", ConstructorParameterDescription(_data.initialSalePrice)), ("lastSaleDate", ConstructorParameterDescription(_data.lastSaleDate)), ("lastSalePrice", ConstructorParameterDescription(_data.lastSalePrice)), ("floorPrice", ConstructorParameterDescription(_data.floorPrice)), ("averagePrice", ConstructorParameterDescription(_data.averagePrice)), ("listedCount", ConstructorParameterDescription(_data.listedCount)), ("fragmentListedCount", ConstructorParameterDescription(_data.fragmentListedCount)), ("fragmentListedUrl", ConstructorParameterDescription(_data.fragmentListedUrl))]) - } - } - - public static func parse_uniqueStarGiftValueInfo(_ reader: BufferReader) -> UniqueStarGiftValueInfo? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: Int64? - _3 = reader.readInt64() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int64? - _5 = reader.readInt64() - var _6: Int64? - _6 = reader.readInt64() - var _7: Int32? - if Int(_1!) & Int(1 << 0) != 0 { - _7 = reader.readInt32() - } - var _8: Int64? - if Int(_1!) & Int(1 << 0) != 0 { - _8 = reader.readInt64() - } - var _9: Int64? - if Int(_1!) & Int(1 << 2) != 0 { - _9 = reader.readInt64() - } - var _10: Int64? - if Int(_1!) & Int(1 << 3) != 0 { - _10 = reader.readInt64() - } - var _11: Int32? - if Int(_1!) & Int(1 << 4) != 0 { - _11 = reader.readInt32() - } - var _12: Int32? - if Int(_1!) & Int(1 << 5) != 0 { - _12 = reader.readInt32() - } - var _13: String? - if Int(_1!) & Int(1 << 5) != 0 { - _13 = parseString(reader) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 0) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 3) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 4) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 5) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 5) == 0) || _13 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 { - return Api.payments.UniqueStarGiftValueInfo.uniqueStarGiftValueInfo(Cons_uniqueStarGiftValueInfo(flags: _1!, currency: _2!, value: _3!, initialSaleDate: _4!, initialSaleStars: _5!, initialSalePrice: _6!, lastSaleDate: _7, lastSalePrice: _8, floorPrice: _9, averagePrice: _10, listedCount: _11, fragmentListedCount: _12, fragmentListedUrl: _13)) - } - else { - return nil - } - } - } -} -public extension Api.payments { - enum ValidatedRequestedInfo: TypeConstructorDescription { - public class Cons_validatedRequestedInfo: TypeConstructorDescription { - public var flags: Int32 - public var id: String? - public var shippingOptions: [Api.ShippingOption]? - public init(flags: Int32, id: String?, shippingOptions: [Api.ShippingOption]?) { - self.flags = flags - self.id = id - self.shippingOptions = shippingOptions - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("validatedRequestedInfo", [("flags", ConstructorParameterDescription(self.flags)), ("id", ConstructorParameterDescription(self.id)), ("shippingOptions", ConstructorParameterDescription(self.shippingOptions))]) - } - } - case validatedRequestedInfo(Cons_validatedRequestedInfo) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .validatedRequestedInfo(let _data): - if boxed { - buffer.appendInt32(-784000893) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - if Int(_data.flags) & Int(1 << 0) != 0 { - serializeString(_data.id!, buffer: buffer, boxed: false) - } - if Int(_data.flags) & Int(1 << 1) != 0 { - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(_data.shippingOptions!.count)) - for item in _data.shippingOptions! { - item.serialize(buffer, true) - } - } - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .validatedRequestedInfo(let _data): - return ("validatedRequestedInfo", [("flags", ConstructorParameterDescription(_data.flags)), ("id", ConstructorParameterDescription(_data.id)), ("shippingOptions", ConstructorParameterDescription(_data.shippingOptions))]) - } - } - - public static func parse_validatedRequestedInfo(_ reader: BufferReader) -> ValidatedRequestedInfo? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - if Int(_1!) & Int(1 << 0) != 0 { - _2 = parseString(reader) - } - var _3: [Api.ShippingOption]? - if Int(_1!) & Int(1 << 1) != 0 { - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ShippingOption.self) - } - } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - if _c1 && _c2 && _c3 { - return Api.payments.ValidatedRequestedInfo.validatedRequestedInfo(Cons_validatedRequestedInfo(flags: _1!, id: _2, shippingOptions: _3)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index 36500500ca..dbd5ef3cd2 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -1,3 +1,675 @@ +public extension Api.payments { + enum StarsRevenueAdsAccountUrl: TypeConstructorDescription { + public class Cons_starsRevenueAdsAccountUrl: TypeConstructorDescription { + public var url: String + public init(url: String) { + self.url = url + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("starsRevenueAdsAccountUrl", [("url", ConstructorParameterDescription(self.url))]) + } + } + case starsRevenueAdsAccountUrl(Cons_starsRevenueAdsAccountUrl) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsRevenueAdsAccountUrl(let _data): + if boxed { + buffer.appendInt32(961445665) + } + serializeString(_data.url, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .starsRevenueAdsAccountUrl(let _data): + return ("starsRevenueAdsAccountUrl", [("url", ConstructorParameterDescription(_data.url))]) + } + } + + public static func parse_starsRevenueAdsAccountUrl(_ reader: BufferReader) -> StarsRevenueAdsAccountUrl? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.payments.StarsRevenueAdsAccountUrl.starsRevenueAdsAccountUrl(Cons_starsRevenueAdsAccountUrl(url: _1!)) + } + else { + return nil + } + } + } +} +public extension Api.payments { + enum StarsRevenueStats: TypeConstructorDescription { + public class Cons_starsRevenueStats: TypeConstructorDescription { + public var flags: Int32 + public var topHoursGraph: Api.StatsGraph? + public var revenueGraph: Api.StatsGraph + public var status: Api.StarsRevenueStatus + public var usdRate: Double + public init(flags: Int32, topHoursGraph: Api.StatsGraph?, revenueGraph: Api.StatsGraph, status: Api.StarsRevenueStatus, usdRate: Double) { + self.flags = flags + self.topHoursGraph = topHoursGraph + self.revenueGraph = revenueGraph + self.status = status + self.usdRate = usdRate + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("starsRevenueStats", [("flags", ConstructorParameterDescription(self.flags)), ("topHoursGraph", ConstructorParameterDescription(self.topHoursGraph)), ("revenueGraph", ConstructorParameterDescription(self.revenueGraph)), ("status", ConstructorParameterDescription(self.status)), ("usdRate", ConstructorParameterDescription(self.usdRate))]) + } + } + case starsRevenueStats(Cons_starsRevenueStats) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsRevenueStats(let _data): + if boxed { + buffer.appendInt32(1814066038) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 0) != 0 { + _data.topHoursGraph!.serialize(buffer, true) + } + _data.revenueGraph.serialize(buffer, true) + _data.status.serialize(buffer, true) + serializeDouble(_data.usdRate, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .starsRevenueStats(let _data): + return ("starsRevenueStats", [("flags", ConstructorParameterDescription(_data.flags)), ("topHoursGraph", ConstructorParameterDescription(_data.topHoursGraph)), ("revenueGraph", ConstructorParameterDescription(_data.revenueGraph)), ("status", ConstructorParameterDescription(_data.status)), ("usdRate", ConstructorParameterDescription(_data.usdRate))]) + } + } + + public static func parse_starsRevenueStats(_ reader: BufferReader) -> StarsRevenueStats? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.StatsGraph? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + } + var _3: Api.StatsGraph? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + var _4: Api.StarsRevenueStatus? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.StarsRevenueStatus + } + var _5: Double? + _5 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.payments.StarsRevenueStats.starsRevenueStats(Cons_starsRevenueStats(flags: _1!, topHoursGraph: _2, revenueGraph: _3!, status: _4!, usdRate: _5!)) + } + else { + return nil + } + } + } +} +public extension Api.payments { + enum StarsRevenueWithdrawalUrl: TypeConstructorDescription { + public class Cons_starsRevenueWithdrawalUrl: TypeConstructorDescription { + public var url: String + public init(url: String) { + self.url = url + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("starsRevenueWithdrawalUrl", [("url", ConstructorParameterDescription(self.url))]) + } + } + case starsRevenueWithdrawalUrl(Cons_starsRevenueWithdrawalUrl) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsRevenueWithdrawalUrl(let _data): + if boxed { + buffer.appendInt32(497778871) + } + serializeString(_data.url, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .starsRevenueWithdrawalUrl(let _data): + return ("starsRevenueWithdrawalUrl", [("url", ConstructorParameterDescription(_data.url))]) + } + } + + public static func parse_starsRevenueWithdrawalUrl(_ reader: BufferReader) -> StarsRevenueWithdrawalUrl? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.payments.StarsRevenueWithdrawalUrl.starsRevenueWithdrawalUrl(Cons_starsRevenueWithdrawalUrl(url: _1!)) + } + else { + return nil + } + } + } +} +public extension Api.payments { + enum StarsStatus: TypeConstructorDescription { + public class Cons_starsStatus: TypeConstructorDescription { + public var flags: Int32 + public var balance: Api.StarsAmount + public var subscriptions: [Api.StarsSubscription]? + public var subscriptionsNextOffset: String? + public var subscriptionsMissingBalance: Int64? + public var history: [Api.StarsTransaction]? + public var nextOffset: String? + public var chats: [Api.Chat] + public var users: [Api.User] + public init(flags: Int32, balance: Api.StarsAmount, subscriptions: [Api.StarsSubscription]?, subscriptionsNextOffset: String?, subscriptionsMissingBalance: Int64?, history: [Api.StarsTransaction]?, nextOffset: String?, chats: [Api.Chat], users: [Api.User]) { + self.flags = flags + self.balance = balance + self.subscriptions = subscriptions + self.subscriptionsNextOffset = subscriptionsNextOffset + self.subscriptionsMissingBalance = subscriptionsMissingBalance + self.history = history + self.nextOffset = nextOffset + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("starsStatus", [("flags", ConstructorParameterDescription(self.flags)), ("balance", ConstructorParameterDescription(self.balance)), ("subscriptions", ConstructorParameterDescription(self.subscriptions)), ("subscriptionsNextOffset", ConstructorParameterDescription(self.subscriptionsNextOffset)), ("subscriptionsMissingBalance", ConstructorParameterDescription(self.subscriptionsMissingBalance)), ("history", ConstructorParameterDescription(self.history)), ("nextOffset", ConstructorParameterDescription(self.nextOffset)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case starsStatus(Cons_starsStatus) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsStatus(let _data): + if boxed { + buffer.appendInt32(1822222573) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + _data.balance.serialize(buffer, true) + if Int(_data.flags) & Int(1 << 1) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.subscriptions!.count)) + for item in _data.subscriptions! { + item.serialize(buffer, true) + } + } + if Int(_data.flags) & Int(1 << 2) != 0 { + serializeString(_data.subscriptionsNextOffset!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 4) != 0 { + serializeInt64(_data.subscriptionsMissingBalance!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 3) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.history!.count)) + for item in _data.history! { + item.serialize(buffer, true) + } + } + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeString(_data.nextOffset!, buffer: buffer, boxed: false) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .starsStatus(let _data): + return ("starsStatus", [("flags", ConstructorParameterDescription(_data.flags)), ("balance", ConstructorParameterDescription(_data.balance)), ("subscriptions", ConstructorParameterDescription(_data.subscriptions)), ("subscriptionsNextOffset", ConstructorParameterDescription(_data.subscriptionsNextOffset)), ("subscriptionsMissingBalance", ConstructorParameterDescription(_data.subscriptionsMissingBalance)), ("history", ConstructorParameterDescription(_data.history)), ("nextOffset", ConstructorParameterDescription(_data.nextOffset)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_starsStatus(_ reader: BufferReader) -> StarsStatus? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.StarsAmount? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StarsAmount + } + var _3: [Api.StarsSubscription]? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsSubscription.self) + } + } + var _4: String? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _4 = parseString(reader) + } + var _5: Int64? + if Int(_1 ?? 0) & Int(1 << 4) != 0 { + _5 = reader.readInt64() + } + var _6: [Api.StarsTransaction]? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarsTransaction.self) + } + } + var _7: String? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _7 = parseString(reader) + } + var _8: [Api.Chat]? + if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _9: [Api.User]? + if let _ = reader.readInt32() { + _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.payments.StarsStatus.starsStatus(Cons_starsStatus(flags: _1!, balance: _2!, subscriptions: _3, subscriptionsNextOffset: _4, subscriptionsMissingBalance: _5, history: _6, nextOffset: _7, chats: _8!, users: _9!)) + } + else { + return nil + } + } + } +} +public extension Api.payments { + enum SuggestedStarRefBots: TypeConstructorDescription { + public class Cons_suggestedStarRefBots: TypeConstructorDescription { + public var flags: Int32 + public var count: Int32 + public var suggestedBots: [Api.StarRefProgram] + public var users: [Api.User] + public var nextOffset: String? + public init(flags: Int32, count: Int32, suggestedBots: [Api.StarRefProgram], users: [Api.User], nextOffset: String?) { + self.flags = flags + self.count = count + self.suggestedBots = suggestedBots + self.users = users + self.nextOffset = nextOffset + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("suggestedStarRefBots", [("flags", ConstructorParameterDescription(self.flags)), ("count", ConstructorParameterDescription(self.count)), ("suggestedBots", ConstructorParameterDescription(self.suggestedBots)), ("users", ConstructorParameterDescription(self.users)), ("nextOffset", ConstructorParameterDescription(self.nextOffset))]) + } + } + case suggestedStarRefBots(Cons_suggestedStarRefBots) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .suggestedStarRefBots(let _data): + if boxed { + buffer.appendInt32(-1261053863) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeInt32(_data.count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.suggestedBots.count)) + for item in _data.suggestedBots { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeString(_data.nextOffset!, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .suggestedStarRefBots(let _data): + return ("suggestedStarRefBots", [("flags", ConstructorParameterDescription(_data.flags)), ("count", ConstructorParameterDescription(_data.count)), ("suggestedBots", ConstructorParameterDescription(_data.suggestedBots)), ("users", ConstructorParameterDescription(_data.users)), ("nextOffset", ConstructorParameterDescription(_data.nextOffset))]) + } + } + + public static func parse_suggestedStarRefBots(_ reader: BufferReader) -> SuggestedStarRefBots? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.StarRefProgram]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarRefProgram.self) + } + var _4: [Api.User]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _5: String? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _5 = parseString(reader) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.payments.SuggestedStarRefBots.suggestedStarRefBots(Cons_suggestedStarRefBots(flags: _1!, count: _2!, suggestedBots: _3!, users: _4!, nextOffset: _5)) + } + else { + return nil + } + } + } +} +public extension Api.payments { + enum UniqueStarGift: TypeConstructorDescription { + public class Cons_uniqueStarGift: TypeConstructorDescription { + public var gift: Api.StarGift + public var chats: [Api.Chat] + public var users: [Api.User] + public init(gift: Api.StarGift, chats: [Api.Chat], users: [Api.User]) { + self.gift = gift + self.chats = chats + self.users = users + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("uniqueStarGift", [("gift", ConstructorParameterDescription(self.gift)), ("chats", ConstructorParameterDescription(self.chats)), ("users", ConstructorParameterDescription(self.users))]) + } + } + case uniqueStarGift(Cons_uniqueStarGift) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .uniqueStarGift(let _data): + if boxed { + buffer.appendInt32(1097619176) + } + _data.gift.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.chats.count)) + for item in _data.chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.users.count)) + for item in _data.users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .uniqueStarGift(let _data): + return ("uniqueStarGift", [("gift", ConstructorParameterDescription(_data.gift)), ("chats", ConstructorParameterDescription(_data.chats)), ("users", ConstructorParameterDescription(_data.users))]) + } + } + + public static func parse_uniqueStarGift(_ reader: BufferReader) -> UniqueStarGift? { + var _1: Api.StarGift? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StarGift + } + var _2: [Api.Chat]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _3: [Api.User]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.payments.UniqueStarGift.uniqueStarGift(Cons_uniqueStarGift(gift: _1!, chats: _2!, users: _3!)) + } + else { + return nil + } + } + } +} +public extension Api.payments { + enum UniqueStarGiftValueInfo: TypeConstructorDescription { + public class Cons_uniqueStarGiftValueInfo: TypeConstructorDescription { + public var flags: Int32 + public var currency: String + public var value: Int64 + public var initialSaleDate: Int32 + public var initialSaleStars: Int64 + public var initialSalePrice: Int64 + public var lastSaleDate: Int32? + public var lastSalePrice: Int64? + public var floorPrice: Int64? + public var averagePrice: Int64? + public var listedCount: Int32? + public var fragmentListedCount: Int32? + public var fragmentListedUrl: String? + public init(flags: Int32, currency: String, value: Int64, initialSaleDate: Int32, initialSaleStars: Int64, initialSalePrice: Int64, lastSaleDate: Int32?, lastSalePrice: Int64?, floorPrice: Int64?, averagePrice: Int64?, listedCount: Int32?, fragmentListedCount: Int32?, fragmentListedUrl: String?) { + self.flags = flags + self.currency = currency + self.value = value + self.initialSaleDate = initialSaleDate + self.initialSaleStars = initialSaleStars + self.initialSalePrice = initialSalePrice + self.lastSaleDate = lastSaleDate + self.lastSalePrice = lastSalePrice + self.floorPrice = floorPrice + self.averagePrice = averagePrice + self.listedCount = listedCount + self.fragmentListedCount = fragmentListedCount + self.fragmentListedUrl = fragmentListedUrl + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("uniqueStarGiftValueInfo", [("flags", ConstructorParameterDescription(self.flags)), ("currency", ConstructorParameterDescription(self.currency)), ("value", ConstructorParameterDescription(self.value)), ("initialSaleDate", ConstructorParameterDescription(self.initialSaleDate)), ("initialSaleStars", ConstructorParameterDescription(self.initialSaleStars)), ("initialSalePrice", ConstructorParameterDescription(self.initialSalePrice)), ("lastSaleDate", ConstructorParameterDescription(self.lastSaleDate)), ("lastSalePrice", ConstructorParameterDescription(self.lastSalePrice)), ("floorPrice", ConstructorParameterDescription(self.floorPrice)), ("averagePrice", ConstructorParameterDescription(self.averagePrice)), ("listedCount", ConstructorParameterDescription(self.listedCount)), ("fragmentListedCount", ConstructorParameterDescription(self.fragmentListedCount)), ("fragmentListedUrl", ConstructorParameterDescription(self.fragmentListedUrl))]) + } + } + case uniqueStarGiftValueInfo(Cons_uniqueStarGiftValueInfo) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .uniqueStarGiftValueInfo(let _data): + if boxed { + buffer.appendInt32(1362093126) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeString(_data.currency, buffer: buffer, boxed: false) + serializeInt64(_data.value, buffer: buffer, boxed: false) + serializeInt32(_data.initialSaleDate, buffer: buffer, boxed: false) + serializeInt64(_data.initialSaleStars, buffer: buffer, boxed: false) + serializeInt64(_data.initialSalePrice, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeInt32(_data.lastSaleDate!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeInt64(_data.lastSalePrice!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 2) != 0 { + serializeInt64(_data.floorPrice!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 3) != 0 { + serializeInt64(_data.averagePrice!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 4) != 0 { + serializeInt32(_data.listedCount!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 5) != 0 { + serializeInt32(_data.fragmentListedCount!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 5) != 0 { + serializeString(_data.fragmentListedUrl!, buffer: buffer, boxed: false) + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .uniqueStarGiftValueInfo(let _data): + return ("uniqueStarGiftValueInfo", [("flags", ConstructorParameterDescription(_data.flags)), ("currency", ConstructorParameterDescription(_data.currency)), ("value", ConstructorParameterDescription(_data.value)), ("initialSaleDate", ConstructorParameterDescription(_data.initialSaleDate)), ("initialSaleStars", ConstructorParameterDescription(_data.initialSaleStars)), ("initialSalePrice", ConstructorParameterDescription(_data.initialSalePrice)), ("lastSaleDate", ConstructorParameterDescription(_data.lastSaleDate)), ("lastSalePrice", ConstructorParameterDescription(_data.lastSalePrice)), ("floorPrice", ConstructorParameterDescription(_data.floorPrice)), ("averagePrice", ConstructorParameterDescription(_data.averagePrice)), ("listedCount", ConstructorParameterDescription(_data.listedCount)), ("fragmentListedCount", ConstructorParameterDescription(_data.fragmentListedCount)), ("fragmentListedUrl", ConstructorParameterDescription(_data.fragmentListedUrl))]) + } + } + + public static func parse_uniqueStarGiftValueInfo(_ reader: BufferReader) -> UniqueStarGiftValueInfo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int64? + _5 = reader.readInt64() + var _6: Int64? + _6 = reader.readInt64() + var _7: Int32? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _7 = reader.readInt32() + } + var _8: Int64? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _8 = reader.readInt64() + } + var _9: Int64? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _9 = reader.readInt64() + } + var _10: Int64? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _10 = reader.readInt64() + } + var _11: Int32? + if Int(_1 ?? 0) & Int(1 << 4) != 0 { + _11 = reader.readInt32() + } + var _12: Int32? + if Int(_1 ?? 0) & Int(1 << 5) != 0 { + _12 = reader.readInt32() + } + var _13: String? + if Int(_1 ?? 0) & Int(1 << 5) != 0 { + _13 = parseString(reader) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _13 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 { + return Api.payments.UniqueStarGiftValueInfo.uniqueStarGiftValueInfo(Cons_uniqueStarGiftValueInfo(flags: _1!, currency: _2!, value: _3!, initialSaleDate: _4!, initialSaleStars: _5!, initialSalePrice: _6!, lastSaleDate: _7, lastSalePrice: _8, floorPrice: _9, averagePrice: _10, listedCount: _11, fragmentListedCount: _12, fragmentListedUrl: _13)) + } + else { + return nil + } + } + } +} +public extension Api.payments { + enum ValidatedRequestedInfo: TypeConstructorDescription { + public class Cons_validatedRequestedInfo: TypeConstructorDescription { + public var flags: Int32 + public var id: String? + public var shippingOptions: [Api.ShippingOption]? + public init(flags: Int32, id: String?, shippingOptions: [Api.ShippingOption]?) { + self.flags = flags + self.id = id + self.shippingOptions = shippingOptions + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("validatedRequestedInfo", [("flags", ConstructorParameterDescription(self.flags)), ("id", ConstructorParameterDescription(self.id)), ("shippingOptions", ConstructorParameterDescription(self.shippingOptions))]) + } + } + case validatedRequestedInfo(Cons_validatedRequestedInfo) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .validatedRequestedInfo(let _data): + if boxed { + buffer.appendInt32(-784000893) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + if Int(_data.flags) & Int(1 << 0) != 0 { + serializeString(_data.id!, buffer: buffer, boxed: false) + } + if Int(_data.flags) & Int(1 << 1) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(_data.shippingOptions!.count)) + for item in _data.shippingOptions! { + item.serialize(buffer, true) + } + } + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .validatedRequestedInfo(let _data): + return ("validatedRequestedInfo", [("flags", ConstructorParameterDescription(_data.flags)), ("id", ConstructorParameterDescription(_data.id)), ("shippingOptions", ConstructorParameterDescription(_data.shippingOptions))]) + } + } + + public static func parse_validatedRequestedInfo(_ reader: BufferReader) -> ValidatedRequestedInfo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _2 = parseString(reader) + } + var _3: [Api.ShippingOption]? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.ShippingOption.self) + } + } + let _c1 = _1 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + if _c1 && _c2 && _c3 { + return Api.payments.ValidatedRequestedInfo.validatedRequestedInfo(Cons_validatedRequestedInfo(flags: _1!, id: _2, shippingOptions: _3)) + } + else { + return nil + } + } + } +} public extension Api.phone { enum ExportedGroupCallInvite: TypeConstructorDescription { public class Cons_exportedGroupCallInvite: TypeConstructorDescription { @@ -770,7 +1442,7 @@ public extension Api.premium { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Boost.self) } var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } var _5: [Api.User]? @@ -780,7 +1452,7 @@ public extension Api.premium { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.premium.BoostsList.boostsList(Cons_boostsList(flags: _1!, count: _2!, boosts: _3!, nextOffset: _4, users: _5!)) @@ -877,15 +1549,15 @@ public extension Api.premium { var _4: Int32? _4 = reader.readInt32() var _5: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _5 = reader.readInt32() } var _6: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = reader.readInt32() } var _7: Api.StatsPercentValue? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.StatsPercentValue } @@ -893,13 +1565,13 @@ public extension Api.premium { var _8: String? _8 = parseString(reader) var _9: [Api.PrepaidGiveaway]? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let _ = reader.readInt32() { _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PrepaidGiveaway.self) } } var _10: [Int32]? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let _ = reader.readInt32() { _10 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } @@ -908,12 +1580,12 @@ public extension Api.premium { let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 3) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 2) == 0) || _10 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _10 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { return Api.premium.BoostsStatus.boostsStatus(Cons_boostsStatus(flags: _1!, level: _2!, currentLevelBoosts: _3!, boosts: _4!, giftBoosts: _5, nextLevelBoosts: _6, premiumAudience: _7, boostUrl: _8!, prepaidGiveaways: _9, myBoostSlots: _10)) } @@ -1115,7 +1787,7 @@ public extension Api.smsjobs { var _6: Int32? _6 = reader.readInt32() var _7: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _7 = parseString(reader) } var _8: String? @@ -1126,7 +1798,7 @@ public extension Api.smsjobs { let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _7 != nil let _c8 = _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.smsjobs.Status.status(Cons_status(flags: _1!, recentSent: _2!, recentSince: _3!, recentRemains: _4!, totalSent: _5!, totalSince: _6!, lastGiftSlug: _7, termsUrl: _8!)) @@ -1599,6 +2271,52 @@ public extension Api.stats { } } } +public extension Api.stats { + enum PollStats: TypeConstructorDescription { + public class Cons_pollStats: TypeConstructorDescription { + public var votesGraph: Api.StatsGraph + public init(votesGraph: Api.StatsGraph) { + self.votesGraph = votesGraph + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("pollStats", [("votesGraph", ConstructorParameterDescription(self.votesGraph))]) + } + } + case pollStats(Cons_pollStats) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .pollStats(let _data): + if boxed { + buffer.appendInt32(697941741) + } + _data.votesGraph.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .pollStats(let _data): + return ("pollStats", [("votesGraph", ConstructorParameterDescription(_data.votesGraph))]) + } + } + + public static func parse_pollStats(_ reader: BufferReader) -> PollStats? { + var _1: Api.StatsGraph? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StatsGraph + } + let _c1 = _1 != nil + if _c1 { + return Api.stats.PollStats.pollStats(Cons_pollStats(votesGraph: _1!)) + } + else { + return nil + } + } + } +} public extension Api.stats { enum PublicForwards: TypeConstructorDescription { public class Cons_publicForwards: TypeConstructorDescription { @@ -1669,7 +2387,7 @@ public extension Api.stats { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PublicForward.self) } var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } var _5: [Api.Chat]? @@ -1683,7 +2401,7 @@ public extension Api.stats { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { diff --git a/submodules/TelegramApi/Sources/Api39.swift b/submodules/TelegramApi/Sources/Api39.swift index e2afa538c0..e7194b40ee 100644 --- a/submodules/TelegramApi/Sources/Api39.swift +++ b/submodules/TelegramApi/Sources/Api39.swift @@ -322,7 +322,7 @@ public extension Api.stories { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.FoundStory.self) } var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } var _5: [Api.Chat]? @@ -336,7 +336,7 @@ public extension Api.stories { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { @@ -492,7 +492,7 @@ public extension Api.stories { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryItem.self) } var _4: [Int32]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) } @@ -508,7 +508,7 @@ public extension Api.stories { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { @@ -598,7 +598,7 @@ public extension Api.stories { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } var _6: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = parseString(reader) } let _c1 = _1 != nil @@ -606,7 +606,7 @@ public extension Api.stories { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.stories.StoryReactionsList.storyReactionsList(Cons_storyReactionsList(flags: _1!, count: _2!, reactions: _3!, chats: _4!, users: _5!, nextOffset: _6)) } @@ -771,7 +771,7 @@ public extension Api.stories { _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } var _9: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _9 = parseString(reader) } let _c1 = _1 != nil @@ -782,7 +782,7 @@ public extension Api.stories { let _c6 = _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 0) == 0) || _9 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.stories.StoryViewsList.storyViewsList(Cons_storyViewsList(flags: _1!, count: _2!, viewsCount: _3!, forwardsCount: _4!, reactionsCount: _5!, views: _6!, chats: _7!, users: _8!, nextOffset: _9)) } @@ -938,7 +938,7 @@ public extension Api.updates { var _2: Int32? _2 = reader.readInt32() var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = reader.readInt32() } var _4: [Api.Message]? @@ -959,7 +959,7 @@ public extension Api.updates { } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil @@ -977,12 +977,12 @@ public extension Api.updates { var _2: Int32? _2 = reader.readInt32() var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.updates.ChannelDifference.channelDifferenceEmpty(Cons_channelDifferenceEmpty(flags: _1!, pts: _2!, timeout: _3)) } @@ -994,7 +994,7 @@ public extension Api.updates { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _2 = reader.readInt32() } var _3: Api.Dialog? @@ -1014,7 +1014,7 @@ public extension Api.updates { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index 1b2814cb3a..c6f42117da 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -386,18 +386,18 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = reader.readInt32() } var _5: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.ChannelParticipant.channelParticipant(Cons_channelParticipant(flags: _1!, userId: _2!, date: _3!, subscriptionUntilDate: _4, rank: _5)) } @@ -411,7 +411,7 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Int64? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = reader.readInt64() } var _4: Int64? @@ -423,16 +423,16 @@ public extension Api { _6 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } var _7: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _7 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.ChannelParticipant.channelParticipantAdmin(Cons_channelParticipantAdmin(flags: _1!, userId: _2!, inviterId: _3, promotedBy: _4!, date: _5!, adminRights: _6!, rank: _7)) } @@ -456,7 +456,7 @@ public extension Api { _5 = Api.parse(reader, signature: signature) as? Api.ChatBannedRights } var _6: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _6 = parseString(reader) } let _c1 = _1 != nil @@ -464,7 +464,7 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.ChannelParticipant.channelParticipantBanned(Cons_channelParticipantBanned(flags: _1!, peer: _2!, kickedBy: _3!, date: _4!, bannedRights: _5!, rank: _6)) } @@ -482,13 +482,13 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } var _4: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _4 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.ChannelParticipant.channelParticipantCreator(Cons_channelParticipantCreator(flags: _1!, userId: _2!, adminRights: _3!, rank: _4)) } @@ -519,19 +519,19 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() var _5: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = reader.readInt32() } var _6: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _6 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.ChannelParticipant.channelParticipantSelf(Cons_channelParticipantSelf(flags: _1!, userId: _2!, inviterId: _3!, date: _4!, subscriptionUntilDate: _5, rank: _6)) } @@ -721,16 +721,16 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = parseString(reader) } var _3: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = reader.readInt32() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.ChannelParticipantsFilter.channelParticipantsMentions(Cons_channelParticipantsMentions(flags: _1!, q: _2, topMsgId: _3)) } @@ -1023,13 +1023,13 @@ public extension Api { var _3: Int64? _3 = reader.readInt64() var _4: Int64? - if Int(_1!) & Int(1 << 13) != 0 { + if Int(_1 ?? 0) & Int(1 << 13) != 0 { _4 = reader.readInt64() } var _5: String? _5 = parseString(reader) var _6: String? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _6 = parseString(reader) } var _7: Api.ChatPhoto? @@ -1039,106 +1039,106 @@ public extension Api { var _8: Int32? _8 = reader.readInt32() var _9: [Api.RestrictionReason]? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { if let _ = reader.readInt32() { _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.RestrictionReason.self) } } var _10: Api.ChatAdminRights? - if Int(_1!) & Int(1 << 14) != 0 { + if Int(_1 ?? 0) & Int(1 << 14) != 0 { if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } } var _11: Api.ChatBannedRights? - if Int(_1!) & Int(1 << 15) != 0 { + if Int(_1 ?? 0) & Int(1 << 15) != 0 { if let signature = reader.readInt32() { _11 = Api.parse(reader, signature: signature) as? Api.ChatBannedRights } } var _12: Api.ChatBannedRights? - if Int(_1!) & Int(1 << 18) != 0 { + if Int(_1 ?? 0) & Int(1 << 18) != 0 { if let signature = reader.readInt32() { _12 = Api.parse(reader, signature: signature) as? Api.ChatBannedRights } } var _13: Int32? - if Int(_1!) & Int(1 << 17) != 0 { + if Int(_1 ?? 0) & Int(1 << 17) != 0 { _13 = reader.readInt32() } var _14: [Api.Username]? - if Int(_2!) & Int(1 << 0) != 0 { + if Int(_2 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Username.self) } } var _15: Api.RecentStory? - if Int(_2!) & Int(1 << 4) != 0 { + if Int(_2 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _15 = Api.parse(reader, signature: signature) as? Api.RecentStory } } var _16: Api.PeerColor? - if Int(_2!) & Int(1 << 7) != 0 { + if Int(_2 ?? 0) & Int(1 << 7) != 0 { if let signature = reader.readInt32() { _16 = Api.parse(reader, signature: signature) as? Api.PeerColor } } var _17: Api.PeerColor? - if Int(_2!) & Int(1 << 8) != 0 { + if Int(_2 ?? 0) & Int(1 << 8) != 0 { if let signature = reader.readInt32() { _17 = Api.parse(reader, signature: signature) as? Api.PeerColor } } var _18: Api.EmojiStatus? - if Int(_2!) & Int(1 << 9) != 0 { + if Int(_2 ?? 0) & Int(1 << 9) != 0 { if let signature = reader.readInt32() { _18 = Api.parse(reader, signature: signature) as? Api.EmojiStatus } } var _19: Int32? - if Int(_2!) & Int(1 << 10) != 0 { + if Int(_2 ?? 0) & Int(1 << 10) != 0 { _19 = reader.readInt32() } var _20: Int32? - if Int(_2!) & Int(1 << 11) != 0 { + if Int(_2 ?? 0) & Int(1 << 11) != 0 { _20 = reader.readInt32() } var _21: Int64? - if Int(_2!) & Int(1 << 13) != 0 { + if Int(_2 ?? 0) & Int(1 << 13) != 0 { _21 = reader.readInt64() } var _22: Int64? - if Int(_2!) & Int(1 << 14) != 0 { + if Int(_2 ?? 0) & Int(1 << 14) != 0 { _22 = reader.readInt64() } var _23: Int64? - if Int(_2!) & Int(1 << 18) != 0 { + if Int(_2 ?? 0) & Int(1 << 18) != 0 { _23 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 13) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 13) == 0) || _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 6) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 9) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 15) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 18) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 17) == 0) || _13 != nil - let _c14 = (Int(_2!) & Int(1 << 0) == 0) || _14 != nil - let _c15 = (Int(_2!) & Int(1 << 4) == 0) || _15 != nil - let _c16 = (Int(_2!) & Int(1 << 7) == 0) || _16 != nil - let _c17 = (Int(_2!) & Int(1 << 8) == 0) || _17 != nil - let _c18 = (Int(_2!) & Int(1 << 9) == 0) || _18 != nil - let _c19 = (Int(_2!) & Int(1 << 10) == 0) || _19 != nil - let _c20 = (Int(_2!) & Int(1 << 11) == 0) || _20 != nil - let _c21 = (Int(_2!) & Int(1 << 13) == 0) || _21 != nil - let _c22 = (Int(_2!) & Int(1 << 14) == 0) || _22 != nil - let _c23 = (Int(_2!) & Int(1 << 18) == 0) || _23 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 14) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 18) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _13 != nil + let _c14 = (Int(_2 ?? 0) & Int(1 << 0) == 0) || _14 != nil + let _c15 = (Int(_2 ?? 0) & Int(1 << 4) == 0) || _15 != nil + let _c16 = (Int(_2 ?? 0) & Int(1 << 7) == 0) || _16 != nil + let _c17 = (Int(_2 ?? 0) & Int(1 << 8) == 0) || _17 != nil + let _c18 = (Int(_2 ?? 0) & Int(1 << 9) == 0) || _18 != nil + let _c19 = (Int(_2 ?? 0) & Int(1 << 10) == 0) || _19 != nil + let _c20 = (Int(_2 ?? 0) & Int(1 << 11) == 0) || _20 != nil + let _c21 = (Int(_2 ?? 0) & Int(1 << 13) == 0) || _21 != nil + let _c22 = (Int(_2 ?? 0) & Int(1 << 14) == 0) || _22 != nil + let _c23 = (Int(_2 ?? 0) & Int(1 << 18) == 0) || _23 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 { return Api.Chat.channel(Cons_channel(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, title: _5!, username: _6, photo: _7!, date: _8!, restrictionReason: _9, adminRights: _10, bannedRights: _11, defaultBannedRights: _12, participantsCount: _13, usernames: _14, storiesMaxId: _15, color: _16, profileColor: _17, emojiStatus: _18, level: _19, subscriptionUntilDate: _20, botVerificationIcon: _21, sendPaidMessagesStars: _22, linkedMonoforumId: _23)) } @@ -1156,14 +1156,14 @@ public extension Api { var _4: String? _4 = parseString(reader) var _5: Int32? - if Int(_1!) & Int(1 << 16) != 0 { + if Int(_1 ?? 0) & Int(1 << 16) != 0 { _5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 16) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 16) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.Chat.channelForbidden(Cons_channelForbidden(flags: _1!, id: _2!, accessHash: _3!, title: _4!, untilDate: _5)) } @@ -1189,19 +1189,19 @@ public extension Api { var _7: Int32? _7 = reader.readInt32() var _8: Api.InputChannel? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.InputChannel } } var _9: Api.ChatAdminRights? - if Int(_1!) & Int(1 << 14) != 0 { + if Int(_1 ?? 0) & Int(1 << 14) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.ChatAdminRights } } var _10: Api.ChatBannedRights? - if Int(_1!) & Int(1 << 18) != 0 { + if Int(_1 ?? 0) & Int(1 << 18) != 0 { if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.ChatBannedRights } @@ -1213,9 +1213,9 @@ public extension Api { let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 6) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 14) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 18) == 0) || _10 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 14) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 18) == 0) || _10 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { return Api.Chat.chat(Cons_chat(flags: _1!, id: _2!, title: _3!, photo: _4!, participantsCount: _5!, date: _6!, version: _7!, migratedTo: _8, adminRights: _9, defaultBannedRights: _10)) } @@ -1766,23 +1766,23 @@ public extension Api { var _4: String? _4 = parseString(reader) var _5: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = reader.readInt32() } var _6: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _6 = reader.readInt32() } var _7: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _7 = reader.readInt32() } var _8: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _8 = reader.readInt32() } var _9: Int32? - if Int(_1!) & Int(1 << 13) != 0 { + if Int(_1 ?? 0) & Int(1 << 13) != 0 { _9 = reader.readInt32() } var _10: Int32? @@ -1800,7 +1800,7 @@ public extension Api { _14 = Api.parse(reader, signature: signature) as? Api.PeerNotifySettings } var _15: Api.ExportedChatInvite? - if Int(_1!) & Int(1 << 23) != 0 { + if Int(_1 ?? 0) & Int(1 << 23) != 0 { if let signature = reader.readInt32() { _15 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite } @@ -1810,149 +1810,149 @@ public extension Api { _16 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotInfo.self) } var _17: Int64? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _17 = reader.readInt64() } var _18: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _18 = reader.readInt32() } var _19: Int32? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _19 = reader.readInt32() } var _20: Api.StickerSet? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { if let signature = reader.readInt32() { _20 = Api.parse(reader, signature: signature) as? Api.StickerSet } } var _21: Int32? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { _21 = reader.readInt32() } var _22: Int32? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _22 = reader.readInt32() } var _23: Int64? - if Int(_1!) & Int(1 << 14) != 0 { + if Int(_1 ?? 0) & Int(1 << 14) != 0 { _23 = reader.readInt64() } var _24: Api.ChannelLocation? - if Int(_1!) & Int(1 << 15) != 0 { + if Int(_1 ?? 0) & Int(1 << 15) != 0 { if let signature = reader.readInt32() { _24 = Api.parse(reader, signature: signature) as? Api.ChannelLocation } } var _25: Int32? - if Int(_1!) & Int(1 << 17) != 0 { + if Int(_1 ?? 0) & Int(1 << 17) != 0 { _25 = reader.readInt32() } var _26: Int32? - if Int(_1!) & Int(1 << 18) != 0 { + if Int(_1 ?? 0) & Int(1 << 18) != 0 { _26 = reader.readInt32() } var _27: Int32? - if Int(_1!) & Int(1 << 12) != 0 { + if Int(_1 ?? 0) & Int(1 << 12) != 0 { _27 = reader.readInt32() } var _28: Int32? _28 = reader.readInt32() var _29: Api.InputGroupCall? - if Int(_1!) & Int(1 << 21) != 0 { + if Int(_1 ?? 0) & Int(1 << 21) != 0 { if let signature = reader.readInt32() { _29 = Api.parse(reader, signature: signature) as? Api.InputGroupCall } } var _30: Int32? - if Int(_1!) & Int(1 << 24) != 0 { + if Int(_1 ?? 0) & Int(1 << 24) != 0 { _30 = reader.readInt32() } var _31: [String]? - if Int(_1!) & Int(1 << 25) != 0 { + if Int(_1 ?? 0) & Int(1 << 25) != 0 { if let _ = reader.readInt32() { _31 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self) } } var _32: Api.Peer? - if Int(_1!) & Int(1 << 26) != 0 { + if Int(_1 ?? 0) & Int(1 << 26) != 0 { if let signature = reader.readInt32() { _32 = Api.parse(reader, signature: signature) as? Api.Peer } } var _33: String? - if Int(_1!) & Int(1 << 27) != 0 { + if Int(_1 ?? 0) & Int(1 << 27) != 0 { _33 = parseString(reader) } var _34: Int32? - if Int(_1!) & Int(1 << 28) != 0 { + if Int(_1 ?? 0) & Int(1 << 28) != 0 { _34 = reader.readInt32() } var _35: [Int64]? - if Int(_1!) & Int(1 << 28) != 0 { + if Int(_1 ?? 0) & Int(1 << 28) != 0 { if let _ = reader.readInt32() { _35 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } } var _36: Api.Peer? - if Int(_1!) & Int(1 << 29) != 0 { + if Int(_1 ?? 0) & Int(1 << 29) != 0 { if let signature = reader.readInt32() { _36 = Api.parse(reader, signature: signature) as? Api.Peer } } var _37: Api.ChatReactions? - if Int(_1!) & Int(1 << 30) != 0 { + if Int(_1 ?? 0) & Int(1 << 30) != 0 { if let signature = reader.readInt32() { _37 = Api.parse(reader, signature: signature) as? Api.ChatReactions } } var _38: Int32? - if Int(_2!) & Int(1 << 13) != 0 { + if Int(_2 ?? 0) & Int(1 << 13) != 0 { _38 = reader.readInt32() } var _39: Api.PeerStories? - if Int(_2!) & Int(1 << 4) != 0 { + if Int(_2 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _39 = Api.parse(reader, signature: signature) as? Api.PeerStories } } var _40: Api.WallPaper? - if Int(_2!) & Int(1 << 7) != 0 { + if Int(_2 ?? 0) & Int(1 << 7) != 0 { if let signature = reader.readInt32() { _40 = Api.parse(reader, signature: signature) as? Api.WallPaper } } var _41: Int32? - if Int(_2!) & Int(1 << 8) != 0 { + if Int(_2 ?? 0) & Int(1 << 8) != 0 { _41 = reader.readInt32() } var _42: Int32? - if Int(_2!) & Int(1 << 9) != 0 { + if Int(_2 ?? 0) & Int(1 << 9) != 0 { _42 = reader.readInt32() } var _43: Api.StickerSet? - if Int(_2!) & Int(1 << 10) != 0 { + if Int(_2 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _43 = Api.parse(reader, signature: signature) as? Api.StickerSet } } var _44: Api.BotVerification? - if Int(_2!) & Int(1 << 17) != 0 { + if Int(_2 ?? 0) & Int(1 << 17) != 0 { if let signature = reader.readInt32() { _44 = Api.parse(reader, signature: signature) as? Api.BotVerification } } var _45: Int32? - if Int(_2!) & Int(1 << 18) != 0 { + if Int(_2 ?? 0) & Int(1 << 18) != 0 { _45 = reader.readInt32() } var _46: Int64? - if Int(_2!) & Int(1 << 21) != 0 { + if Int(_2 ?? 0) & Int(1 << 21) != 0 { _46 = reader.readInt64() } var _47: Api.ProfileTab? - if Int(_2!) & Int(1 << 22) != 0 { + if Int(_2 ?? 0) & Int(1 << 22) != 0 { if let signature = reader.readInt32() { _47 = Api.parse(reader, signature: signature) as? Api.ProfileTab } @@ -1961,49 +1961,49 @@ public extension Api { let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 13) == 0) || _9 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 13) == 0) || _9 != nil let _c10 = _10 != nil let _c11 = _11 != nil let _c12 = _12 != nil let _c13 = _13 != nil let _c14 = _14 != nil - let _c15 = (Int(_1!) & Int(1 << 23) == 0) || _15 != nil + let _c15 = (Int(_1 ?? 0) & Int(1 << 23) == 0) || _15 != nil let _c16 = _16 != nil - let _c17 = (Int(_1!) & Int(1 << 4) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 4) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 5) == 0) || _19 != nil - let _c20 = (Int(_1!) & Int(1 << 8) == 0) || _20 != nil - let _c21 = (Int(_1!) & Int(1 << 9) == 0) || _21 != nil - let _c22 = (Int(_1!) & Int(1 << 11) == 0) || _22 != nil - let _c23 = (Int(_1!) & Int(1 << 14) == 0) || _23 != nil - let _c24 = (Int(_1!) & Int(1 << 15) == 0) || _24 != nil - let _c25 = (Int(_1!) & Int(1 << 17) == 0) || _25 != nil - let _c26 = (Int(_1!) & Int(1 << 18) == 0) || _26 != nil - let _c27 = (Int(_1!) & Int(1 << 12) == 0) || _27 != nil + let _c17 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _17 != nil + let _c18 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _18 != nil + let _c19 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _19 != nil + let _c20 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _20 != nil + let _c21 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _21 != nil + let _c22 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _22 != nil + let _c23 = (Int(_1 ?? 0) & Int(1 << 14) == 0) || _23 != nil + let _c24 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _24 != nil + let _c25 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _25 != nil + let _c26 = (Int(_1 ?? 0) & Int(1 << 18) == 0) || _26 != nil + let _c27 = (Int(_1 ?? 0) & Int(1 << 12) == 0) || _27 != nil let _c28 = _28 != nil - let _c29 = (Int(_1!) & Int(1 << 21) == 0) || _29 != nil - let _c30 = (Int(_1!) & Int(1 << 24) == 0) || _30 != nil - let _c31 = (Int(_1!) & Int(1 << 25) == 0) || _31 != nil - let _c32 = (Int(_1!) & Int(1 << 26) == 0) || _32 != nil - let _c33 = (Int(_1!) & Int(1 << 27) == 0) || _33 != nil - let _c34 = (Int(_1!) & Int(1 << 28) == 0) || _34 != nil - let _c35 = (Int(_1!) & Int(1 << 28) == 0) || _35 != nil - let _c36 = (Int(_1!) & Int(1 << 29) == 0) || _36 != nil - let _c37 = (Int(_1!) & Int(1 << 30) == 0) || _37 != nil - let _c38 = (Int(_2!) & Int(1 << 13) == 0) || _38 != nil - let _c39 = (Int(_2!) & Int(1 << 4) == 0) || _39 != nil - let _c40 = (Int(_2!) & Int(1 << 7) == 0) || _40 != nil - let _c41 = (Int(_2!) & Int(1 << 8) == 0) || _41 != nil - let _c42 = (Int(_2!) & Int(1 << 9) == 0) || _42 != nil - let _c43 = (Int(_2!) & Int(1 << 10) == 0) || _43 != nil - let _c44 = (Int(_2!) & Int(1 << 17) == 0) || _44 != nil - let _c45 = (Int(_2!) & Int(1 << 18) == 0) || _45 != nil - let _c46 = (Int(_2!) & Int(1 << 21) == 0) || _46 != nil - let _c47 = (Int(_2!) & Int(1 << 22) == 0) || _47 != nil + let _c29 = (Int(_1 ?? 0) & Int(1 << 21) == 0) || _29 != nil + let _c30 = (Int(_1 ?? 0) & Int(1 << 24) == 0) || _30 != nil + let _c31 = (Int(_1 ?? 0) & Int(1 << 25) == 0) || _31 != nil + let _c32 = (Int(_1 ?? 0) & Int(1 << 26) == 0) || _32 != nil + let _c33 = (Int(_1 ?? 0) & Int(1 << 27) == 0) || _33 != nil + let _c34 = (Int(_1 ?? 0) & Int(1 << 28) == 0) || _34 != nil + let _c35 = (Int(_1 ?? 0) & Int(1 << 28) == 0) || _35 != nil + let _c36 = (Int(_1 ?? 0) & Int(1 << 29) == 0) || _36 != nil + let _c37 = (Int(_1 ?? 0) & Int(1 << 30) == 0) || _37 != nil + let _c38 = (Int(_2 ?? 0) & Int(1 << 13) == 0) || _38 != nil + let _c39 = (Int(_2 ?? 0) & Int(1 << 4) == 0) || _39 != nil + let _c40 = (Int(_2 ?? 0) & Int(1 << 7) == 0) || _40 != nil + let _c41 = (Int(_2 ?? 0) & Int(1 << 8) == 0) || _41 != nil + let _c42 = (Int(_2 ?? 0) & Int(1 << 9) == 0) || _42 != nil + let _c43 = (Int(_2 ?? 0) & Int(1 << 10) == 0) || _43 != nil + let _c44 = (Int(_2 ?? 0) & Int(1 << 17) == 0) || _44 != nil + let _c45 = (Int(_2 ?? 0) & Int(1 << 18) == 0) || _45 != nil + let _c46 = (Int(_2 ?? 0) & Int(1 << 21) == 0) || _46 != nil + let _c47 = (Int(_2 ?? 0) & Int(1 << 22) == 0) || _47 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 && _c30 && _c31 && _c32 && _c33 && _c34 && _c35 && _c36 && _c37 && _c38 && _c39 && _c40 && _c41 && _c42 && _c43 && _c44 && _c45 && _c46 && _c47 { return Api.ChatFull.channelFull(Cons_channelFull(flags: _1!, flags2: _2!, id: _3!, about: _4!, participantsCount: _5, adminsCount: _6, kickedCount: _7, bannedCount: _8, onlineCount: _9, readInboxMaxId: _10!, readOutboxMaxId: _11!, unreadCount: _12!, chatPhoto: _13!, notifySettings: _14!, exportedInvite: _15, botInfo: _16!, migratedFromChatId: _17, migratedFromMaxId: _18, pinnedMsgId: _19, stickerset: _20, availableMinId: _21, folderId: _22, linkedChatId: _23, location: _24, slowmodeSeconds: _25, slowmodeNextSendDate: _26, statsDc: _27, pts: _28!, call: _29, ttlPeriod: _30, pendingSuggestions: _31, groupcallDefaultJoinAs: _32, themeEmoticon: _33, requestsPending: _34, recentRequesters: _35, defaultSendAs: _36, availableReactions: _37, reactionsLimit: _38, stories: _39, wallpaper: _40, boostsApplied: _41, boostsUnrestrict: _42, emojiset: _43, botVerification: _44, stargiftsCount: _45, sendPaidMessagesStars: _46, mainTab: _47)) } @@ -2023,7 +2023,7 @@ public extension Api { _4 = Api.parse(reader, signature: signature) as? Api.ChatParticipants } var _5: Api.Photo? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.Photo } @@ -2033,83 +2033,83 @@ public extension Api { _6 = Api.parse(reader, signature: signature) as? Api.PeerNotifySettings } var _7: Api.ExportedChatInvite? - if Int(_1!) & Int(1 << 13) != 0 { + if Int(_1 ?? 0) & Int(1 << 13) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite } } var _8: [Api.BotInfo]? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let _ = reader.readInt32() { _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.BotInfo.self) } } var _9: Int32? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { _9 = reader.readInt32() } var _10: Int32? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _10 = reader.readInt32() } var _11: Api.InputGroupCall? - if Int(_1!) & Int(1 << 12) != 0 { + if Int(_1 ?? 0) & Int(1 << 12) != 0 { if let signature = reader.readInt32() { _11 = Api.parse(reader, signature: signature) as? Api.InputGroupCall } } var _12: Int32? - if Int(_1!) & Int(1 << 14) != 0 { + if Int(_1 ?? 0) & Int(1 << 14) != 0 { _12 = reader.readInt32() } var _13: Api.Peer? - if Int(_1!) & Int(1 << 15) != 0 { + if Int(_1 ?? 0) & Int(1 << 15) != 0 { if let signature = reader.readInt32() { _13 = Api.parse(reader, signature: signature) as? Api.Peer } } var _14: String? - if Int(_1!) & Int(1 << 16) != 0 { + if Int(_1 ?? 0) & Int(1 << 16) != 0 { _14 = parseString(reader) } var _15: Int32? - if Int(_1!) & Int(1 << 17) != 0 { + if Int(_1 ?? 0) & Int(1 << 17) != 0 { _15 = reader.readInt32() } var _16: [Int64]? - if Int(_1!) & Int(1 << 17) != 0 { + if Int(_1 ?? 0) & Int(1 << 17) != 0 { if let _ = reader.readInt32() { _16 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) } } var _17: Api.ChatReactions? - if Int(_1!) & Int(1 << 18) != 0 { + if Int(_1 ?? 0) & Int(1 << 18) != 0 { if let signature = reader.readInt32() { _17 = Api.parse(reader, signature: signature) as? Api.ChatReactions } } var _18: Int32? - if Int(_1!) & Int(1 << 20) != 0 { + if Int(_1 ?? 0) & Int(1 << 20) != 0 { _18 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 13) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 11) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 12) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 14) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 15) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 16) == 0) || _14 != nil - let _c15 = (Int(_1!) & Int(1 << 17) == 0) || _15 != nil - let _c16 = (Int(_1!) & Int(1 << 17) == 0) || _16 != nil - let _c17 = (Int(_1!) & Int(1 << 18) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 20) == 0) || _18 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 13) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 12) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 14) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _13 != nil + let _c14 = (Int(_1 ?? 0) & Int(1 << 16) == 0) || _14 != nil + let _c15 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _15 != nil + let _c16 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _16 != nil + let _c17 = (Int(_1 ?? 0) & Int(1 << 18) == 0) || _17 != nil + let _c18 = (Int(_1 ?? 0) & Int(1 << 20) == 0) || _18 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 { return Api.ChatFull.chatFull(Cons_chatFull(flags: _1!, id: _2!, about: _3!, participants: _4!, chatPhoto: _5, notifySettings: _6!, exportedInvite: _7, botInfo: _8, pinnedMsgId: _9, folderId: _10, call: _11, ttlPeriod: _12, groupcallDefaultJoinAs: _13, themeEmoticon: _14, requestsPending: _15, recentRequesters: _16, availableReactions: _17, reactionsLimit: _18)) } @@ -2236,7 +2236,7 @@ public extension Api { var _2: String? _2 = parseString(reader) var _3: String? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _3 = parseString(reader) } var _4: Api.Photo? @@ -2246,7 +2246,7 @@ public extension Api { var _5: Int32? _5 = reader.readInt32() var _6: [Api.User]? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let _ = reader.readInt32() { _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } @@ -2254,31 +2254,31 @@ public extension Api { var _7: Int32? _7 = reader.readInt32() var _8: Api.StarsSubscriptionPricing? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.StarsSubscriptionPricing } } var _9: Int64? - if Int(_1!) & Int(1 << 12) != 0 { + if Int(_1 ?? 0) & Int(1 << 12) != 0 { _9 = reader.readInt64() } var _10: Api.BotVerification? - if Int(_1!) & Int(1 << 13) != 0 { + if Int(_1 ?? 0) & Int(1 << 13) != 0 { if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.BotVerification } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 5) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 10) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 12) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 13) == 0) || _10 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 12) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 13) == 0) || _10 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { return Api.ChatInvite.chatInvite(Cons_chatInvite(flags: _1!, title: _2!, about: _3, photo: _4!, participantsCount: _5!, participants: _6, color: _7!, subscriptionPricing: _8, subscriptionFormId: _9, botVerification: _10)) } diff --git a/submodules/TelegramApi/Sources/Api40.swift b/submodules/TelegramApi/Sources/Api40.swift index 3ed3e4f75b..27a36a3d77 100644 --- a/submodules/TelegramApi/Sources/Api40.swift +++ b/submodules/TelegramApi/Sources/Api40.swift @@ -103,6 +103,21 @@ public extension Api.functions.account { }) } } +public extension Api.functions.account { + static func confirmBotConnection(botId: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1743593320) + botId.serialize(buffer, true) + return (FunctionDescription(name: "account.confirmBotConnection", parameters: [("botId", ConstructorParameterDescription(botId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.account { static func confirmPasswordEmail(code: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -2022,6 +2037,129 @@ public extension Api.functions.account { }) } } +public extension Api.functions.aicompose { + static func createTone(flags: Int32, emojiId: Int64, title: String, prompt: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1252538643) + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(emojiId, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(prompt, buffer: buffer, boxed: false) + return (FunctionDescription(name: "aicompose.createTone", parameters: [("flags", ConstructorParameterDescription(flags)), ("emojiId", ConstructorParameterDescription(emojiId)), ("title", ConstructorParameterDescription(title)), ("prompt", ConstructorParameterDescription(prompt))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.AiComposeTone? in + let reader = BufferReader(buffer) + var result: Api.AiComposeTone? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.AiComposeTone + } + return result + }) + } +} +public extension Api.functions.aicompose { + static func deleteTone(tone: Api.InputAiComposeTone) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-583454358) + tone.serialize(buffer, true) + return (FunctionDescription(name: "aicompose.deleteTone", parameters: [("tone", ConstructorParameterDescription(tone))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} +public extension Api.functions.aicompose { + static func getTone(tone: Api.InputAiComposeTone) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1293370877) + tone.serialize(buffer, true) + return (FunctionDescription(name: "aicompose.getTone", parameters: [("tone", ConstructorParameterDescription(tone))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.aicompose.Tones? in + let reader = BufferReader(buffer) + var result: Api.aicompose.Tones? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.aicompose.Tones + } + return result + }) + } +} +public extension Api.functions.aicompose { + static func getToneExample(tone: Api.InputAiComposeTone, num: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-776688876) + tone.serialize(buffer, true) + serializeInt32(num, buffer: buffer, boxed: false) + return (FunctionDescription(name: "aicompose.getToneExample", parameters: [("tone", ConstructorParameterDescription(tone)), ("num", ConstructorParameterDescription(num))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.AiComposeToneExample? in + let reader = BufferReader(buffer) + var result: Api.AiComposeToneExample? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.AiComposeToneExample + } + return result + }) + } +} +public extension Api.functions.aicompose { + static func getTones(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1412066815) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "aicompose.getTones", parameters: [("hash", ConstructorParameterDescription(hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.aicompose.Tones? in + let reader = BufferReader(buffer) + var result: Api.aicompose.Tones? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.aicompose.Tones + } + return result + }) + } +} +public extension Api.functions.aicompose { + static func saveTone(tone: Api.InputAiComposeTone, unsave: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(394447793) + tone.serialize(buffer, true) + unsave.serialize(buffer, true) + return (FunctionDescription(name: "aicompose.saveTone", parameters: [("tone", ConstructorParameterDescription(tone)), ("unsave", ConstructorParameterDescription(unsave))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} +public extension Api.functions.aicompose { + static func updateTone(flags: Int32, tone: Api.InputAiComposeTone, displayAuthor: Api.Bool?, emojiId: Int64?, title: String?, prompt: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1875128487) + serializeInt32(flags, buffer: buffer, boxed: false) + tone.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 { + displayAuthor!.serialize(buffer, true) + } + if Int(flags) & Int(1 << 1) != 0 { + serializeInt64(emojiId!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 2) != 0 { + serializeString(title!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 3) != 0 { + serializeString(prompt!, buffer: buffer, boxed: false) + } + return (FunctionDescription(name: "aicompose.updateTone", parameters: [("flags", ConstructorParameterDescription(flags)), ("tone", ConstructorParameterDescription(tone)), ("displayAuthor", ConstructorParameterDescription(displayAuthor)), ("emojiId", ConstructorParameterDescription(emojiId)), ("title", ConstructorParameterDescription(title)), ("prompt", ConstructorParameterDescription(prompt))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.AiComposeTone? in + let reader = BufferReader(buffer) + var result: Api.AiComposeTone? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.AiComposeTone + } + return result + }) + } +} public extension Api.functions.auth { static func acceptLoginToken(token: Buffer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -2611,6 +2749,29 @@ public extension Api.functions.bots { }) } } +public extension Api.functions.bots { + static func editAccessSettings(flags: Int32, bot: Api.InputUser, addUsers: [Api.InputUser]?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(830553304) + serializeInt32(flags, buffer: buffer, boxed: false) + bot.serialize(buffer, true) + if Int(flags) & Int(1 << 1) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(addUsers!.count)) + for item in addUsers! { + item.serialize(buffer, true) + } + } + return (FunctionDescription(name: "bots.editAccessSettings", parameters: [("flags", ConstructorParameterDescription(flags)), ("bot", ConstructorParameterDescription(bot)), ("addUsers", ConstructorParameterDescription(addUsers))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.bots { static func editPreviewMedia(bot: Api.InputUser, langCode: String, media: Api.InputMedia, newMedia: Api.InputMedia) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -2645,6 +2806,21 @@ public extension Api.functions.bots { }) } } +public extension Api.functions.bots { + static func getAccessSettings(bot: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(557339555) + bot.serialize(buffer, true) + return (FunctionDescription(name: "bots.getAccessSettings", parameters: [("bot", ConstructorParameterDescription(bot))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.bots.AccessSettings? in + let reader = BufferReader(buffer) + var result: Api.bots.AccessSettings? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.bots.AccessSettings + } + return result + }) + } +} public extension Api.functions.bots { static func getAdminedBots() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.User]>) { let buffer = Buffer() @@ -5496,18 +5672,18 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func composeMessageWithAI(flags: Int32, text: Api.TextWithEntities, translateToLang: String?, changeTone: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func composeMessageWithAI(flags: Int32, text: Api.TextWithEntities, translateToLang: String?, tone: Api.InputAiComposeTone?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-45978882) + buffer.appendInt32(-622017143) serializeInt32(flags, buffer: buffer, boxed: false) text.serialize(buffer, true) if Int(flags) & Int(1 << 1) != 0 { serializeString(translateToLang!, buffer: buffer, boxed: false) } if Int(flags) & Int(1 << 2) != 0 { - serializeString(changeTone!, buffer: buffer, boxed: false) + tone!.serialize(buffer, true) } - return (FunctionDescription(name: "messages.composeMessageWithAI", parameters: [("flags", ConstructorParameterDescription(flags)), ("text", ConstructorParameterDescription(text)), ("translateToLang", ConstructorParameterDescription(translateToLang)), ("changeTone", ConstructorParameterDescription(changeTone))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.ComposedMessageWithAI? in + return (FunctionDescription(name: "messages.composeMessageWithAI", parameters: [("flags", ConstructorParameterDescription(flags)), ("text", ConstructorParameterDescription(text)), ("translateToLang", ConstructorParameterDescription(translateToLang)), ("tone", ConstructorParameterDescription(tone))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.ComposedMessageWithAI? in let reader = BufferReader(buffer) var result: Api.messages.ComposedMessageWithAI? if let signature = reader.readInt32() { @@ -5690,6 +5866,39 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func deleteParticipantReaction(peer: Api.InputPeer, msgId: Int32, participant: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-474482644) + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + participant.serialize(buffer, true) + return (FunctionDescription(name: "messages.deleteParticipantReaction", parameters: [("peer", ConstructorParameterDescription(peer)), ("msgId", ConstructorParameterDescription(msgId)), ("participant", ConstructorParameterDescription(participant))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} +public extension Api.functions.messages { + static func deleteParticipantReactions(peer: Api.InputPeer, participant: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1598550792) + peer.serialize(buffer, true) + participant.serialize(buffer, true) + return (FunctionDescription(name: "messages.deleteParticipantReactions", parameters: [("peer", ConstructorParameterDescription(peer)), ("participant", ConstructorParameterDescription(participant))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func deletePhoneCallHistory(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -7292,6 +7501,25 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getPersonalChannelHistory(userId: Api.InputUser, limit: Int32, maxId: Int32, minId: Int32, hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1442515350) + userId.serialize(buffer, true) + serializeInt32(limit, buffer: buffer, boxed: false) + serializeInt32(maxId, buffer: buffer, boxed: false) + serializeInt32(minId, buffer: buffer, boxed: false) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getPersonalChannelHistory", parameters: [("userId", ConstructorParameterDescription(userId)), ("limit", ConstructorParameterDescription(limit)), ("maxId", ConstructorParameterDescription(maxId)), ("minId", ConstructorParameterDescription(minId)), ("hash", ConstructorParameterDescription(hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.Messages? in + let reader = BufferReader(buffer) + var result: Api.messages.Messages? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.Messages + } + return result + }) + } +} public extension Api.functions.messages { static func getPinnedDialogs(folderId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -9393,6 +9621,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func setBotGuestChatResult(queryId: Int64, result: Api.InputBotInlineResult) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(86706395) + serializeInt64(queryId, buffer: buffer, boxed: false) + result.serialize(buffer, true) + return (FunctionDescription(name: "messages.setBotGuestChatResult", parameters: [("queryId", ConstructorParameterDescription(queryId)), ("result", ConstructorParameterDescription(result))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.messages { static func setBotPrecheckoutResults(flags: Int32, queryId: Int64, error: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -12446,6 +12690,23 @@ public extension Api.functions.stats { }) } } +public extension Api.functions.stats { + static func getPollStats(flags: Int32, peer: Api.InputPeer, msgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1031931288) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "stats.getPollStats", parameters: [("flags", ConstructorParameterDescription(flags)), ("peer", ConstructorParameterDescription(peer)), ("msgId", ConstructorParameterDescription(msgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stats.PollStats? in + let reader = BufferReader(buffer) + var result: Api.stats.PollStats? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.stats.PollStats + } + return result + }) + } +} public extension Api.functions.stats { static func getStoryPublicForwards(peer: Api.InputPeer, id: Int32, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramApi/Sources/Api5.swift b/submodules/TelegramApi/Sources/Api5.swift index d5bc6b26a1..a8463caf27 100644 --- a/submodules/TelegramApi/Sources/Api5.swift +++ b/submodules/TelegramApi/Sources/Api5.swift @@ -53,18 +53,18 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = parseString(reader) } var _5: Int64? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _5 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.ChatInviteImporter.chatInviteImporter(Cons_chatInviteImporter(flags: _1!, userId: _2!, date: _3!, about: _4, approvedBy: _5)) } @@ -231,14 +231,14 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() var _5: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.ChatParticipant.chatParticipant(Cons_chatParticipant(flags: _1!, userId: _2!, inviterId: _3!, date: _4!, rank: _5)) } @@ -256,14 +256,14 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() var _5: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.ChatParticipant.chatParticipantAdmin(Cons_chatParticipantAdmin(flags: _1!, userId: _2!, inviterId: _3!, date: _4!, rank: _5)) } @@ -277,12 +277,12 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.ChatParticipant.chatParticipantCreator(Cons_chatParticipantCreator(flags: _1!, userId: _2!, rank: _3)) } @@ -384,14 +384,14 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Api.ChatParticipant? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.ChatParticipant } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.ChatParticipants.chatParticipantsForbidden(Cons_chatParticipantsForbidden(flags: _1!, chatId: _2!, selfParticipant: _3)) } @@ -457,14 +457,14 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Buffer? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _3 = parseBytes(reader) } var _4: Int32? _4 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.ChatPhoto.chatPhoto(Cons_chatPhoto(flags: _1!, photoId: _2!, strippedThumb: _3, dcId: _4!)) @@ -709,25 +709,25 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: [Buffer]? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let _ = reader.readInt32() { _2 = Api.parseVector(reader, elementSignature: -1255641564, elementType: Buffer.self) } } var _3: String? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _3 = parseString(reader) } var _4: Api.Bool? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.Bool } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 6) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 8) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 8) == 0) || _4 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.CodeSettings.codeSettings(Cons_codeSettings(flags: _1!, logoutTokens: _2, token: _3, appSandbox: _4)) } @@ -974,7 +974,7 @@ public extension Api { var _24: Int32? _24 = reader.readInt32() var _25: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _25 = reader.readInt32() } var _26: Int32? @@ -988,23 +988,23 @@ public extension Api { var _30: String? _30 = parseString(reader) var _31: String? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _31 = parseString(reader) } var _32: String? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { _32 = parseString(reader) } var _33: String? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { _33 = parseString(reader) } var _34: String? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _34 = parseString(reader) } var _35: String? - if Int(_1!) & Int(1 << 12) != 0 { + if Int(_1 ?? 0) & Int(1 << 12) != 0 { _35 = parseString(reader) } var _36: Int32? @@ -1014,25 +1014,25 @@ public extension Api { var _38: Int32? _38 = reader.readInt32() var _39: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _39 = parseString(reader) } var _40: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _40 = reader.readInt32() } var _41: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _41 = reader.readInt32() } var _42: Api.Reaction? - if Int(_1!) & Int(1 << 15) != 0 { + if Int(_1 ?? 0) & Int(1 << 15) != 0 { if let signature = reader.readInt32() { _42 = Api.parse(reader, signature: signature) as? Api.Reaction } } var _43: String? - if Int(_1!) & Int(1 << 16) != 0 { + if Int(_1 ?? 0) & Int(1 << 16) != 0 { _43 = parseString(reader) } let _c1 = _1 != nil @@ -1059,25 +1059,25 @@ public extension Api { let _c22 = _22 != nil let _c23 = _23 != nil let _c24 = _24 != nil - let _c25 = (Int(_1!) & Int(1 << 0) == 0) || _25 != nil + let _c25 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _25 != nil let _c26 = _26 != nil let _c27 = _27 != nil let _c28 = _28 != nil let _c29 = _29 != nil let _c30 = _30 != nil - let _c31 = (Int(_1!) & Int(1 << 7) == 0) || _31 != nil - let _c32 = (Int(_1!) & Int(1 << 9) == 0) || _32 != nil - let _c33 = (Int(_1!) & Int(1 << 10) == 0) || _33 != nil - let _c34 = (Int(_1!) & Int(1 << 11) == 0) || _34 != nil - let _c35 = (Int(_1!) & Int(1 << 12) == 0) || _35 != nil + let _c31 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _31 != nil + let _c32 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _32 != nil + let _c33 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _33 != nil + let _c34 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _34 != nil + let _c35 = (Int(_1 ?? 0) & Int(1 << 12) == 0) || _35 != nil let _c36 = _36 != nil let _c37 = _37 != nil let _c38 = _38 != nil - let _c39 = (Int(_1!) & Int(1 << 2) == 0) || _39 != nil - let _c40 = (Int(_1!) & Int(1 << 2) == 0) || _40 != nil - let _c41 = (Int(_1!) & Int(1 << 2) == 0) || _41 != nil - let _c42 = (Int(_1!) & Int(1 << 15) == 0) || _42 != nil - let _c43 = (Int(_1!) & Int(1 << 16) == 0) || _43 != nil + let _c39 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _39 != nil + let _c40 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _40 != nil + let _c41 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _41 != nil + let _c42 = (Int(_1 ?? 0) & Int(1 << 15) == 0) || _42 != nil + let _c43 = (Int(_1 ?? 0) & Int(1 << 16) == 0) || _43 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 && _c23 && _c24 && _c25 && _c26 && _c27 && _c28 && _c29 && _c30 && _c31 && _c32 && _c33 && _c34 && _c35 && _c36 && _c37 && _c38 && _c39 && _c40 && _c41 && _c42 && _c43 { return Api.Config.config(Cons_config(flags: _1!, date: _2!, expires: _3!, testMode: _4!, thisDc: _5!, dcOptions: _6!, dcTxtDomainName: _7!, chatSizeMax: _8!, megagroupSizeMax: _9!, forwardedCountMax: _10!, onlineUpdatePeriodMs: _11!, offlineBlurTimeoutMs: _12!, offlineIdleTimeoutMs: _13!, onlineCloudTimeoutMs: _14!, notifyCloudDelayMs: _15!, notifyDefaultDelayMs: _16!, pushChatPeriodMs: _17!, pushChatLimit: _18!, editTimeLimit: _19!, revokeTimeLimit: _20!, revokePmTimeLimit: _21!, ratingEDecay: _22!, stickersRecentLimit: _23!, channelsReadMediaPeriod: _24!, tmpSessions: _25, callReceiveTimeoutMs: _26!, callRingTimeoutMs: _27!, callConnectTimeoutMs: _28!, callPacketTimeoutMs: _29!, meUrlPrefix: _30!, autoupdateUrlPrefix: _31, gifSearchUsername: _32, venueSearchUsername: _33, imgSearchUsername: _34, staticMapsProvider: _35, captionLengthMax: _36!, messageLengthMax: _37!, webfileDcId: _38!, suggestedLangCode: _39, langPackVersion: _40, baseLangPackVersion: _41, reactionsDefault: _42, autologinToken: _43)) } @@ -1219,7 +1219,7 @@ public extension Api { var _5: Int32? _5 = reader.readInt32() var _6: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = reader.readInt32() } var _7: Int64? @@ -1231,7 +1231,7 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { @@ -1498,14 +1498,14 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() var _5: Buffer? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { _5 = parseBytes(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 10) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.DcOption.dcOption(Cons_dcOption(flags: _1!, id: _2!, ipAddress: _3!, port: _4!, secret: _5)) } @@ -1702,21 +1702,21 @@ public extension Api { _10 = Api.parse(reader, signature: signature) as? Api.PeerNotifySettings } var _11: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _11 = reader.readInt32() } var _12: Api.DraftMessage? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _12 = Api.parse(reader, signature: signature) as? Api.DraftMessage } } var _13: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _13 = reader.readInt32() } var _14: Int32? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _14 = reader.readInt32() } let _c1 = _1 != nil @@ -1729,10 +1729,10 @@ public extension Api { let _c8 = _8 != nil let _c9 = _9 != nil let _c10 = _10 != nil - let _c11 = (Int(_1!) & Int(1 << 0) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 1) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 4) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 5) == 0) || _14 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _13 != nil + let _c14 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _14 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { return Api.Dialog.dialog(Cons_dialog(flags: _1!, peer: _2!, topMessage: _3!, readInboxMaxId: _4!, readOutboxMaxId: _5!, unreadCount: _6!, unreadMentionsCount: _7!, unreadReactionsCount: _8!, unreadPollVotesCount: _9!, notifySettings: _10!, pts: _11, draft: _12, folderId: _13, ttlPeriod: _14)) } @@ -1912,11 +1912,11 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } var _4: String? - if Int(_1!) & Int(1 << 25) != 0 { + if Int(_1 ?? 0) & Int(1 << 25) != 0 { _4 = parseString(reader) } var _5: Int32? - if Int(_1!) & Int(1 << 27) != 0 { + if Int(_1 ?? 0) & Int(1 << 27) != 0 { _5 = reader.readInt32() } var _6: [Api.InputPeer]? @@ -1934,8 +1934,8 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 25) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 27) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 25) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 27) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil @@ -1956,11 +1956,11 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } var _4: String? - if Int(_1!) & Int(1 << 25) != 0 { + if Int(_1 ?? 0) & Int(1 << 25) != 0 { _4 = parseString(reader) } var _5: Int32? - if Int(_1!) & Int(1 << 27) != 0 { + if Int(_1 ?? 0) & Int(1 << 27) != 0 { _5 = reader.readInt32() } var _6: [Api.InputPeer]? @@ -1974,8 +1974,8 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 25) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 27) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 25) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 27) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { diff --git a/submodules/TelegramApi/Sources/Api6.swift b/submodules/TelegramApi/Sources/Api6.swift index 355cd7863e..5fce4f058c 100644 --- a/submodules/TelegramApi/Sources/Api6.swift +++ b/submodules/TelegramApi/Sources/Api6.swift @@ -153,13 +153,13 @@ public extension Api { var _7: Int64? _7 = reader.readInt64() var _8: [Api.PhotoSize]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PhotoSize.self) } } var _9: [Api.VideoSize]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _9 = Api.parseVector(reader, elementSignature: 0, elementType: Api.VideoSize.self) } @@ -177,8 +177,8 @@ public extension Api { let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 0) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 1) == 0) || _9 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _9 != nil let _c10 = _10 != nil let _c11 = _11 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { @@ -409,22 +409,22 @@ public extension Api { var _2: Int32? _2 = reader.readInt32() var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } var _4: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = parseString(reader) } var _5: Buffer? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = parseBytes(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.DocumentAttribute.documentAttributeAudio(Cons_documentAttributeAudio(flags: _1!, duration: _2!, title: _3, performer: _4, waveform: _5)) } @@ -489,7 +489,7 @@ public extension Api { _3 = Api.parse(reader, signature: signature) as? Api.InputStickerSet } var _4: Api.MaskCoords? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.MaskCoords } @@ -497,7 +497,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.DocumentAttribute.documentAttributeSticker(Cons_documentAttributeSticker(flags: _1!, alt: _2!, stickerset: _3!, maskCoords: _4)) } @@ -515,24 +515,24 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() var _5: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = reader.readInt32() } var _6: Double? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _6 = reader.readDouble() } var _7: String? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _7 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 5) == 0) || _7 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { return Api.DocumentAttribute.documentAttributeVideo(Cons_documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!, preloadPrefixSize: _5, videoStartTs: _6, videoCodec: _7)) } @@ -635,7 +635,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.InputReplyTo? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.InputReplyTo } @@ -643,13 +643,13 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let _ = reader.readInt32() { _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _5: Api.InputMedia? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.InputMedia } @@ -657,23 +657,23 @@ public extension Api { var _6: Int32? _6 = reader.readInt32() var _7: Int64? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _7 = reader.readInt64() } var _8: Api.SuggestedPost? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.SuggestedPost } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 5) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 7) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 8) == 0) || _8 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.DraftMessage.draftMessage(Cons_draftMessage(flags: _1!, replyTo: _2, message: _3!, entities: _4, media: _5, date: _6!, effect: _7, suggestedPost: _8)) } @@ -685,11 +685,11 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil if _c1 && _c2 { return Api.DraftMessage.draftMessageEmpty(Cons_draftMessageEmpty(flags: _1!, date: _2)) } @@ -1422,12 +1422,12 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.EmojiStatus.emojiStatus(Cons_emojiStatus(flags: _1!, documentId: _2!, until: _3)) } @@ -1457,7 +1457,7 @@ public extension Api { var _10: Int32? _10 = reader.readInt32() var _11: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _11 = reader.readInt32() } let _c1 = _1 != nil @@ -1470,7 +1470,7 @@ public extension Api { let _c8 = _8 != nil let _c9 = _9 != nil let _c10 = _10 != nil - let _c11 = (Int(_1!) & Int(1 << 0) == 0) || _11 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _11 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { return Api.EmojiStatus.emojiStatusCollectible(Cons_emojiStatusCollectible(flags: _1!, collectibleId: _2!, documentId: _3!, title: _4!, slug: _5!, patternDocumentId: _6!, centerColor: _7!, edgeColor: _8!, patternColor: _9!, textColor: _10!, until: _11)) } @@ -1487,12 +1487,12 @@ public extension Api { var _2: Int64? _2 = reader.readInt64() var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.EmojiStatus.inputEmojiStatusCollectible(Cons_inputEmojiStatusCollectible(flags: _1!, collectibleId: _2!, until: _3)) } diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index 035ec1c502..38afddaed2 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -259,7 +259,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _2 = reader.readInt32() } var _3: Int32? @@ -275,7 +275,7 @@ public extension Api { var _8: Buffer? _8 = parseBytes(reader) let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil @@ -608,35 +608,35 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() var _5: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _5 = reader.readInt32() } var _6: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _6 = reader.readInt32() } var _7: Int32? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _7 = reader.readInt32() } var _8: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _8 = reader.readInt32() } var _9: Int32? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _9 = reader.readInt32() } var _10: Int32? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { _10 = reader.readInt32() } var _11: String? - if Int(_1!) & Int(1 << 8) != 0 { + if Int(_1 ?? 0) & Int(1 << 8) != 0 { _11 = parseString(reader) } var _12: Api.StarsSubscriptionPricing? - if Int(_1!) & Int(1 << 9) != 0 { + if Int(_1 ?? 0) & Int(1 << 9) != 0 { if let signature = reader.readInt32() { _12 = Api.parse(reader, signature: signature) as? Api.StarsSubscriptionPricing } @@ -645,14 +645,14 @@ public extension Api { let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 4) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 2) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 7) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 10) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 8) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 9) == 0) || _12 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 8) == 0) || _11 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _12 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { return Api.ExportedChatInvite.chatInviteExported(Cons_chatInviteExported(flags: _1!, link: _2!, adminId: _3!, date: _4!, startDate: _5, expireDate: _6, usageLimit: _7, usage: _8, requested: _9, subscriptionExpired: _10, title: _11, subscriptionPricing: _12)) } @@ -925,11 +925,11 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _2 = parseString(reader) } var _3: Api.TextWithEntities? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } @@ -937,8 +937,8 @@ public extension Api { var _4: Int64? _4 = reader.readInt64() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil let _c4 = _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.FactCheck.factCheck(Cons_factCheck(flags: _1!, country: _2, text: _3, hash: _4!)) @@ -1055,7 +1055,7 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Api.ChatPhoto? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.ChatPhoto } @@ -1063,7 +1063,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.Folder.folder(Cons_folder(flags: _1!, id: _2!, title: _3!, photo: _4)) } @@ -1242,7 +1242,7 @@ public extension Api { var _6: Int32? _6 = reader.readInt32() var _7: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _7 = reader.readInt64() } var _8: Int32? @@ -1268,7 +1268,7 @@ public extension Api { _16 = Api.parse(reader, signature: signature) as? Api.PeerNotifySettings } var _17: Api.DraftMessage? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _17 = Api.parse(reader, signature: signature) as? Api.DraftMessage } @@ -1279,7 +1279,7 @@ public extension Api { let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _7 != nil let _c8 = _8 != nil let _c9 = _9 != nil let _c10 = _10 != nil @@ -1289,7 +1289,7 @@ public extension Api { let _c14 = _14 != nil let _c15 = _15 != nil let _c16 = _16 != nil - let _c17 = (Int(_1!) & Int(1 << 4) == 0) || _17 != nil + let _c17 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _17 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { return Api.ForumTopic.forumTopic(Cons_forumTopic(flags: _1!, id: _2!, date: _3!, peer: _4!, title: _5!, iconColor: _6!, iconEmojiId: _7, topMessage: _8!, readInboxMaxId: _9!, readOutboxMaxId: _10!, unreadCount: _11!, unreadMentionsCount: _12!, unreadReactionsCount: _13!, unreadPollVotesCount: _14!, fromId: _15!, notifySettings: _16!, draft: _17)) } @@ -1436,7 +1436,7 @@ public extension Api { _7 = Api.parse(reader, signature: signature) as? Api.Photo } var _8: Api.Document? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.Document } @@ -1448,7 +1448,7 @@ public extension Api { let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 0) == 0) || _8 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.Game.game(Cons_game(flags: _1!, id: _2!, accessHash: _3!, shortName: _4!, title: _5!, description: _6!, photo: _7!, document: _8)) } @@ -1521,14 +1521,14 @@ public extension Api { var _4: Int64? _4 = reader.readInt64() var _5: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _5 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.GeoPoint.geoPoint(Cons_geoPoint(flags: _1!, long: _2!, lat: _3!, accessHash: _4!, accuracyRadius: _5)) } @@ -1596,22 +1596,22 @@ public extension Api { var _2: String? _2 = parseString(reader) var _3: String? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = parseString(reader) } var _4: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = parseString(reader) } var _5: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.GeoPointAddress.geoPointAddress(Cons_geoPointAddress(flags: _1!, countryIso2: _2!, state: _3, city: _4, street: _5)) } @@ -1666,18 +1666,18 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Int64? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _2 = reader.readInt64() } var _3: Api.DisallowedGiftsSettings? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.DisallowedGiftsSettings } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 5) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 6) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.GlobalPrivacySettings.globalPrivacySettings(Cons_globalPrivacySettings(flags: _1!, noncontactPeersPaidStars: _2, disallowedGifts: _3)) } @@ -1807,23 +1807,23 @@ public extension Api { var _4: Int32? _4 = reader.readInt32() var _5: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _5 = parseString(reader) } var _6: Int32? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { _6 = reader.readInt32() } var _7: Int32? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { _7 = reader.readInt32() } var _8: Int32? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _8 = reader.readInt32() } var _9: Int32? - if Int(_1!) & Int(1 << 10) != 0 { + if Int(_1 ?? 0) & Int(1 << 10) != 0 { _9 = reader.readInt32() } var _10: Int32? @@ -1831,15 +1831,15 @@ public extension Api { var _11: Int32? _11 = reader.readInt32() var _12: String? - if Int(_1!) & Int(1 << 16) != 0 { + if Int(_1 ?? 0) & Int(1 << 16) != 0 { _12 = parseString(reader) } var _13: Int64? - if Int(_1!) & Int(1 << 20) != 0 { + if Int(_1 ?? 0) & Int(1 << 20) != 0 { _13 = reader.readInt64() } var _14: Api.Peer? - if Int(_1!) & Int(1 << 21) != 0 { + if Int(_1 ?? 0) & Int(1 << 21) != 0 { if let signature = reader.readInt32() { _14 = Api.parse(reader, signature: signature) as? Api.Peer } @@ -1848,16 +1848,16 @@ public extension Api { let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 4) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 5) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 7) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 10) == 0) || _9 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 10) == 0) || _9 != nil let _c10 = _10 != nil let _c11 = _11 != nil - let _c12 = (Int(_1!) & Int(1 << 16) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 20) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 21) == 0) || _14 != nil + let _c12 = (Int(_1 ?? 0) & Int(1 << 16) == 0) || _12 != nil + let _c13 = (Int(_1 ?? 0) & Int(1 << 20) == 0) || _13 != nil + let _c14 = (Int(_1 ?? 0) & Int(1 << 21) == 0) || _14 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { return Api.GroupCall.groupCall(Cons_groupCall(flags: _1!, id: _2!, accessHash: _3!, participantsCount: _4!, title: _5, streamDcId: _6, recordStartDate: _7, scheduleDate: _8, unmutedVideoCount: _9, unmutedVideoLimit: _10!, version: _11!, inviteLink: _12, sendPaidMessagesStars: _13, defaultSendAs: _14)) } @@ -1927,7 +1927,7 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.Peer? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.Peer } @@ -1935,7 +1935,7 @@ public extension Api { var _3: Int64? _3 = reader.readInt64() let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 3) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _2 != nil let _c3 = _3 != nil if _c1 && _c2 && _c3 { return Api.GroupCallDonor.groupCallDonor(Cons_groupCallDonor(flags: _1!, peerId: _2, stars: _3!)) @@ -2010,7 +2010,7 @@ public extension Api { _5 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } var _6: Int64? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _6 = reader.readInt64() } let _c1 = _1 != nil @@ -2018,7 +2018,7 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.GroupCallMessage.groupCallMessage(Cons_groupCallMessage(flags: _1!, id: _2!, fromId: _3!, date: _4!, message: _5!, paidMessageStars: _6)) } diff --git a/submodules/TelegramApi/Sources/Api8.swift b/submodules/TelegramApi/Sources/Api8.swift index 0a41db4fc6..6d78d0b9ed 100644 --- a/submodules/TelegramApi/Sources/Api8.swift +++ b/submodules/TelegramApi/Sources/Api8.swift @@ -83,50 +83,50 @@ public extension Api { var _3: Int32? _3 = reader.readInt32() var _4: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _4 = reader.readInt32() } var _5: Int32? _5 = reader.readInt32() var _6: Int32? - if Int(_1!) & Int(1 << 7) != 0 { + if Int(_1 ?? 0) & Int(1 << 7) != 0 { _6 = reader.readInt32() } var _7: String? - if Int(_1!) & Int(1 << 11) != 0 { + if Int(_1 ?? 0) & Int(1 << 11) != 0 { _7 = parseString(reader) } var _8: Int64? - if Int(_1!) & Int(1 << 13) != 0 { + if Int(_1 ?? 0) & Int(1 << 13) != 0 { _8 = reader.readInt64() } var _9: Api.GroupCallParticipantVideo? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.GroupCallParticipantVideo } } var _10: Api.GroupCallParticipantVideo? - if Int(_1!) & Int(1 << 14) != 0 { + if Int(_1 ?? 0) & Int(1 << 14) != 0 { if let signature = reader.readInt32() { _10 = Api.parse(reader, signature: signature) as? Api.GroupCallParticipantVideo } } var _11: Int64? - if Int(_1!) & Int(1 << 16) != 0 { + if Int(_1 ?? 0) & Int(1 << 16) != 0 { _11 = reader.readInt64() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 3) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 7) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 13) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 16) == 0) || _11 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 13) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _9 != nil + let _c10 = (Int(_1 ?? 0) & Int(1 << 14) == 0) || _10 != nil + let _c11 = (Int(_1 ?? 0) & Int(1 << 16) == 0) || _11 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { return Api.GroupCallParticipant.groupCallParticipant(Cons_groupCallParticipant(flags: _1!, peer: _2!, date: _3!, activeDate: _4, source: _5!, volume: _6, about: _7, raiseHandRating: _8, video: _9, presentation: _10, paidStarsTotal: _11)) } @@ -192,13 +192,13 @@ public extension Api { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.GroupCallParticipantVideoSourceGroup.self) } var _4: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.GroupCallParticipantVideo.groupCallParticipantVideo(Cons_groupCallParticipantVideo(flags: _1!, endpoint: _2!, sourceGroups: _3!, audioSource: _4)) } @@ -607,6 +607,114 @@ public extension Api { } } } +public extension Api { + enum InputAiComposeTone: TypeConstructorDescription { + public class Cons_inputAiComposeToneDefault: TypeConstructorDescription { + public var tone: String + public init(tone: String) { + self.tone = tone + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("inputAiComposeToneDefault", [("tone", ConstructorParameterDescription(self.tone))]) + } + } + public class Cons_inputAiComposeToneID: TypeConstructorDescription { + public var id: Int64 + public var accessHash: Int64 + public init(id: Int64, accessHash: Int64) { + self.id = id + self.accessHash = accessHash + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("inputAiComposeToneID", [("id", ConstructorParameterDescription(self.id)), ("accessHash", ConstructorParameterDescription(self.accessHash))]) + } + } + public class Cons_inputAiComposeToneSlug: TypeConstructorDescription { + public var slug: String + public init(slug: String) { + self.slug = slug + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("inputAiComposeToneSlug", [("slug", ConstructorParameterDescription(self.slug))]) + } + } + case inputAiComposeToneDefault(Cons_inputAiComposeToneDefault) + case inputAiComposeToneID(Cons_inputAiComposeToneID) + case inputAiComposeToneSlug(Cons_inputAiComposeToneSlug) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputAiComposeToneDefault(let _data): + if boxed { + buffer.appendInt32(535407039) + } + serializeString(_data.tone, buffer: buffer, boxed: false) + break + case .inputAiComposeToneID(let _data): + if boxed { + buffer.appendInt32(125026432) + } + serializeInt64(_data.id, buffer: buffer, boxed: false) + serializeInt64(_data.accessHash, buffer: buffer, boxed: false) + break + case .inputAiComposeToneSlug(let _data): + if boxed { + buffer.appendInt32(530584407) + } + serializeString(_data.slug, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .inputAiComposeToneDefault(let _data): + return ("inputAiComposeToneDefault", [("tone", ConstructorParameterDescription(_data.tone))]) + case .inputAiComposeToneID(let _data): + return ("inputAiComposeToneID", [("id", ConstructorParameterDescription(_data.id)), ("accessHash", ConstructorParameterDescription(_data.accessHash))]) + case .inputAiComposeToneSlug(let _data): + return ("inputAiComposeToneSlug", [("slug", ConstructorParameterDescription(_data.slug))]) + } + } + + public static func parse_inputAiComposeToneDefault(_ reader: BufferReader) -> InputAiComposeTone? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputAiComposeTone.inputAiComposeToneDefault(Cons_inputAiComposeToneDefault(tone: _1!)) + } + else { + return nil + } + } + public static func parse_inputAiComposeToneID(_ reader: BufferReader) -> InputAiComposeTone? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputAiComposeTone.inputAiComposeToneID(Cons_inputAiComposeToneID(id: _1!, accessHash: _2!)) + } + else { + return nil + } + } + public static func parse_inputAiComposeToneSlug(_ reader: BufferReader) -> InputAiComposeTone? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return Api.InputAiComposeTone.inputAiComposeToneSlug(Cons_inputAiComposeToneSlug(slug: _1!)) + } + else { + return nil + } + } + } +} public extension Api { enum InputAppEvent: TypeConstructorDescription { public class Cons_inputAppEvent: TypeConstructorDescription { @@ -1069,13 +1177,13 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 2) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _2 != nil if _c1 && _c2 { return Api.InputBotInlineMessage.inputBotInlineMessageGame(Cons_inputBotInlineMessageGame(flags: _1!, replyMarkup: _2)) } @@ -1089,21 +1197,21 @@ public extension Api { var _2: String? _2 = parseString(reader) var _3: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _4: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.InputBotInlineMessage.inputBotInlineMessageMediaAuto(Cons_inputBotInlineMessageMediaAuto(flags: _1!, message: _2!, entities: _3, replyMarkup: _4)) } @@ -1123,7 +1231,7 @@ public extension Api { var _5: String? _5 = parseString(reader) var _6: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } @@ -1133,7 +1241,7 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.InputBotInlineMessage.inputBotInlineMessageMediaContact(Cons_inputBotInlineMessageMediaContact(flags: _1!, phoneNumber: _2!, firstName: _3!, lastName: _4!, vcard: _5!, replyMarkup: _6)) } @@ -1149,29 +1257,29 @@ public extension Api { _2 = Api.parse(reader, signature: signature) as? Api.InputGeoPoint } var _3: Int32? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { _3 = reader.readInt32() } var _4: Int32? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = reader.readInt32() } var _5: Int32? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _5 = reader.readInt32() } var _6: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 2) == 0) || _6 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.InputBotInlineMessage.inputBotInlineMessageMediaGeo(Cons_inputBotInlineMessageMediaGeo(flags: _1!, geoPoint: _2!, heading: _3, period: _4, proximityNotificationRadius: _5, replyMarkup: _6)) } @@ -1187,7 +1295,7 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Api.InputWebDocument? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.InputWebDocument } @@ -1205,7 +1313,7 @@ public extension Api { _8 = Api.parse(reader, signature: signature) as? Api.DataJSON } var _9: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _9 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } @@ -1213,12 +1321,12 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 2) == 0) || _9 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.InputBotInlineMessage.inputBotInlineMessageMediaInvoice(Cons_inputBotInlineMessageMediaInvoice(flags: _1!, title: _2!, description: _3!, photo: _4, invoice: _5!, payload: _6!, provider: _7!, providerData: _8!, replyMarkup: _9)) } @@ -1244,7 +1352,7 @@ public extension Api { var _7: String? _7 = parseString(reader) var _8: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } @@ -1256,7 +1364,7 @@ public extension Api { let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil - let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _8 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { return Api.InputBotInlineMessage.inputBotInlineMessageMediaVenue(Cons_inputBotInlineMessageMediaVenue(flags: _1!, geoPoint: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!, venueType: _7!, replyMarkup: _8)) } @@ -1270,7 +1378,7 @@ public extension Api { var _2: String? _2 = parseString(reader) var _3: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } @@ -1278,16 +1386,16 @@ public extension Api { var _4: String? _4 = parseString(reader) var _5: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.InputBotInlineMessage.inputBotInlineMessageMediaWebPage(Cons_inputBotInlineMessageMediaWebPage(flags: _1!, message: _2!, entities: _3, url: _4!, replyMarkup: _5)) } @@ -1301,21 +1409,21 @@ public extension Api { var _2: String? _2 = parseString(reader) var _3: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _4: Api.ReplyMarkup? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.ReplyMarkup } } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.InputBotInlineMessage.inputBotInlineMessageText(Cons_inputBotInlineMessageText(flags: _1!, message: _2!, entities: _3, replyMarkup: _4)) } @@ -1592,25 +1700,25 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = parseString(reader) } var _5: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = parseString(reader) } var _6: String? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { _6 = parseString(reader) } var _7: Api.InputWebDocument? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.InputWebDocument } } var _8: Api.InputWebDocument? - if Int(_1!) & Int(1 << 5) != 0 { + if Int(_1 ?? 0) & Int(1 << 5) != 0 { if let signature = reader.readInt32() { _8 = Api.parse(reader, signature: signature) as? Api.InputWebDocument } @@ -1622,11 +1730,11 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 5) == 0) || _8 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 5) == 0) || _8 != nil let _c9 = _9 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { return Api.InputBotInlineResult.inputBotInlineResult(Cons_inputBotInlineResult(flags: _1!, id: _2!, type: _3!, title: _4, description: _5, url: _6, thumb: _7, content: _8, sendMessage: _9!)) @@ -1643,11 +1751,11 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = parseString(reader) } var _5: String? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _5 = parseString(reader) } var _6: Api.InputDocument? @@ -1661,8 +1769,8 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil let _c6 = _6 != nil let _c7 = _7 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { @@ -1717,69 +1825,3 @@ public extension Api { } } } -public extension Api { - enum InputBusinessAwayMessage: TypeConstructorDescription { - public class Cons_inputBusinessAwayMessage: TypeConstructorDescription { - public var flags: Int32 - public var shortcutId: Int32 - public var schedule: Api.BusinessAwayMessageSchedule - public var recipients: Api.InputBusinessRecipients - public init(flags: Int32, shortcutId: Int32, schedule: Api.BusinessAwayMessageSchedule, recipients: Api.InputBusinessRecipients) { - self.flags = flags - self.shortcutId = shortcutId - self.schedule = schedule - self.recipients = recipients - } - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - return ("inputBusinessAwayMessage", [("flags", ConstructorParameterDescription(self.flags)), ("shortcutId", ConstructorParameterDescription(self.shortcutId)), ("schedule", ConstructorParameterDescription(self.schedule)), ("recipients", ConstructorParameterDescription(self.recipients))]) - } - } - case inputBusinessAwayMessage(Cons_inputBusinessAwayMessage) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputBusinessAwayMessage(let _data): - if boxed { - buffer.appendInt32(-2094959136) - } - serializeInt32(_data.flags, buffer: buffer, boxed: false) - serializeInt32(_data.shortcutId, buffer: buffer, boxed: false) - _data.schedule.serialize(buffer, true) - _data.recipients.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { - switch self { - case .inputBusinessAwayMessage(let _data): - return ("inputBusinessAwayMessage", [("flags", ConstructorParameterDescription(_data.flags)), ("shortcutId", ConstructorParameterDescription(_data.shortcutId)), ("schedule", ConstructorParameterDescription(_data.schedule)), ("recipients", ConstructorParameterDescription(_data.recipients))]) - } - } - - public static func parse_inputBusinessAwayMessage(_ reader: BufferReader) -> InputBusinessAwayMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Api.BusinessAwayMessageSchedule? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.BusinessAwayMessageSchedule - } - var _4: Api.InputBusinessRecipients? - if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.InputBusinessRecipients - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputBusinessAwayMessage.inputBusinessAwayMessage(Cons_inputBusinessAwayMessage(flags: _1!, shortcutId: _2!, schedule: _3!, recipients: _4!)) - } - else { - return nil - } - } - } -} diff --git a/submodules/TelegramApi/Sources/Api9.swift b/submodules/TelegramApi/Sources/Api9.swift index c31a55774b..aacd42f393 100644 --- a/submodules/TelegramApi/Sources/Api9.swift +++ b/submodules/TelegramApi/Sources/Api9.swift @@ -1,3 +1,69 @@ +public extension Api { + enum InputBusinessAwayMessage: TypeConstructorDescription { + public class Cons_inputBusinessAwayMessage: TypeConstructorDescription { + public var flags: Int32 + public var shortcutId: Int32 + public var schedule: Api.BusinessAwayMessageSchedule + public var recipients: Api.InputBusinessRecipients + public init(flags: Int32, shortcutId: Int32, schedule: Api.BusinessAwayMessageSchedule, recipients: Api.InputBusinessRecipients) { + self.flags = flags + self.shortcutId = shortcutId + self.schedule = schedule + self.recipients = recipients + } + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + return ("inputBusinessAwayMessage", [("flags", ConstructorParameterDescription(self.flags)), ("shortcutId", ConstructorParameterDescription(self.shortcutId)), ("schedule", ConstructorParameterDescription(self.schedule)), ("recipients", ConstructorParameterDescription(self.recipients))]) + } + } + case inputBusinessAwayMessage(Cons_inputBusinessAwayMessage) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputBusinessAwayMessage(let _data): + if boxed { + buffer.appendInt32(-2094959136) + } + serializeInt32(_data.flags, buffer: buffer, boxed: false) + serializeInt32(_data.shortcutId, buffer: buffer, boxed: false) + _data.schedule.serialize(buffer, true) + _data.recipients.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, ConstructorParameterDescription)]) { + switch self { + case .inputBusinessAwayMessage(let _data): + return ("inputBusinessAwayMessage", [("flags", ConstructorParameterDescription(_data.flags)), ("shortcutId", ConstructorParameterDescription(_data.shortcutId)), ("schedule", ConstructorParameterDescription(_data.schedule)), ("recipients", ConstructorParameterDescription(_data.recipients))]) + } + } + + public static func parse_inputBusinessAwayMessage(_ reader: BufferReader) -> InputBusinessAwayMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.BusinessAwayMessageSchedule? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.BusinessAwayMessageSchedule + } + var _4: Api.InputBusinessRecipients? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.InputBusinessRecipients + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputBusinessAwayMessage.inputBusinessAwayMessage(Cons_inputBusinessAwayMessage(flags: _1!, shortcutId: _2!, schedule: _3!, recipients: _4!)) + } + else { + return nil + } + } + } +} public extension Api { enum InputBusinessBotRecipients: TypeConstructorDescription { public class Cons_inputBusinessBotRecipients: TypeConstructorDescription { @@ -51,20 +117,20 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: [Api.InputUser]? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let _ = reader.readInt32() { _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) } } var _3: [Api.InputUser]? - if Int(_1!) & Int(1 << 6) != 0 { + if Int(_1 ?? 0) & Int(1 << 6) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 6) == 0) || _3 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 6) == 0) || _3 != nil if _c1 && _c2 && _c3 { return Api.InputBusinessBotRecipients.inputBusinessBotRecipients(Cons_inputBusinessBotRecipients(flags: _1!, users: _2, excludeUsers: _3)) } @@ -128,19 +194,19 @@ public extension Api { var _2: String? _2 = parseString(reader) var _3: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let _ = reader.readInt32() { _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) } } var _4: String? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { _4 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.InputBusinessChatLink.inputBusinessChatLink(Cons_inputBusinessChatLink(flags: _1!, message: _2!, entities: _3, title: _4)) } @@ -258,7 +324,7 @@ public extension Api { var _3: String? _3 = parseString(reader) var _4: Api.InputDocument? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _4 = Api.parse(reader, signature: signature) as? Api.InputDocument } @@ -266,7 +332,7 @@ public extension Api { let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _4 != nil if _c1 && _c2 && _c3 && _c4 { return Api.InputBusinessIntro.inputBusinessIntro(Cons_inputBusinessIntro(flags: _1!, title: _2!, description: _3!, sticker: _4)) } @@ -320,13 +386,13 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: [Api.InputUser]? - if Int(_1!) & Int(1 << 4) != 0 { + if Int(_1 ?? 0) & Int(1 << 4) != 0 { if let _ = reader.readInt32() { _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.InputUser.self) } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 4) == 0) || _2 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 4) == 0) || _2 != nil if _c1 && _c2 { return Api.InputBusinessRecipients.inputBusinessRecipients(Cons_inputBusinessRecipients(flags: _1!, users: _2)) } @@ -537,32 +603,32 @@ public extension Api { var _1: Int32? _1 = reader.readInt32() var _2: Api.InputFile? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _2 = Api.parse(reader, signature: signature) as? Api.InputFile } } var _3: Api.InputFile? - if Int(_1!) & Int(1 << 1) != 0 { + if Int(_1 ?? 0) & Int(1 << 1) != 0 { if let signature = reader.readInt32() { _3 = Api.parse(reader, signature: signature) as? Api.InputFile } } var _4: Double? - if Int(_1!) & Int(1 << 2) != 0 { + if Int(_1 ?? 0) & Int(1 << 2) != 0 { _4 = reader.readDouble() } var _5: Api.VideoSize? - if Int(_1!) & Int(1 << 3) != 0 { + if Int(_1 ?? 0) & Int(1 << 3) != 0 { if let signature = reader.readInt32() { _5 = Api.parse(reader, signature: signature) as? Api.VideoSize } } let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil + let _c2 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { return Api.InputChatPhoto.inputChatUploadedPhoto(Cons_inputChatUploadedPhoto(flags: _1!, file: _2, video: _3, videoStartTs: _4, videoEmojiMarkup: _5)) } @@ -950,7 +1016,7 @@ public extension Api { var _5: String? _5 = parseString(reader) var _6: Api.TextWithEntities? - if Int(_1!) & Int(1 << 0) != 0 { + if Int(_1 ?? 0) & Int(1 << 0) != 0 { if let signature = reader.readInt32() { _6 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } @@ -960,7 +1026,7 @@ public extension Api { let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _6 != nil if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { return Api.InputContact.inputPhoneContact(Cons_inputPhoneContact(flags: _1!, clientId: _2!, phone: _3!, firstName: _4!, lastName: _5!, note: _6)) } diff --git a/submodules/TelegramApi/Sources/SecretApiLayer101.swift b/submodules/TelegramApi/Sources/SecretApiLayer101.swift index 4c870f3f02..1d9d2acc95 100644 --- a/submodules/TelegramApi/Sources/SecretApiLayer101.swift +++ b/submodules/TelegramApi/Sources/SecretApiLayer101.swift @@ -5,58 +5,73 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[570911930] = { return $0.readInt64() } dict[571523412] = { return $0.readDouble() } dict[-1255641564] = { return parseString($0) } - dict[-1586283796] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } - dict[206520510] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } - dict[1700872964] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } - dict[-1967000459] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } - dict[1729750108] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } - dict[-217806717] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } - dict[1360072880] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } - dict[-204906213] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionRequestKey($0) } - dict[1877046107] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionAcceptKey($0) } - dict[-586814357] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionAbortKey($0) } - dict[-332526693] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionCommitKey($0) } - dict[-1473258141] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionNoop($0) } - dict[236446268] = { return SecretApi101.PhotoSize.parse_photoSizeEmpty($0) } - dict[2009052699] = { return SecretApi101.PhotoSize.parse_photoSize($0) } - dict[-374917894] = { return SecretApi101.PhotoSize.parse_photoCachedSize($0) } - dict[2086234950] = { return SecretApi101.FileLocation.parse_fileLocationUnavailable($0) } - dict[1406570614] = { return SecretApi101.FileLocation.parse_fileLocation($0) } - dict[467867529] = { return SecretApi101.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } - dict[1930838368] = { return SecretApi101.DecryptedMessage.parse_decryptedMessageService($0) } + dict[-1132882121] = { return SecretApi101.Bool.parse_boolFalse($0) } + dict[-1720552011] = { return SecretApi101.Bool.parse_boolTrue($0) } dict[-1848883596] = { return SecretApi101.DecryptedMessage.parse_decryptedMessage($0) } - dict[1815593308] = { return SecretApi101.DocumentAttribute.parse_documentAttributeImageSize($0) } + dict[1930838368] = { return SecretApi101.DecryptedMessage.parse_decryptedMessageService($0) } + dict[-586814357] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionAbortKey($0) } + dict[1877046107] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionAcceptKey($0) } + dict[-332526693] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionCommitKey($0) } + dict[1700872964] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } + dict[1729750108] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } + dict[-1473258141] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionNoop($0) } + dict[-217806717] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } + dict[206520510] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } + dict[-204906213] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionRequestKey($0) } + dict[1360072880] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } + dict[-1967000459] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } + dict[-1586283796] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } + dict[-860719551] = { return SecretApi101.DecryptedMessageAction.parse_decryptedMessageActionTyping($0) } + dict[467867529] = { return SecretApi101.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } + dict[1474341323] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } + dict[1485441687] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } + dict[2063502050] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } + dict[144661578] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } + dict[-90853155] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaExternalDocument($0) } + dict[893913689] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } + dict[-235238024] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } + dict[-1978796689] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaVenue($0) } + dict[-1760785394] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } + dict[-452652584] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaWebPage($0) } dict[297109817] = { return SecretApi101.DocumentAttribute.parse_documentAttributeAnimated($0) } - dict[358154344] = { return SecretApi101.DocumentAttribute.parse_documentAttributeFilename($0) } - dict[978674434] = { return SecretApi101.DocumentAttribute.parse_documentAttributeSticker($0) } dict[-1739392570] = { return SecretApi101.DocumentAttribute.parse_documentAttributeAudio($0) } + dict[358154344] = { return SecretApi101.DocumentAttribute.parse_documentAttributeFilename($0) } + dict[1815593308] = { return SecretApi101.DocumentAttribute.parse_documentAttributeImageSize($0) } + dict[978674434] = { return SecretApi101.DocumentAttribute.parse_documentAttributeSticker($0) } dict[250621158] = { return SecretApi101.DocumentAttribute.parse_documentAttributeVideo($0) } - dict[-2044933984] = { return SecretApi101.InputStickerSet.parse_inputStickerSetShortName($0) } + dict[1406570614] = { return SecretApi101.FileLocation.parse_fileLocation($0) } + dict[2086234950] = { return SecretApi101.FileLocation.parse_fileLocationUnavailable($0) } dict[-4838507] = { return SecretApi101.InputStickerSet.parse_inputStickerSetEmpty($0) } - dict[-1148011883] = { return SecretApi101.MessageEntity.parse_messageEntityUnknown($0) } - dict[-100378723] = { return SecretApi101.MessageEntity.parse_messageEntityMention($0) } - dict[1868782349] = { return SecretApi101.MessageEntity.parse_messageEntityHashtag($0) } - dict[1827637959] = { return SecretApi101.MessageEntity.parse_messageEntityBotCommand($0) } - dict[1859134776] = { return SecretApi101.MessageEntity.parse_messageEntityUrl($0) } - dict[1692693954] = { return SecretApi101.MessageEntity.parse_messageEntityEmail($0) } + dict[-2044933984] = { return SecretApi101.InputStickerSet.parse_inputStickerSetShortName($0) } + dict[34469328] = { return SecretApi101.MessageEntity.parse_messageEntityBlockquote($0) } dict[-1117713463] = { return SecretApi101.MessageEntity.parse_messageEntityBold($0) } - dict[-2106619040] = { return SecretApi101.MessageEntity.parse_messageEntityItalic($0) } + dict[1827637959] = { return SecretApi101.MessageEntity.parse_messageEntityBotCommand($0) } dict[681706865] = { return SecretApi101.MessageEntity.parse_messageEntityCode($0) } + dict[1692693954] = { return SecretApi101.MessageEntity.parse_messageEntityEmail($0) } + dict[1868782349] = { return SecretApi101.MessageEntity.parse_messageEntityHashtag($0) } + dict[-2106619040] = { return SecretApi101.MessageEntity.parse_messageEntityItalic($0) } + dict[-100378723] = { return SecretApi101.MessageEntity.parse_messageEntityMention($0) } dict[1938967520] = { return SecretApi101.MessageEntity.parse_messageEntityPre($0) } + dict[-1090087980] = { return SecretApi101.MessageEntity.parse_messageEntityStrike($0) } dict[1990644519] = { return SecretApi101.MessageEntity.parse_messageEntityTextUrl($0) } dict[-1672577397] = { return SecretApi101.MessageEntity.parse_messageEntityUnderline($0) } - dict[-1090087980] = { return SecretApi101.MessageEntity.parse_messageEntityStrike($0) } - dict[34469328] = { return SecretApi101.MessageEntity.parse_messageEntityBlockquote($0) } - dict[144661578] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } - dict[893913689] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } - dict[1485441687] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } - dict[1474341323] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } - dict[-90853155] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaExternalDocument($0) } - dict[-235238024] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } - dict[2063502050] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } - dict[-1760785394] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } - dict[-1978796689] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaVenue($0) } - dict[-452652584] = { return SecretApi101.DecryptedMessageMedia.parse_decryptedMessageMediaWebPage($0) } + dict[-1148011883] = { return SecretApi101.MessageEntity.parse_messageEntityUnknown($0) } + dict[1859134776] = { return SecretApi101.MessageEntity.parse_messageEntityUrl($0) } + dict[-374917894] = { return SecretApi101.PhotoSize.parse_photoCachedSize($0) } + dict[2009052699] = { return SecretApi101.PhotoSize.parse_photoSize($0) } + dict[236446268] = { return SecretApi101.PhotoSize.parse_photoSizeEmpty($0) } + dict[-44119819] = { return SecretApi101.SendMessageAction.parse_sendMessageCancelAction($0) } + dict[1653390447] = { return SecretApi101.SendMessageAction.parse_sendMessageChooseContactAction($0) } + dict[393186209] = { return SecretApi101.SendMessageAction.parse_sendMessageGeoLocationAction($0) } + dict[-718310409] = { return SecretApi101.SendMessageAction.parse_sendMessageRecordAudioAction($0) } + dict[-1997373508] = { return SecretApi101.SendMessageAction.parse_sendMessageRecordRoundAction($0) } + dict[-1584933265] = { return SecretApi101.SendMessageAction.parse_sendMessageRecordVideoAction($0) } + dict[381645902] = { return SecretApi101.SendMessageAction.parse_sendMessageTypingAction($0) } + dict[-424899985] = { return SecretApi101.SendMessageAction.parse_sendMessageUploadAudioAction($0) } + dict[-1884362354] = { return SecretApi101.SendMessageAction.parse_sendMessageUploadDocumentAction($0) } + dict[-1727382502] = { return SecretApi101.SendMessageAction.parse_sendMessageUploadPhotoAction($0) } + dict[-1150187996] = { return SecretApi101.SendMessageAction.parse_sendMessageUploadRoundAction($0) } + dict[-1845219337] = { return SecretApi101.SendMessageAction.parse_sendMessageUploadVideoAction($0) } return dict }() @@ -68,18 +83,18 @@ public struct SecretApi101 { } return nil } - - fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { - if let parser = parsers[signature] { - return parser(reader) - } - else { - telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") - return nil - } + + fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { + if let parser = parsers[signature] { + return parser(reader) } - - fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { + else { + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + return nil + } + } + + fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { if let count = reader.readInt32() { var array = [T]() var i: Int32 = 0 @@ -105,226 +120,293 @@ public struct SecretApi101 { } return nil } - + public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) { switch object { - case let _1 as SecretApi101.DecryptedMessageAction: - _1.serialize(buffer, boxed) - case let _1 as SecretApi101.PhotoSize: - _1.serialize(buffer, boxed) - case let _1 as SecretApi101.FileLocation: - _1.serialize(buffer, boxed) - case let _1 as SecretApi101.DecryptedMessageLayer: - _1.serialize(buffer, boxed) - case let _1 as SecretApi101.DecryptedMessage: - _1.serialize(buffer, boxed) - case let _1 as SecretApi101.DocumentAttribute: - _1.serialize(buffer, boxed) - case let _1 as SecretApi101.InputStickerSet: - _1.serialize(buffer, boxed) - case let _1 as SecretApi101.MessageEntity: - _1.serialize(buffer, boxed) - case let _1 as SecretApi101.DecryptedMessageMedia: - _1.serialize(buffer, boxed) - default: + case let _1 as SecretApi101.Bool: + _1.serialize(buffer, boxed) + case let _1 as SecretApi101.DecryptedMessage: + _1.serialize(buffer, boxed) + case let _1 as SecretApi101.DecryptedMessageAction: + _1.serialize(buffer, boxed) + case let _1 as SecretApi101.DecryptedMessageLayer: + _1.serialize(buffer, boxed) + case let _1 as SecretApi101.DecryptedMessageMedia: + _1.serialize(buffer, boxed) + case let _1 as SecretApi101.DocumentAttribute: + _1.serialize(buffer, boxed) + case let _1 as SecretApi101.FileLocation: + _1.serialize(buffer, boxed) + case let _1 as SecretApi101.InputStickerSet: + _1.serialize(buffer, boxed) + case let _1 as SecretApi101.MessageEntity: + _1.serialize(buffer, boxed) + case let _1 as SecretApi101.PhotoSize: + _1.serialize(buffer, boxed) + case let _1 as SecretApi101.SendMessageAction: + _1.serialize(buffer, boxed) + default: + break + } + } + + public enum Bool { + case boolFalse + case boolTrue + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boolFalse: + if boxed { + buffer.appendInt32(-1132882121) + } break + case .boolTrue: + if boxed { + buffer.appendInt32(-1720552011) + } + break + } + } + + fileprivate static func parse_boolFalse(_ reader: BufferReader) -> Bool? { + return SecretApi101.Bool.boolFalse + } + fileprivate static func parse_boolTrue(_ reader: BufferReader) -> Bool? { + return SecretApi101.Bool.boolTrue + } + } + + public enum DecryptedMessage { + case decryptedMessage(flags: Int32, randomId: Int64, ttl: Int32, message: String, media: SecretApi101.DecryptedMessageMedia?, entities: [SecretApi101.MessageEntity]?, viaBotName: String?, replyToRandomId: Int64?, groupedId: Int64?) + case decryptedMessageService(randomId: Int64, action: SecretApi101.DecryptedMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessage(let flags, let randomId, let ttl, let message, let media, let entities, let viaBotName, let replyToRandomId, let groupedId): + if boxed { + buffer.appendInt32(-1848883596) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(randomId, buffer: buffer, boxed: false) + serializeInt32(ttl, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 9) != 0 { + media!.serialize(buffer, true) + } + if Int(flags) & Int(1 << 7) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + } + } + if Int(flags) & Int(1 << 11) != 0 { + serializeString(viaBotName!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 3) != 0 { + serializeInt64(replyToRandomId!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 17) != 0 { + serializeInt64(groupedId!, buffer: buffer, boxed: false) + } + break + case .decryptedMessageService(let randomId, let action): + if boxed { + buffer.appendInt32(1930838368) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: SecretApi101.DecryptedMessageMedia? + if Int(_1 ?? 0) & Int(1 << 9) != 0 { + if let signature = reader.readInt32() { + _5 = SecretApi101.parse(reader, signature: signature) as? SecretApi101.DecryptedMessageMedia + } + } + var _6: [SecretApi101.MessageEntity]? + if Int(_1 ?? 0) & Int(1 << 7) != 0 { + if let _ = reader.readInt32() { + _6 = SecretApi101.parseVector(reader, elementSignature: 0, elementType: SecretApi101.MessageEntity.self) + } + } + var _7: String? + if Int(_1 ?? 0) & Int(1 << 11) != 0 { + _7 = parseString(reader) + } + var _8: Int64? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _8 = reader.readInt64() + } + var _9: Int64? + if Int(_1 ?? 0) & Int(1 << 17) != 0 { + _9 = reader.readInt64() + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi101.DecryptedMessage.decryptedMessage(flags: _1!, randomId: _2!, ttl: _3!, message: _4!, media: _5, entities: _6, viaBotName: _7, replyToRandomId: _8, groupedId: _9) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: SecretApi101.DecryptedMessageAction? + if let signature = reader.readInt32() { + _2 = SecretApi101.parse(reader, signature: signature) as? SecretApi101.DecryptedMessageAction + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) + } + else { + return nil + } } } public enum DecryptedMessageAction { - case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) - case decryptedMessageActionReadMessages(randomIds: [Int64]) - case decryptedMessageActionDeleteMessages(randomIds: [Int64]) - case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) - case decryptedMessageActionFlushHistory - case decryptedMessageActionNotifyLayer(layer: Int32) - case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) - case decryptedMessageActionRequestKey(exchangeId: Int64, gA: Buffer) - case decryptedMessageActionAcceptKey(exchangeId: Int64, gB: Buffer, keyFingerprint: Int64) case decryptedMessageActionAbortKey(exchangeId: Int64) + case decryptedMessageActionAcceptKey(exchangeId: Int64, gB: Buffer, keyFingerprint: Int64) case decryptedMessageActionCommitKey(exchangeId: Int64, keyFingerprint: Int64) + case decryptedMessageActionDeleteMessages(randomIds: [Int64]) + case decryptedMessageActionFlushHistory case decryptedMessageActionNoop - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageActionSetMessageTTL(let ttlSeconds): - if boxed { - buffer.appendInt32(-1586283796) - } - serializeInt32(ttlSeconds, buffer: buffer, boxed: false) - break - case .decryptedMessageActionReadMessages(let randomIds): - if boxed { - buffer.appendInt32(206520510) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionDeleteMessages(let randomIds): - if boxed { - buffer.appendInt32(1700872964) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionScreenshotMessages(let randomIds): - if boxed { - buffer.appendInt32(-1967000459) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionFlushHistory: - if boxed { - buffer.appendInt32(1729750108) - } - - break - case .decryptedMessageActionNotifyLayer(let layer): - if boxed { - buffer.appendInt32(-217806717) - } - serializeInt32(layer, buffer: buffer, boxed: false) - break - case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): - if boxed { - buffer.appendInt32(1360072880) - } - serializeInt32(startSeqNo, buffer: buffer, boxed: false) - serializeInt32(endSeqNo, buffer: buffer, boxed: false) - break - case .decryptedMessageActionRequestKey(let exchangeId, let gA): - if boxed { - buffer.appendInt32(-204906213) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - serializeBytes(gA, buffer: buffer, boxed: false) - break - case .decryptedMessageActionAcceptKey(let exchangeId, let gB, let keyFingerprint): - if boxed { - buffer.appendInt32(1877046107) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - serializeBytes(gB, buffer: buffer, boxed: false) - serializeInt64(keyFingerprint, buffer: buffer, boxed: false) - break - case .decryptedMessageActionAbortKey(let exchangeId): - if boxed { - buffer.appendInt32(-586814357) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - break - case .decryptedMessageActionCommitKey(let exchangeId, let keyFingerprint): - if boxed { - buffer.appendInt32(-332526693) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - serializeInt64(keyFingerprint, buffer: buffer, boxed: false) - break - case .decryptedMessageActionNoop: - if boxed { - buffer.appendInt32(-1473258141) - } - - break - } - } - fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return SecretApi101.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) - } - else { - return nil + case decryptedMessageActionNotifyLayer(layer: Int32) + case decryptedMessageActionReadMessages(randomIds: [Int64]) + case decryptedMessageActionRequestKey(exchangeId: Int64, gA: Buffer) + case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) + case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) + case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) + case decryptedMessageActionTyping(action: SecretApi101.SendMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageActionAbortKey(let exchangeId): + if boxed { + buffer.appendInt32(-586814357) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + break + case .decryptedMessageActionAcceptKey(let exchangeId, let gB, let keyFingerprint): + if boxed { + buffer.appendInt32(1877046107) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gB, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionCommitKey(let exchangeId, let keyFingerprint): + if boxed { + buffer.appendInt32(-332526693) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionDeleteMessages(let randomIds): + if boxed { + buffer.appendInt32(1700872964) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionFlushHistory: + if boxed { + buffer.appendInt32(1729750108) + } + break + case .decryptedMessageActionNoop: + if boxed { + buffer.appendInt32(-1473258141) + } + break + case .decryptedMessageActionNotifyLayer(let layer): + if boxed { + buffer.appendInt32(-217806717) + } + serializeInt32(layer, buffer: buffer, boxed: false) + break + case .decryptedMessageActionReadMessages(let randomIds): + if boxed { + buffer.appendInt32(206520510) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionRequestKey(let exchangeId, let gA): + if boxed { + buffer.appendInt32(-204906213) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gA, buffer: buffer, boxed: false) + break + case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): + if boxed { + buffer.appendInt32(1360072880) + } + serializeInt32(startSeqNo, buffer: buffer, boxed: false) + serializeInt32(endSeqNo, buffer: buffer, boxed: false) + break + case .decryptedMessageActionScreenshotMessages(let randomIds): + if boxed { + buffer.appendInt32(-1967000459) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionSetMessageTTL(let ttlSeconds): + if boxed { + buffer.appendInt32(-1586283796) + } + serializeInt32(ttlSeconds, buffer: buffer, boxed: false) + break + case .decryptedMessageActionTyping(let action): + if boxed { + buffer.appendInt32(-860719551) + } + action.serialize(buffer, true) + break } } - fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi101.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi101.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi101.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi101.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi101.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi101.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { - return SecretApi101.DecryptedMessageAction.decryptedMessageActionFlushHistory - } - fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return SecretApi101.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionRequestKey(_ reader: BufferReader) -> DecryptedMessageAction? { + + fileprivate static func parse_decryptedMessageActionAbortKey(_ reader: BufferReader) -> DecryptedMessageAction? { var _1: Int64? _1 = reader.readInt64() - var _2: Buffer? - _2 = parseBytes(reader) let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.DecryptedMessageAction.decryptedMessageActionRequestKey(exchangeId: _1!, gA: _2!) + if _c1 { + return SecretApi101.DecryptedMessageAction.decryptedMessageActionAbortKey(exchangeId: _1!) } else { return nil @@ -347,17 +429,6 @@ public struct SecretApi101 { return nil } } - fileprivate static func parse_decryptedMessageActionAbortKey(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return SecretApi101.DecryptedMessageAction.decryptedMessageActionAbortKey(exchangeId: _1!) - } - else { - return nil - } - } fileprivate static func parse_decryptedMessageActionCommitKey(_ reader: BufferReader) -> DecryptedMessageAction? { var _1: Int64? _1 = reader.readInt64() @@ -372,196 +443,134 @@ public struct SecretApi101 { return nil } } + fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi101.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi101.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi101.DecryptedMessageAction.decryptedMessageActionFlushHistory + } fileprivate static func parse_decryptedMessageActionNoop(_ reader: BufferReader) -> DecryptedMessageAction? { return SecretApi101.DecryptedMessageAction.decryptedMessageActionNoop } - - - } - - public enum PhotoSize { - case photoSizeEmpty(type: String) - case photoSize(type: String, location: SecretApi101.FileLocation, w: Int32, h: Int32, size: Int32) - case photoCachedSize(type: String, location: SecretApi101.FileLocation, w: Int32, h: Int32, bytes: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .photoSizeEmpty(let type): - if boxed { - buffer.appendInt32(236446268) - } - serializeString(type, buffer: buffer, boxed: false) - break - case .photoSize(let type, let location, let w, let h, let size): - if boxed { - buffer.appendInt32(2009052699) - } - serializeString(type, buffer: buffer, boxed: false) - location.serialize(buffer, true) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - break - case .photoCachedSize(let type, let location, let w, let h, let bytes): - if boxed { - buffer.appendInt32(-374917894) - } - serializeString(type, buffer: buffer, boxed: false) - location.serialize(buffer, true) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeBytes(bytes, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_photoSizeEmpty(_ reader: BufferReader) -> PhotoSize? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return SecretApi101.PhotoSize.photoSizeEmpty(type: _1!) - } - else { - return nil - } - } - fileprivate static func parse_photoSize(_ reader: BufferReader) -> PhotoSize? { - var _1: String? - _1 = parseString(reader) - var _2: SecretApi101.FileLocation? - if let signature = reader.readInt32() { - _2 = SecretApi101.parse(reader, signature: signature) as? SecretApi101.FileLocation - } - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi101.PhotoSize.photoSize(type: _1!, location: _2!, w: _3!, h: _4!, size: _5!) - } - else { - return nil - } - } - fileprivate static func parse_photoCachedSize(_ reader: BufferReader) -> PhotoSize? { - var _1: String? - _1 = parseString(reader) - var _2: SecretApi101.FileLocation? - if let signature = reader.readInt32() { - _2 = SecretApi101.parse(reader, signature: signature) as? SecretApi101.FileLocation - } - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Buffer? - _5 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi101.PhotoSize.photoCachedSize(type: _1!, location: _2!, w: _3!, h: _4!, bytes: _5!) - } - else { - return nil - } - } - - - } - - public enum FileLocation { - case fileLocationUnavailable(volumeId: Int64, localId: Int32, secret: Int64) - case fileLocation(dcId: Int32, volumeId: Int64, localId: Int32, secret: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .fileLocationUnavailable(let volumeId, let localId, let secret): - if boxed { - buffer.appendInt32(2086234950) - } - serializeInt64(volumeId, buffer: buffer, boxed: false) - serializeInt32(localId, buffer: buffer, boxed: false) - serializeInt64(secret, buffer: buffer, boxed: false) - break - case .fileLocation(let dcId, let volumeId, let localId, let secret): - if boxed { - buffer.appendInt32(1406570614) - } - serializeInt32(dcId, buffer: buffer, boxed: false) - serializeInt64(volumeId, buffer: buffer, boxed: false) - serializeInt32(localId, buffer: buffer, boxed: false) - serializeInt64(secret, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_fileLocationUnavailable(_ reader: BufferReader) -> FileLocation? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi101.FileLocation.fileLocationUnavailable(volumeId: _1!, localId: _2!, secret: _3!) - } - else { - return nil - } - } - fileprivate static func parse_fileLocation(_ reader: BufferReader) -> FileLocation? { + fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { var _1: Int32? _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int64? - _4 = reader.readInt64() let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return SecretApi101.FileLocation.fileLocation(dcId: _1!, volumeId: _2!, localId: _3!, secret: _4!) + if _c1 { + return SecretApi101.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi101.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi101.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionRequestKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.DecryptedMessageAction.decryptedMessageActionRequestKey(exchangeId: _1!, gA: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi101.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi101.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi101.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionTyping(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: SecretApi101.SendMessageAction? + if let signature = reader.readInt32() { + _1 = SecretApi101.parse(reader, signature: signature) as? SecretApi101.SendMessageAction + } + let _c1 = _1 != nil + if _c1 { + return SecretApi101.DecryptedMessageAction.decryptedMessageActionTyping(action: _1!) } else { return nil } } - - } public enum DecryptedMessageLayer { case decryptedMessageLayer(randomBytes: Buffer, layer: Int32, inSeqNo: Int32, outSeqNo: Int32, message: SecretApi101.DecryptedMessage) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): - if boxed { - buffer.appendInt32(467867529) - } - serializeBytes(randomBytes, buffer: buffer, boxed: false) - serializeInt32(layer, buffer: buffer, boxed: false) - serializeInt32(inSeqNo, buffer: buffer, boxed: false) - serializeInt32(outSeqNo, buffer: buffer, boxed: false) - message.serialize(buffer, true) - break - } - } + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): + if boxed { + buffer.appendInt32(467867529) + } + serializeBytes(randomBytes, buffer: buffer, boxed: false) + serializeInt32(layer, buffer: buffer, boxed: false) + serializeInt32(inSeqNo, buffer: buffer, boxed: false) + serializeInt32(outSeqNo, buffer: buffer, boxed: false) + message.serialize(buffer, true) + break + } + } + fileprivate static func parse_decryptedMessageLayer(_ reader: BufferReader) -> DecryptedMessageLayer? { var _1: Buffer? _1 = parseBytes(reader) @@ -587,755 +596,156 @@ public struct SecretApi101 { return nil } } - - - } - - public enum DecryptedMessage { - case decryptedMessageService(randomId: Int64, action: SecretApi101.DecryptedMessageAction) - case decryptedMessage(flags: Int32, randomId: Int64, ttl: Int32, message: String, media: SecretApi101.DecryptedMessageMedia?, entities: [SecretApi101.MessageEntity]?, viaBotName: String?, replyToRandomId: Int64?, groupedId: Int64?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageService(let randomId, let action): - if boxed { - buffer.appendInt32(1930838368) - } - serializeInt64(randomId, buffer: buffer, boxed: false) - action.serialize(buffer, true) - break - case .decryptedMessage(let flags, let randomId, let ttl, let message, let media, let entities, let viaBotName, let replyToRandomId, let groupedId): - if boxed { - buffer.appendInt32(-1848883596) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(randomId, buffer: buffer, boxed: false) - serializeInt32(ttl, buffer: buffer, boxed: false) - serializeString(message, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 9) != 0 {media!.serialize(buffer, true)} - if Int(flags) & Int(1 << 7) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(entities!.count)) - for item in entities! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 11) != 0 {serializeString(viaBotName!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {serializeInt64(replyToRandomId!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 17) != 0 {serializeInt64(groupedId!, buffer: buffer, boxed: false)} - break - } - } - fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { - var _1: Int64? - _1 = reader.readInt64() - var _2: SecretApi101.DecryptedMessageAction? - if let signature = reader.readInt32() { - _2 = SecretApi101.parse(reader, signature: signature) as? SecretApi101.DecryptedMessageAction - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() - var _4: String? - _4 = parseString(reader) - var _5: SecretApi101.DecryptedMessageMedia? - if Int(_1!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { - _5 = SecretApi101.parse(reader, signature: signature) as? SecretApi101.DecryptedMessageMedia - } } - var _6: [SecretApi101.MessageEntity]? - if Int(_1!) & Int(1 << 7) != 0 {if let _ = reader.readInt32() { - _6 = SecretApi101.parseVector(reader, elementSignature: 0, elementType: SecretApi101.MessageEntity.self) - } } - var _7: String? - if Int(_1!) & Int(1 << 11) != 0 {_7 = parseString(reader) } - var _8: Int64? - if Int(_1!) & Int(1 << 3) != 0 {_8 = reader.readInt64() } - var _9: Int64? - if Int(_1!) & Int(1 << 17) != 0 {_9 = reader.readInt64() } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 9) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 7) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 17) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return SecretApi101.DecryptedMessage.decryptedMessage(flags: _1!, randomId: _2!, ttl: _3!, message: _4!, media: _5, entities: _6, viaBotName: _7, replyToRandomId: _8, groupedId: _9) - } - else { - return nil - } - } - - - } - - public enum DocumentAttribute { - case documentAttributeImageSize(w: Int32, h: Int32) - case documentAttributeAnimated - case documentAttributeFilename(fileName: String) - case documentAttributeSticker(alt: String, stickerset: SecretApi101.InputStickerSet) - case documentAttributeAudio(flags: Int32, duration: Int32, title: String?, performer: String?, waveform: Buffer?) - case documentAttributeVideo(flags: Int32, duration: Int32, w: Int32, h: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .documentAttributeImageSize(let w, let h): - if boxed { - buffer.appendInt32(1815593308) - } - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - break - case .documentAttributeAnimated: - if boxed { - buffer.appendInt32(297109817) - } - - break - case .documentAttributeFilename(let fileName): - if boxed { - buffer.appendInt32(358154344) - } - serializeString(fileName, buffer: buffer, boxed: false) - break - case .documentAttributeSticker(let alt, let stickerset): - if boxed { - buffer.appendInt32(978674434) - } - serializeString(alt, buffer: buffer, boxed: false) - stickerset.serialize(buffer, true) - break - case .documentAttributeAudio(let flags, let duration, let title, let performer, let waveform): - if boxed { - buffer.appendInt32(-1739392570) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(duration, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(title!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(performer!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {serializeBytes(waveform!, buffer: buffer, boxed: false)} - break - case .documentAttributeVideo(let flags, let duration, let w, let h): - if boxed { - buffer.appendInt32(250621158) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(duration, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_documentAttributeImageSize(_ reader: BufferReader) -> DocumentAttribute? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.DocumentAttribute.documentAttributeImageSize(w: _1!, h: _2!) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeAnimated(_ reader: BufferReader) -> DocumentAttribute? { - return SecretApi101.DocumentAttribute.documentAttributeAnimated - } - fileprivate static func parse_documentAttributeFilename(_ reader: BufferReader) -> DocumentAttribute? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return SecretApi101.DocumentAttribute.documentAttributeFilename(fileName: _1!) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeSticker(_ reader: BufferReader) -> DocumentAttribute? { - var _1: String? - _1 = parseString(reader) - var _2: SecretApi101.InputStickerSet? - if let signature = reader.readInt32() { - _2 = SecretApi101.parse(reader, signature: signature) as? SecretApi101.InputStickerSet - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.DocumentAttribute.documentAttributeSticker(alt: _1!, stickerset: _2!) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeAudio(_ reader: BufferReader) -> DocumentAttribute? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } - var _4: String? - if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } - var _5: Buffer? - if Int(_1!) & Int(1 << 2) != 0 {_5 = parseBytes(reader) } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi101.DocumentAttribute.documentAttributeAudio(flags: _1!, duration: _2!, title: _3, performer: _4, waveform: _5) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeVideo(_ reader: BufferReader) -> DocumentAttribute? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return SecretApi101.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!) - } - else { - return nil - } - } - - - } - - public enum InputStickerSet { - case inputStickerSetShortName(shortName: String) - case inputStickerSetEmpty - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputStickerSetShortName(let shortName): - if boxed { - buffer.appendInt32(-2044933984) - } - serializeString(shortName, buffer: buffer, boxed: false) - break - case .inputStickerSetEmpty: - if boxed { - buffer.appendInt32(-4838507) - } - - break - } - } - fileprivate static func parse_inputStickerSetShortName(_ reader: BufferReader) -> InputStickerSet? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return SecretApi101.InputStickerSet.inputStickerSetShortName(shortName: _1!) - } - else { - return nil - } - } - fileprivate static func parse_inputStickerSetEmpty(_ reader: BufferReader) -> InputStickerSet? { - return SecretApi101.InputStickerSet.inputStickerSetEmpty - } - - - } - - public enum MessageEntity { - case messageEntityUnknown(offset: Int32, length: Int32) - case messageEntityMention(offset: Int32, length: Int32) - case messageEntityHashtag(offset: Int32, length: Int32) - case messageEntityBotCommand(offset: Int32, length: Int32) - case messageEntityUrl(offset: Int32, length: Int32) - case messageEntityEmail(offset: Int32, length: Int32) - case messageEntityBold(offset: Int32, length: Int32) - case messageEntityItalic(offset: Int32, length: Int32) - case messageEntityCode(offset: Int32, length: Int32) - case messageEntityPre(offset: Int32, length: Int32, language: String) - case messageEntityTextUrl(offset: Int32, length: Int32, url: String) - case messageEntityUnderline(offset: Int32, length: Int32) - case messageEntityStrike(offset: Int32, length: Int32) - case messageEntityBlockquote(offset: Int32, length: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .messageEntityUnknown(let offset, let length): - if boxed { - buffer.appendInt32(-1148011883) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityMention(let offset, let length): - if boxed { - buffer.appendInt32(-100378723) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityHashtag(let offset, let length): - if boxed { - buffer.appendInt32(1868782349) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityBotCommand(let offset, let length): - if boxed { - buffer.appendInt32(1827637959) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityUrl(let offset, let length): - if boxed { - buffer.appendInt32(1859134776) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityEmail(let offset, let length): - if boxed { - buffer.appendInt32(1692693954) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityBold(let offset, let length): - if boxed { - buffer.appendInt32(-1117713463) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityItalic(let offset, let length): - if boxed { - buffer.appendInt32(-2106619040) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityCode(let offset, let length): - if boxed { - buffer.appendInt32(681706865) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityPre(let offset, let length, let language): - if boxed { - buffer.appendInt32(1938967520) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - serializeString(language, buffer: buffer, boxed: false) - break - case .messageEntityTextUrl(let offset, let length, let url): - if boxed { - buffer.appendInt32(1990644519) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - serializeString(url, buffer: buffer, boxed: false) - break - case .messageEntityUnderline(let offset, let length): - if boxed { - buffer.appendInt32(-1672577397) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityStrike(let offset, let length): - if boxed { - buffer.appendInt32(-1090087980) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityBlockquote(let offset, let length): - if boxed { - buffer.appendInt32(34469328) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_messageEntityUnknown(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.MessageEntity.messageEntityUnknown(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityMention(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.MessageEntity.messageEntityMention(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityHashtag(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.MessageEntity.messageEntityHashtag(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityBotCommand(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.MessageEntity.messageEntityBotCommand(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityUrl(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.MessageEntity.messageEntityUrl(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.MessageEntity.messageEntityEmail(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityBold(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.MessageEntity.messageEntityBold(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityItalic(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.MessageEntity.messageEntityItalic(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityCode(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.MessageEntity.messageEntityCode(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityPre(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi101.MessageEntity.messageEntityPre(offset: _1!, length: _2!, language: _3!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityTextUrl(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi101.MessageEntity.messageEntityTextUrl(offset: _1!, length: _2!, url: _3!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityUnderline(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.MessageEntity.messageEntityUnderline(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityStrike(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.MessageEntity.messageEntityStrike(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityBlockquote(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.MessageEntity.messageEntityBlockquote(offset: _1!, length: _2!) - } - else { - return nil - } - } - } public enum DecryptedMessageMedia { - case decryptedMessageMediaEmpty - case decryptedMessageMediaGeoPoint(lat: Double, long: Double) - case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) case decryptedMessageMediaAudio(duration: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer) - case decryptedMessageMediaExternalDocument(id: Int64, accessHash: Int64, date: Int32, mimeType: String, size: Int32, thumb: SecretApi101.PhotoSize, dcId: Int32, attributes: [SecretApi101.DocumentAttribute]) - case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) case decryptedMessageMediaDocument(thumb: Buffer, thumbW: Int32, thumbH: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer, attributes: [SecretApi101.DocumentAttribute], caption: String) - case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaEmpty + case decryptedMessageMediaExternalDocument(id: Int64, accessHash: Int64, date: Int32, mimeType: String, size: Int32, thumb: SecretApi101.PhotoSize, dcId: Int32, attributes: [SecretApi101.DocumentAttribute]) + case decryptedMessageMediaGeoPoint(lat: Double, long: Double) + case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) case decryptedMessageMediaVenue(lat: Double, long: Double, title: String, address: String, provider: String, venueId: String) + case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) case decryptedMessageMediaWebPage(url: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageMediaEmpty: - if boxed { - buffer.appendInt32(144661578) - } - - break - case .decryptedMessageMediaGeoPoint(let lat, let long): - if boxed { - buffer.appendInt32(893913689) - } - serializeDouble(lat, buffer: buffer, boxed: false) - serializeDouble(long, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): - if boxed { - buffer.appendInt32(1485441687) - } - serializeString(phoneNumber, buffer: buffer, boxed: false) - serializeString(firstName, buffer: buffer, boxed: false) - serializeString(lastName, buffer: buffer, boxed: false) - serializeInt32(userId, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): - if boxed { - buffer.appendInt32(1474341323) - } - serializeInt32(duration, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaExternalDocument(let id, let accessHash, let date, let mimeType, let size, let thumb, let dcId, let attributes): - if boxed { - buffer.appendInt32(-90853155) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - thumb.serialize(buffer, true) - serializeInt32(dcId, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(attributes.count)) - for item in attributes { - item.serialize(buffer, true) - } - break - case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv, let caption): - if boxed { - buffer.appendInt32(-235238024) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - serializeString(caption, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let mimeType, let size, let key, let iv, let attributes, let caption): - if boxed { - buffer.appendInt32(2063502050) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(attributes.count)) - for item in attributes { - item.serialize(buffer, true) - } - serializeString(caption, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv, let caption): - if boxed { - buffer.appendInt32(-1760785394) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeInt32(duration, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - serializeString(caption, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaVenue(let lat, let long, let title, let address, let provider, let venueId): - if boxed { - buffer.appendInt32(-1978796689) - } - serializeDouble(lat, buffer: buffer, boxed: false) - serializeDouble(long, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - serializeString(address, buffer: buffer, boxed: false) - serializeString(provider, buffer: buffer, boxed: false) - serializeString(venueId, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaWebPage(let url): - if boxed { - buffer.appendInt32(-452652584) - } - serializeString(url, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { - return SecretApi101.DecryptedMessageMedia.decryptedMessageMediaEmpty + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(1474341323) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): + if boxed { + buffer.appendInt32(1485441687) + } + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + serializeInt32(userId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let mimeType, let size, let key, let iv, let attributes, let caption): + if boxed { + buffer.appendInt32(2063502050) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaEmpty: + if boxed { + buffer.appendInt32(144661578) + } + break + case .decryptedMessageMediaExternalDocument(let id, let accessHash, let date, let mimeType, let size, let thumb, let dcId, let attributes): + if boxed { + buffer.appendInt32(-90853155) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + thumb.serialize(buffer, true) + serializeInt32(dcId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + break + case .decryptedMessageMediaGeoPoint(let lat, let long): + if boxed { + buffer.appendInt32(893913689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-235238024) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVenue(let lat, let long, let title, let address, let provider, let venueId): + if boxed { + buffer.appendInt32(-1978796689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(address, buffer: buffer, boxed: false) + serializeString(provider, buffer: buffer, boxed: false) + serializeString(venueId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-1760785394) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaWebPage(let url): + if boxed { + buffer.appendInt32(-452652584) + } + serializeString(url, buffer: buffer, boxed: false) + break + } } - fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Double? - _1 = reader.readDouble() - var _2: Double? - _2 = reader.readDouble() + + fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + var _4: Buffer? + _4 = parseBytes(reader) + var _5: Buffer? + _5 = parseBytes(reader) let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi101.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi101.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) } else { return nil @@ -1361,29 +771,46 @@ public struct SecretApi101 { return nil } } - fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) + fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() var _3: Int32? _3 = reader.readInt32() - var _4: Buffer? - _4 = parseBytes(reader) - var _5: Buffer? - _5 = parseBytes(reader) + var _4: String? + _4 = parseString(reader) + var _5: Int32? + _5 = reader.readInt32() + var _6: Buffer? + _6 = parseBytes(reader) + var _7: Buffer? + _7 = parseBytes(reader) + var _8: [SecretApi101.DocumentAttribute]? + if let _ = reader.readInt32() { + _8 = SecretApi101.parseVector(reader, elementSignature: 0, elementType: SecretApi101.DocumentAttribute.self) + } + var _9: String? + _9 = parseString(reader) let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi101.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi101.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, mimeType: _4!, size: _5!, key: _6!, iv: _7!, attributes: _8!, caption: _9!) } else { return nil } } + fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { + return SecretApi101.DecryptedMessageMedia.decryptedMessageMediaEmpty + } fileprivate static func parse_decryptedMessageMediaExternalDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: Int64? _1 = reader.readInt64() @@ -1420,6 +847,20 @@ public struct SecretApi101 { return nil } } + fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + } + else { + return nil + } + } fileprivate static func parse_decryptedMessageMediaPhoto(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: Buffer? _1 = parseBytes(reader) @@ -1455,38 +896,27 @@ public struct SecretApi101 { return nil } } - fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Buffer? - _1 = parseBytes(reader) - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() + fileprivate static func parse_decryptedMessageMediaVenue(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + var _3: String? + _3 = parseString(reader) var _4: String? _4 = parseString(reader) - var _5: Int32? - _5 = reader.readInt32() - var _6: Buffer? - _6 = parseBytes(reader) - var _7: Buffer? - _7 = parseBytes(reader) - var _8: [SecretApi101.DocumentAttribute]? - if let _ = reader.readInt32() { - _8 = SecretApi101.parseVector(reader, elementSignature: 0, elementType: SecretApi101.DocumentAttribute.self) - } - var _9: String? - _9 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: String? + _6 = parseString(reader) let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return SecretApi101.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, mimeType: _4!, size: _5!, key: _6!, iv: _7!, attributes: _8!, caption: _9!) + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return SecretApi101.DecryptedMessageMedia.decryptedMessageMediaVenue(lat: _1!, long: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!) } else { return nil @@ -1533,32 +963,6 @@ public struct SecretApi101 { return nil } } - fileprivate static func parse_decryptedMessageMediaVenue(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Double? - _1 = reader.readDouble() - var _2: Double? - _2 = reader.readDouble() - var _3: String? - _3 = parseString(reader) - var _4: String? - _4 = parseString(reader) - var _5: String? - _5 = parseString(reader) - var _6: String? - _6 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return SecretApi101.DecryptedMessageMedia.decryptedMessageMediaVenue(lat: _1!, long: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!) - } - else { - return nil - } - } fileprivate static func parse_decryptedMessageMediaWebPage(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: String? _1 = parseString(reader) @@ -1570,12 +974,806 @@ public struct SecretApi101 { return nil } } - - } - public struct functions { - + public enum DocumentAttribute { + case documentAttributeAnimated + case documentAttributeAudio(flags: Int32, duration: Int32, title: String?, performer: String?, waveform: Buffer?) + case documentAttributeFilename(fileName: String) + case documentAttributeImageSize(w: Int32, h: Int32) + case documentAttributeSticker(alt: String, stickerset: SecretApi101.InputStickerSet) + case documentAttributeVideo(flags: Int32, duration: Int32, w: Int32, h: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .documentAttributeAnimated: + if boxed { + buffer.appendInt32(297109817) + } + break + case .documentAttributeAudio(let flags, let duration, let title, let performer, let waveform): + if boxed { + buffer.appendInt32(-1739392570) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 { + serializeString(title!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 1) != 0 { + serializeString(performer!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 2) != 0 { + serializeBytes(waveform!, buffer: buffer, boxed: false) + } + break + case .documentAttributeFilename(let fileName): + if boxed { + buffer.appendInt32(358154344) + } + serializeString(fileName, buffer: buffer, boxed: false) + break + case .documentAttributeImageSize(let w, let h): + if boxed { + buffer.appendInt32(1815593308) + } + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + case .documentAttributeSticker(let alt, let stickerset): + if boxed { + buffer.appendInt32(978674434) + } + serializeString(alt, buffer: buffer, boxed: false) + stickerset.serialize(buffer, true) + break + case .documentAttributeVideo(let flags, let duration, let w, let h): + if boxed { + buffer.appendInt32(250621158) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_documentAttributeAnimated(_ reader: BufferReader) -> DocumentAttribute? { + return SecretApi101.DocumentAttribute.documentAttributeAnimated + } + fileprivate static func parse_documentAttributeAudio(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _3 = parseString(reader) + } + var _4: String? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _4 = parseString(reader) + } + var _5: Buffer? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _5 = parseBytes(reader) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi101.DocumentAttribute.documentAttributeAudio(flags: _1!, duration: _2!, title: _3, performer: _4, waveform: _5) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeFilename(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi101.DocumentAttribute.documentAttributeFilename(fileName: _1!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeImageSize(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.DocumentAttribute.documentAttributeImageSize(w: _1!, h: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeSticker(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi101.InputStickerSet? + if let signature = reader.readInt32() { + _2 = SecretApi101.parse(reader, signature: signature) as? SecretApi101.InputStickerSet + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.DocumentAttribute.documentAttributeSticker(alt: _1!, stickerset: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeVideo(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi101.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!) + } + else { + return nil + } + } + } + + public enum FileLocation { + case fileLocation(dcId: Int32, volumeId: Int64, localId: Int32, secret: Int64) + case fileLocationUnavailable(volumeId: Int64, localId: Int32, secret: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .fileLocation(let dcId, let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(1406570614) + } + serializeInt32(dcId, buffer: buffer, boxed: false) + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + case .fileLocationUnavailable(let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(2086234950) + } + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_fileLocation(_ reader: BufferReader) -> FileLocation? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int64? + _4 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi101.FileLocation.fileLocation(dcId: _1!, volumeId: _2!, localId: _3!, secret: _4!) + } + else { + return nil + } + } + fileprivate static func parse_fileLocationUnavailable(_ reader: BufferReader) -> FileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi101.FileLocation.fileLocationUnavailable(volumeId: _1!, localId: _2!, secret: _3!) + } + else { + return nil + } + } + } + + public enum InputStickerSet { + case inputStickerSetEmpty + case inputStickerSetShortName(shortName: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputStickerSetEmpty: + if boxed { + buffer.appendInt32(-4838507) + } + break + case .inputStickerSetShortName(let shortName): + if boxed { + buffer.appendInt32(-2044933984) + } + serializeString(shortName, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_inputStickerSetEmpty(_ reader: BufferReader) -> InputStickerSet? { + return SecretApi101.InputStickerSet.inputStickerSetEmpty + } + fileprivate static func parse_inputStickerSetShortName(_ reader: BufferReader) -> InputStickerSet? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi101.InputStickerSet.inputStickerSetShortName(shortName: _1!) + } + else { + return nil + } + } + } + + public enum MessageEntity { + case messageEntityBlockquote(offset: Int32, length: Int32) + case messageEntityBold(offset: Int32, length: Int32) + case messageEntityBotCommand(offset: Int32, length: Int32) + case messageEntityCode(offset: Int32, length: Int32) + case messageEntityEmail(offset: Int32, length: Int32) + case messageEntityHashtag(offset: Int32, length: Int32) + case messageEntityItalic(offset: Int32, length: Int32) + case messageEntityMention(offset: Int32, length: Int32) + case messageEntityPre(offset: Int32, length: Int32, language: String) + case messageEntityStrike(offset: Int32, length: Int32) + case messageEntityTextUrl(offset: Int32, length: Int32, url: String) + case messageEntityUnderline(offset: Int32, length: Int32) + case messageEntityUnknown(offset: Int32, length: Int32) + case messageEntityUrl(offset: Int32, length: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageEntityBlockquote(let offset, let length): + if boxed { + buffer.appendInt32(34469328) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityBold(let offset, let length): + if boxed { + buffer.appendInt32(-1117713463) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityBotCommand(let offset, let length): + if boxed { + buffer.appendInt32(1827637959) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityCode(let offset, let length): + if boxed { + buffer.appendInt32(681706865) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityEmail(let offset, let length): + if boxed { + buffer.appendInt32(1692693954) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityHashtag(let offset, let length): + if boxed { + buffer.appendInt32(1868782349) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityItalic(let offset, let length): + if boxed { + buffer.appendInt32(-2106619040) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityMention(let offset, let length): + if boxed { + buffer.appendInt32(-100378723) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityPre(let offset, let length, let language): + if boxed { + buffer.appendInt32(1938967520) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(language, buffer: buffer, boxed: false) + break + case .messageEntityStrike(let offset, let length): + if boxed { + buffer.appendInt32(-1090087980) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityTextUrl(let offset, let length, let url): + if boxed { + buffer.appendInt32(1990644519) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + break + case .messageEntityUnderline(let offset, let length): + if boxed { + buffer.appendInt32(-1672577397) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityUnknown(let offset, let length): + if boxed { + buffer.appendInt32(-1148011883) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityUrl(let offset, let length): + if boxed { + buffer.appendInt32(1859134776) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_messageEntityBlockquote(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.MessageEntity.messageEntityBlockquote(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityBold(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.MessageEntity.messageEntityBold(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityBotCommand(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.MessageEntity.messageEntityBotCommand(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityCode(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.MessageEntity.messageEntityCode(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.MessageEntity.messageEntityEmail(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityHashtag(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.MessageEntity.messageEntityHashtag(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityItalic(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.MessageEntity.messageEntityItalic(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityMention(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.MessageEntity.messageEntityMention(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityPre(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi101.MessageEntity.messageEntityPre(offset: _1!, length: _2!, language: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityStrike(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.MessageEntity.messageEntityStrike(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityTextUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi101.MessageEntity.messageEntityTextUrl(offset: _1!, length: _2!, url: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUnderline(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.MessageEntity.messageEntityUnderline(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUnknown(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.MessageEntity.messageEntityUnknown(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi101.MessageEntity.messageEntityUrl(offset: _1!, length: _2!) + } + else { + return nil + } + } + } + + public enum PhotoSize { + case photoCachedSize(type: String, location: SecretApi101.FileLocation, w: Int32, h: Int32, bytes: Buffer) + case photoSize(type: String, location: SecretApi101.FileLocation, w: Int32, h: Int32, size: Int32) + case photoSizeEmpty(type: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .photoCachedSize(let type, let location, let w, let h, let bytes): + if boxed { + buffer.appendInt32(-374917894) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeBytes(bytes, buffer: buffer, boxed: false) + break + case .photoSize(let type, let location, let w, let h, let size): + if boxed { + buffer.appendInt32(2009052699) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + break + case .photoSizeEmpty(let type): + if boxed { + buffer.appendInt32(236446268) + } + serializeString(type, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_photoCachedSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi101.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi101.parse(reader, signature: signature) as? SecretApi101.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi101.PhotoSize.photoCachedSize(type: _1!, location: _2!, w: _3!, h: _4!, bytes: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi101.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi101.parse(reader, signature: signature) as? SecretApi101.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi101.PhotoSize.photoSize(type: _1!, location: _2!, w: _3!, h: _4!, size: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSizeEmpty(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi101.PhotoSize.photoSizeEmpty(type: _1!) + } + else { + return nil + } + } + } + + public enum SendMessageAction { + case sendMessageCancelAction + case sendMessageChooseContactAction + case sendMessageGeoLocationAction + case sendMessageRecordAudioAction + case sendMessageRecordRoundAction + case sendMessageRecordVideoAction + case sendMessageTypingAction + case sendMessageUploadAudioAction + case sendMessageUploadDocumentAction + case sendMessageUploadPhotoAction + case sendMessageUploadRoundAction + case sendMessageUploadVideoAction + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sendMessageCancelAction: + if boxed { + buffer.appendInt32(-44119819) + } + break + case .sendMessageChooseContactAction: + if boxed { + buffer.appendInt32(1653390447) + } + break + case .sendMessageGeoLocationAction: + if boxed { + buffer.appendInt32(393186209) + } + break + case .sendMessageRecordAudioAction: + if boxed { + buffer.appendInt32(-718310409) + } + break + case .sendMessageRecordRoundAction: + if boxed { + buffer.appendInt32(-1997373508) + } + break + case .sendMessageRecordVideoAction: + if boxed { + buffer.appendInt32(-1584933265) + } + break + case .sendMessageTypingAction: + if boxed { + buffer.appendInt32(381645902) + } + break + case .sendMessageUploadAudioAction: + if boxed { + buffer.appendInt32(-424899985) + } + break + case .sendMessageUploadDocumentAction: + if boxed { + buffer.appendInt32(-1884362354) + } + break + case .sendMessageUploadPhotoAction: + if boxed { + buffer.appendInt32(-1727382502) + } + break + case .sendMessageUploadRoundAction: + if boxed { + buffer.appendInt32(-1150187996) + } + break + case .sendMessageUploadVideoAction: + if boxed { + buffer.appendInt32(-1845219337) + } + break + } + } + + fileprivate static func parse_sendMessageCancelAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi101.SendMessageAction.sendMessageCancelAction + } + fileprivate static func parse_sendMessageChooseContactAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi101.SendMessageAction.sendMessageChooseContactAction + } + fileprivate static func parse_sendMessageGeoLocationAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi101.SendMessageAction.sendMessageGeoLocationAction + } + fileprivate static func parse_sendMessageRecordAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi101.SendMessageAction.sendMessageRecordAudioAction + } + fileprivate static func parse_sendMessageRecordRoundAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi101.SendMessageAction.sendMessageRecordRoundAction + } + fileprivate static func parse_sendMessageRecordVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi101.SendMessageAction.sendMessageRecordVideoAction + } + fileprivate static func parse_sendMessageTypingAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi101.SendMessageAction.sendMessageTypingAction + } + fileprivate static func parse_sendMessageUploadAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi101.SendMessageAction.sendMessageUploadAudioAction + } + fileprivate static func parse_sendMessageUploadDocumentAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi101.SendMessageAction.sendMessageUploadDocumentAction + } + fileprivate static func parse_sendMessageUploadPhotoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi101.SendMessageAction.sendMessageUploadPhotoAction + } + fileprivate static func parse_sendMessageUploadRoundAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi101.SendMessageAction.sendMessageUploadRoundAction + } + fileprivate static func parse_sendMessageUploadVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi101.SendMessageAction.sendMessageUploadVideoAction + } } } diff --git a/submodules/TelegramApi/Sources/SecretApiLayer143.swift b/submodules/TelegramApi/Sources/SecretApiLayer143.swift new file mode 100644 index 0000000000..2cc35b0c6e --- /dev/null +++ b/submodules/TelegramApi/Sources/SecretApiLayer143.swift @@ -0,0 +1,1779 @@ + +fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { + var dict: [Int32 : (BufferReader) -> Any?] = [:] + dict[-1471112230] = { return $0.readInt32() } + dict[570911930] = { return $0.readInt64() } + dict[571523412] = { return $0.readDouble() } + dict[-1255641564] = { return parseString($0) } + dict[-1132882121] = { return SecretApi143.Bool.parse_boolFalse($0) } + dict[-1720552011] = { return SecretApi143.Bool.parse_boolTrue($0) } + dict[-1848883596] = { return SecretApi143.DecryptedMessage.parse_decryptedMessage($0) } + dict[1930838368] = { return SecretApi143.DecryptedMessage.parse_decryptedMessageService($0) } + dict[-586814357] = { return SecretApi143.DecryptedMessageAction.parse_decryptedMessageActionAbortKey($0) } + dict[1877046107] = { return SecretApi143.DecryptedMessageAction.parse_decryptedMessageActionAcceptKey($0) } + dict[-332526693] = { return SecretApi143.DecryptedMessageAction.parse_decryptedMessageActionCommitKey($0) } + dict[1700872964] = { return SecretApi143.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } + dict[1729750108] = { return SecretApi143.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } + dict[-1473258141] = { return SecretApi143.DecryptedMessageAction.parse_decryptedMessageActionNoop($0) } + dict[-217806717] = { return SecretApi143.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } + dict[206520510] = { return SecretApi143.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } + dict[-204906213] = { return SecretApi143.DecryptedMessageAction.parse_decryptedMessageActionRequestKey($0) } + dict[1360072880] = { return SecretApi143.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } + dict[-1967000459] = { return SecretApi143.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } + dict[-1586283796] = { return SecretApi143.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } + dict[-860719551] = { return SecretApi143.DecryptedMessageAction.parse_decryptedMessageActionTyping($0) } + dict[467867529] = { return SecretApi143.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } + dict[1474341323] = { return SecretApi143.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } + dict[1485441687] = { return SecretApi143.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } + dict[1790809986] = { return SecretApi143.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } + dict[144661578] = { return SecretApi143.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } + dict[-90853155] = { return SecretApi143.DecryptedMessageMedia.parse_decryptedMessageMediaExternalDocument($0) } + dict[893913689] = { return SecretApi143.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } + dict[-235238024] = { return SecretApi143.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } + dict[-1978796689] = { return SecretApi143.DecryptedMessageMedia.parse_decryptedMessageMediaVenue($0) } + dict[-1760785394] = { return SecretApi143.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } + dict[-452652584] = { return SecretApi143.DecryptedMessageMedia.parse_decryptedMessageMediaWebPage($0) } + dict[297109817] = { return SecretApi143.DocumentAttribute.parse_documentAttributeAnimated($0) } + dict[-1739392570] = { return SecretApi143.DocumentAttribute.parse_documentAttributeAudio($0) } + dict[358154344] = { return SecretApi143.DocumentAttribute.parse_documentAttributeFilename($0) } + dict[1815593308] = { return SecretApi143.DocumentAttribute.parse_documentAttributeImageSize($0) } + dict[978674434] = { return SecretApi143.DocumentAttribute.parse_documentAttributeSticker($0) } + dict[250621158] = { return SecretApi143.DocumentAttribute.parse_documentAttributeVideo($0) } + dict[1406570614] = { return SecretApi143.FileLocation.parse_fileLocation($0) } + dict[2086234950] = { return SecretApi143.FileLocation.parse_fileLocationUnavailable($0) } + dict[-4838507] = { return SecretApi143.InputStickerSet.parse_inputStickerSetEmpty($0) } + dict[-2044933984] = { return SecretApi143.InputStickerSet.parse_inputStickerSetShortName($0) } + dict[34469328] = { return SecretApi143.MessageEntity.parse_messageEntityBlockquote($0) } + dict[-1117713463] = { return SecretApi143.MessageEntity.parse_messageEntityBold($0) } + dict[1827637959] = { return SecretApi143.MessageEntity.parse_messageEntityBotCommand($0) } + dict[681706865] = { return SecretApi143.MessageEntity.parse_messageEntityCode($0) } + dict[1692693954] = { return SecretApi143.MessageEntity.parse_messageEntityEmail($0) } + dict[1868782349] = { return SecretApi143.MessageEntity.parse_messageEntityHashtag($0) } + dict[-2106619040] = { return SecretApi143.MessageEntity.parse_messageEntityItalic($0) } + dict[-100378723] = { return SecretApi143.MessageEntity.parse_messageEntityMention($0) } + dict[1938967520] = { return SecretApi143.MessageEntity.parse_messageEntityPre($0) } + dict[-1090087980] = { return SecretApi143.MessageEntity.parse_messageEntityStrike($0) } + dict[1990644519] = { return SecretApi143.MessageEntity.parse_messageEntityTextUrl($0) } + dict[-1672577397] = { return SecretApi143.MessageEntity.parse_messageEntityUnderline($0) } + dict[-1148011883] = { return SecretApi143.MessageEntity.parse_messageEntityUnknown($0) } + dict[1859134776] = { return SecretApi143.MessageEntity.parse_messageEntityUrl($0) } + dict[-374917894] = { return SecretApi143.PhotoSize.parse_photoCachedSize($0) } + dict[2009052699] = { return SecretApi143.PhotoSize.parse_photoSize($0) } + dict[236446268] = { return SecretApi143.PhotoSize.parse_photoSizeEmpty($0) } + dict[-44119819] = { return SecretApi143.SendMessageAction.parse_sendMessageCancelAction($0) } + dict[1653390447] = { return SecretApi143.SendMessageAction.parse_sendMessageChooseContactAction($0) } + dict[393186209] = { return SecretApi143.SendMessageAction.parse_sendMessageGeoLocationAction($0) } + dict[-718310409] = { return SecretApi143.SendMessageAction.parse_sendMessageRecordAudioAction($0) } + dict[-1997373508] = { return SecretApi143.SendMessageAction.parse_sendMessageRecordRoundAction($0) } + dict[-1584933265] = { return SecretApi143.SendMessageAction.parse_sendMessageRecordVideoAction($0) } + dict[381645902] = { return SecretApi143.SendMessageAction.parse_sendMessageTypingAction($0) } + dict[-424899985] = { return SecretApi143.SendMessageAction.parse_sendMessageUploadAudioAction($0) } + dict[-1884362354] = { return SecretApi143.SendMessageAction.parse_sendMessageUploadDocumentAction($0) } + dict[-1727382502] = { return SecretApi143.SendMessageAction.parse_sendMessageUploadPhotoAction($0) } + dict[-1150187996] = { return SecretApi143.SendMessageAction.parse_sendMessageUploadRoundAction($0) } + dict[-1845219337] = { return SecretApi143.SendMessageAction.parse_sendMessageUploadVideoAction($0) } + return dict +}() + +public struct SecretApi143 { + public static func parse(_ buffer: Buffer) -> Any? { + let reader = BufferReader(buffer) + if let signature = reader.readInt32() { + return parse(reader, signature: signature) + } + return nil + } + + fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { + if let parser = parsers[signature] { + return parser(reader) + } + else { + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + return nil + } + } + + fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { + if let count = reader.readInt32() { + var array = [T]() + var i: Int32 = 0 + while i < count { + var signature = elementSignature + if elementSignature == 0 { + if let unboxedSignature = reader.readInt32() { + signature = unboxedSignature + } + else { + return nil + } + } + if let item = SecretApi143.parse(reader, signature: signature) as? T { + array.append(item) + } + else { + return nil + } + i += 1 + } + return array + } + return nil + } + + public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) { + switch object { + case let _1 as SecretApi143.Bool: + _1.serialize(buffer, boxed) + case let _1 as SecretApi143.DecryptedMessage: + _1.serialize(buffer, boxed) + case let _1 as SecretApi143.DecryptedMessageAction: + _1.serialize(buffer, boxed) + case let _1 as SecretApi143.DecryptedMessageLayer: + _1.serialize(buffer, boxed) + case let _1 as SecretApi143.DecryptedMessageMedia: + _1.serialize(buffer, boxed) + case let _1 as SecretApi143.DocumentAttribute: + _1.serialize(buffer, boxed) + case let _1 as SecretApi143.FileLocation: + _1.serialize(buffer, boxed) + case let _1 as SecretApi143.InputStickerSet: + _1.serialize(buffer, boxed) + case let _1 as SecretApi143.MessageEntity: + _1.serialize(buffer, boxed) + case let _1 as SecretApi143.PhotoSize: + _1.serialize(buffer, boxed) + case let _1 as SecretApi143.SendMessageAction: + _1.serialize(buffer, boxed) + default: + break + } + } + + public enum Bool { + case boolFalse + case boolTrue + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boolFalse: + if boxed { + buffer.appendInt32(-1132882121) + } + break + case .boolTrue: + if boxed { + buffer.appendInt32(-1720552011) + } + break + } + } + + fileprivate static func parse_boolFalse(_ reader: BufferReader) -> Bool? { + return SecretApi143.Bool.boolFalse + } + fileprivate static func parse_boolTrue(_ reader: BufferReader) -> Bool? { + return SecretApi143.Bool.boolTrue + } + } + + public enum DecryptedMessage { + case decryptedMessage(flags: Int32, randomId: Int64, ttl: Int32, message: String, media: SecretApi143.DecryptedMessageMedia?, entities: [SecretApi143.MessageEntity]?, viaBotName: String?, replyToRandomId: Int64?, groupedId: Int64?) + case decryptedMessageService(randomId: Int64, action: SecretApi143.DecryptedMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessage(let flags, let randomId, let ttl, let message, let media, let entities, let viaBotName, let replyToRandomId, let groupedId): + if boxed { + buffer.appendInt32(-1848883596) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(randomId, buffer: buffer, boxed: false) + serializeInt32(ttl, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 9) != 0 { + media!.serialize(buffer, true) + } + if Int(flags) & Int(1 << 7) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + } + } + if Int(flags) & Int(1 << 11) != 0 { + serializeString(viaBotName!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 3) != 0 { + serializeInt64(replyToRandomId!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 17) != 0 { + serializeInt64(groupedId!, buffer: buffer, boxed: false) + } + break + case .decryptedMessageService(let randomId, let action): + if boxed { + buffer.appendInt32(1930838368) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: SecretApi143.DecryptedMessageMedia? + if Int(_1 ?? 0) & Int(1 << 9) != 0 { + if let signature = reader.readInt32() { + _5 = SecretApi143.parse(reader, signature: signature) as? SecretApi143.DecryptedMessageMedia + } + } + var _6: [SecretApi143.MessageEntity]? + if Int(_1 ?? 0) & Int(1 << 7) != 0 { + if let _ = reader.readInt32() { + _6 = SecretApi143.parseVector(reader, elementSignature: 0, elementType: SecretApi143.MessageEntity.self) + } + } + var _7: String? + if Int(_1 ?? 0) & Int(1 << 11) != 0 { + _7 = parseString(reader) + } + var _8: Int64? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _8 = reader.readInt64() + } + var _9: Int64? + if Int(_1 ?? 0) & Int(1 << 17) != 0 { + _9 = reader.readInt64() + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi143.DecryptedMessage.decryptedMessage(flags: _1!, randomId: _2!, ttl: _3!, message: _4!, media: _5, entities: _6, viaBotName: _7, replyToRandomId: _8, groupedId: _9) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: SecretApi143.DecryptedMessageAction? + if let signature = reader.readInt32() { + _2 = SecretApi143.parse(reader, signature: signature) as? SecretApi143.DecryptedMessageAction + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageAction { + case decryptedMessageActionAbortKey(exchangeId: Int64) + case decryptedMessageActionAcceptKey(exchangeId: Int64, gB: Buffer, keyFingerprint: Int64) + case decryptedMessageActionCommitKey(exchangeId: Int64, keyFingerprint: Int64) + case decryptedMessageActionDeleteMessages(randomIds: [Int64]) + case decryptedMessageActionFlushHistory + case decryptedMessageActionNoop + case decryptedMessageActionNotifyLayer(layer: Int32) + case decryptedMessageActionReadMessages(randomIds: [Int64]) + case decryptedMessageActionRequestKey(exchangeId: Int64, gA: Buffer) + case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) + case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) + case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) + case decryptedMessageActionTyping(action: SecretApi143.SendMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageActionAbortKey(let exchangeId): + if boxed { + buffer.appendInt32(-586814357) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + break + case .decryptedMessageActionAcceptKey(let exchangeId, let gB, let keyFingerprint): + if boxed { + buffer.appendInt32(1877046107) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gB, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionCommitKey(let exchangeId, let keyFingerprint): + if boxed { + buffer.appendInt32(-332526693) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionDeleteMessages(let randomIds): + if boxed { + buffer.appendInt32(1700872964) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionFlushHistory: + if boxed { + buffer.appendInt32(1729750108) + } + break + case .decryptedMessageActionNoop: + if boxed { + buffer.appendInt32(-1473258141) + } + break + case .decryptedMessageActionNotifyLayer(let layer): + if boxed { + buffer.appendInt32(-217806717) + } + serializeInt32(layer, buffer: buffer, boxed: false) + break + case .decryptedMessageActionReadMessages(let randomIds): + if boxed { + buffer.appendInt32(206520510) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionRequestKey(let exchangeId, let gA): + if boxed { + buffer.appendInt32(-204906213) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gA, buffer: buffer, boxed: false) + break + case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): + if boxed { + buffer.appendInt32(1360072880) + } + serializeInt32(startSeqNo, buffer: buffer, boxed: false) + serializeInt32(endSeqNo, buffer: buffer, boxed: false) + break + case .decryptedMessageActionScreenshotMessages(let randomIds): + if boxed { + buffer.appendInt32(-1967000459) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionSetMessageTTL(let ttlSeconds): + if boxed { + buffer.appendInt32(-1586283796) + } + serializeInt32(ttlSeconds, buffer: buffer, boxed: false) + break + case .decryptedMessageActionTyping(let action): + if boxed { + buffer.appendInt32(-860719551) + } + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessageActionAbortKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return SecretApi143.DecryptedMessageAction.decryptedMessageActionAbortKey(exchangeId: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionAcceptKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi143.DecryptedMessageAction.decryptedMessageActionAcceptKey(exchangeId: _1!, gB: _2!, keyFingerprint: _3!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionCommitKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.DecryptedMessageAction.decryptedMessageActionCommitKey(exchangeId: _1!, keyFingerprint: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi143.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi143.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi143.DecryptedMessageAction.decryptedMessageActionFlushHistory + } + fileprivate static func parse_decryptedMessageActionNoop(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi143.DecryptedMessageAction.decryptedMessageActionNoop + } + fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi143.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi143.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi143.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionRequestKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.DecryptedMessageAction.decryptedMessageActionRequestKey(exchangeId: _1!, gA: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi143.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi143.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi143.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionTyping(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: SecretApi143.SendMessageAction? + if let signature = reader.readInt32() { + _1 = SecretApi143.parse(reader, signature: signature) as? SecretApi143.SendMessageAction + } + let _c1 = _1 != nil + if _c1 { + return SecretApi143.DecryptedMessageAction.decryptedMessageActionTyping(action: _1!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageLayer { + case decryptedMessageLayer(randomBytes: Buffer, layer: Int32, inSeqNo: Int32, outSeqNo: Int32, message: SecretApi143.DecryptedMessage) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): + if boxed { + buffer.appendInt32(467867529) + } + serializeBytes(randomBytes, buffer: buffer, boxed: false) + serializeInt32(layer, buffer: buffer, boxed: false) + serializeInt32(inSeqNo, buffer: buffer, boxed: false) + serializeInt32(outSeqNo, buffer: buffer, boxed: false) + message.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessageLayer(_ reader: BufferReader) -> DecryptedMessageLayer? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: SecretApi143.DecryptedMessage? + if let signature = reader.readInt32() { + _5 = SecretApi143.parse(reader, signature: signature) as? SecretApi143.DecryptedMessage + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi143.DecryptedMessageLayer.decryptedMessageLayer(randomBytes: _1!, layer: _2!, inSeqNo: _3!, outSeqNo: _4!, message: _5!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageMedia { + case decryptedMessageMediaAudio(duration: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) + case decryptedMessageMediaDocument(thumb: Buffer, thumbW: Int32, thumbH: Int32, mimeType: String, size: Int64, key: Buffer, iv: Buffer, attributes: [SecretApi143.DocumentAttribute], caption: String) + case decryptedMessageMediaEmpty + case decryptedMessageMediaExternalDocument(id: Int64, accessHash: Int64, date: Int32, mimeType: String, size: Int32, thumb: SecretApi143.PhotoSize, dcId: Int32, attributes: [SecretApi143.DocumentAttribute]) + case decryptedMessageMediaGeoPoint(lat: Double, long: Double) + case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaVenue(lat: Double, long: Double, title: String, address: String, provider: String, venueId: String) + case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaWebPage(url: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(1474341323) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): + if boxed { + buffer.appendInt32(1485441687) + } + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + serializeInt32(userId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let mimeType, let size, let key, let iv, let attributes, let caption): + if boxed { + buffer.appendInt32(1790809986) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt64(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaEmpty: + if boxed { + buffer.appendInt32(144661578) + } + break + case .decryptedMessageMediaExternalDocument(let id, let accessHash, let date, let mimeType, let size, let thumb, let dcId, let attributes): + if boxed { + buffer.appendInt32(-90853155) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + thumb.serialize(buffer, true) + serializeInt32(dcId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + break + case .decryptedMessageMediaGeoPoint(let lat, let long): + if boxed { + buffer.appendInt32(893913689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-235238024) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVenue(let lat, let long, let title, let address, let provider, let venueId): + if boxed { + buffer.appendInt32(-1978796689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(address, buffer: buffer, boxed: false) + serializeString(provider, buffer: buffer, boxed: false) + serializeString(venueId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-1760785394) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaWebPage(let url): + if boxed { + buffer.appendInt32(-452652584) + } + serializeString(url, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + var _4: Buffer? + _4 = parseBytes(reader) + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi143.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaContact(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi143.DecryptedMessageMedia.decryptedMessageMediaContact(phoneNumber: _1!, firstName: _2!, lastName: _3!, userId: _4!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: Int64? + _5 = reader.readInt64() + var _6: Buffer? + _6 = parseBytes(reader) + var _7: Buffer? + _7 = parseBytes(reader) + var _8: [SecretApi143.DocumentAttribute]? + if let _ = reader.readInt32() { + _8 = SecretApi143.parseVector(reader, elementSignature: 0, elementType: SecretApi143.DocumentAttribute.self) + } + var _9: String? + _9 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi143.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, mimeType: _4!, size: _5!, key: _6!, iv: _7!, attributes: _8!, caption: _9!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { + return SecretApi143.DecryptedMessageMedia.decryptedMessageMediaEmpty + } + fileprivate static func parse_decryptedMessageMediaExternalDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: Int32? + _5 = reader.readInt32() + var _6: SecretApi143.PhotoSize? + if let signature = reader.readInt32() { + _6 = SecretApi143.parse(reader, signature: signature) as? SecretApi143.PhotoSize + } + var _7: Int32? + _7 = reader.readInt32() + var _8: [SecretApi143.DocumentAttribute]? + if let _ = reader.readInt32() { + _8 = SecretApi143.parseVector(reader, elementSignature: 0, elementType: SecretApi143.DocumentAttribute.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi143.DecryptedMessageMedia.decryptedMessageMediaExternalDocument(id: _1!, accessHash: _2!, date: _3!, mimeType: _4!, size: _5!, thumb: _6!, dcId: _7!, attributes: _8!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaPhoto(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: Buffer? + _7 = parseBytes(reader) + var _8: Buffer? + _8 = parseBytes(reader) + var _9: String? + _9 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi143.DecryptedMessageMedia.decryptedMessageMediaPhoto(thumb: _1!, thumbW: _2!, thumbH: _3!, w: _4!, h: _5!, size: _6!, key: _7!, iv: _8!, caption: _9!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaVenue(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + var _3: String? + _3 = parseString(reader) + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: String? + _6 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return SecretApi143.DecryptedMessageMedia.decryptedMessageMediaVenue(lat: _1!, long: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaVideo(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: String? + _5 = parseString(reader) + var _6: Int32? + _6 = reader.readInt32() + var _7: Int32? + _7 = reader.readInt32() + var _8: Int32? + _8 = reader.readInt32() + var _9: Buffer? + _9 = parseBytes(reader) + var _10: Buffer? + _10 = parseBytes(reader) + var _11: String? + _11 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + let _c10 = _10 != nil + let _c11 = _11 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { + return SecretApi143.DecryptedMessageMedia.decryptedMessageMediaVideo(thumb: _1!, thumbW: _2!, thumbH: _3!, duration: _4!, mimeType: _5!, w: _6!, h: _7!, size: _8!, key: _9!, iv: _10!, caption: _11!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaWebPage(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi143.DecryptedMessageMedia.decryptedMessageMediaWebPage(url: _1!) + } + else { + return nil + } + } + } + + public enum DocumentAttribute { + case documentAttributeAnimated + case documentAttributeAudio(flags: Int32, duration: Int32, title: String?, performer: String?, waveform: Buffer?) + case documentAttributeFilename(fileName: String) + case documentAttributeImageSize(w: Int32, h: Int32) + case documentAttributeSticker(alt: String, stickerset: SecretApi143.InputStickerSet) + case documentAttributeVideo(flags: Int32, duration: Int32, w: Int32, h: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .documentAttributeAnimated: + if boxed { + buffer.appendInt32(297109817) + } + break + case .documentAttributeAudio(let flags, let duration, let title, let performer, let waveform): + if boxed { + buffer.appendInt32(-1739392570) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 { + serializeString(title!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 1) != 0 { + serializeString(performer!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 2) != 0 { + serializeBytes(waveform!, buffer: buffer, boxed: false) + } + break + case .documentAttributeFilename(let fileName): + if boxed { + buffer.appendInt32(358154344) + } + serializeString(fileName, buffer: buffer, boxed: false) + break + case .documentAttributeImageSize(let w, let h): + if boxed { + buffer.appendInt32(1815593308) + } + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + case .documentAttributeSticker(let alt, let stickerset): + if boxed { + buffer.appendInt32(978674434) + } + serializeString(alt, buffer: buffer, boxed: false) + stickerset.serialize(buffer, true) + break + case .documentAttributeVideo(let flags, let duration, let w, let h): + if boxed { + buffer.appendInt32(250621158) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_documentAttributeAnimated(_ reader: BufferReader) -> DocumentAttribute? { + return SecretApi143.DocumentAttribute.documentAttributeAnimated + } + fileprivate static func parse_documentAttributeAudio(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _3 = parseString(reader) + } + var _4: String? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _4 = parseString(reader) + } + var _5: Buffer? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _5 = parseBytes(reader) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi143.DocumentAttribute.documentAttributeAudio(flags: _1!, duration: _2!, title: _3, performer: _4, waveform: _5) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeFilename(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi143.DocumentAttribute.documentAttributeFilename(fileName: _1!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeImageSize(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.DocumentAttribute.documentAttributeImageSize(w: _1!, h: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeSticker(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi143.InputStickerSet? + if let signature = reader.readInt32() { + _2 = SecretApi143.parse(reader, signature: signature) as? SecretApi143.InputStickerSet + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.DocumentAttribute.documentAttributeSticker(alt: _1!, stickerset: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeVideo(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi143.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!) + } + else { + return nil + } + } + } + + public enum FileLocation { + case fileLocation(dcId: Int32, volumeId: Int64, localId: Int32, secret: Int64) + case fileLocationUnavailable(volumeId: Int64, localId: Int32, secret: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .fileLocation(let dcId, let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(1406570614) + } + serializeInt32(dcId, buffer: buffer, boxed: false) + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + case .fileLocationUnavailable(let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(2086234950) + } + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_fileLocation(_ reader: BufferReader) -> FileLocation? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int64? + _4 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi143.FileLocation.fileLocation(dcId: _1!, volumeId: _2!, localId: _3!, secret: _4!) + } + else { + return nil + } + } + fileprivate static func parse_fileLocationUnavailable(_ reader: BufferReader) -> FileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi143.FileLocation.fileLocationUnavailable(volumeId: _1!, localId: _2!, secret: _3!) + } + else { + return nil + } + } + } + + public enum InputStickerSet { + case inputStickerSetEmpty + case inputStickerSetShortName(shortName: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputStickerSetEmpty: + if boxed { + buffer.appendInt32(-4838507) + } + break + case .inputStickerSetShortName(let shortName): + if boxed { + buffer.appendInt32(-2044933984) + } + serializeString(shortName, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_inputStickerSetEmpty(_ reader: BufferReader) -> InputStickerSet? { + return SecretApi143.InputStickerSet.inputStickerSetEmpty + } + fileprivate static func parse_inputStickerSetShortName(_ reader: BufferReader) -> InputStickerSet? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi143.InputStickerSet.inputStickerSetShortName(shortName: _1!) + } + else { + return nil + } + } + } + + public enum MessageEntity { + case messageEntityBlockquote(offset: Int32, length: Int32) + case messageEntityBold(offset: Int32, length: Int32) + case messageEntityBotCommand(offset: Int32, length: Int32) + case messageEntityCode(offset: Int32, length: Int32) + case messageEntityEmail(offset: Int32, length: Int32) + case messageEntityHashtag(offset: Int32, length: Int32) + case messageEntityItalic(offset: Int32, length: Int32) + case messageEntityMention(offset: Int32, length: Int32) + case messageEntityPre(offset: Int32, length: Int32, language: String) + case messageEntityStrike(offset: Int32, length: Int32) + case messageEntityTextUrl(offset: Int32, length: Int32, url: String) + case messageEntityUnderline(offset: Int32, length: Int32) + case messageEntityUnknown(offset: Int32, length: Int32) + case messageEntityUrl(offset: Int32, length: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageEntityBlockquote(let offset, let length): + if boxed { + buffer.appendInt32(34469328) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityBold(let offset, let length): + if boxed { + buffer.appendInt32(-1117713463) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityBotCommand(let offset, let length): + if boxed { + buffer.appendInt32(1827637959) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityCode(let offset, let length): + if boxed { + buffer.appendInt32(681706865) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityEmail(let offset, let length): + if boxed { + buffer.appendInt32(1692693954) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityHashtag(let offset, let length): + if boxed { + buffer.appendInt32(1868782349) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityItalic(let offset, let length): + if boxed { + buffer.appendInt32(-2106619040) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityMention(let offset, let length): + if boxed { + buffer.appendInt32(-100378723) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityPre(let offset, let length, let language): + if boxed { + buffer.appendInt32(1938967520) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(language, buffer: buffer, boxed: false) + break + case .messageEntityStrike(let offset, let length): + if boxed { + buffer.appendInt32(-1090087980) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityTextUrl(let offset, let length, let url): + if boxed { + buffer.appendInt32(1990644519) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + break + case .messageEntityUnderline(let offset, let length): + if boxed { + buffer.appendInt32(-1672577397) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityUnknown(let offset, let length): + if boxed { + buffer.appendInt32(-1148011883) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityUrl(let offset, let length): + if boxed { + buffer.appendInt32(1859134776) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_messageEntityBlockquote(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.MessageEntity.messageEntityBlockquote(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityBold(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.MessageEntity.messageEntityBold(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityBotCommand(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.MessageEntity.messageEntityBotCommand(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityCode(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.MessageEntity.messageEntityCode(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.MessageEntity.messageEntityEmail(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityHashtag(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.MessageEntity.messageEntityHashtag(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityItalic(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.MessageEntity.messageEntityItalic(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityMention(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.MessageEntity.messageEntityMention(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityPre(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi143.MessageEntity.messageEntityPre(offset: _1!, length: _2!, language: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityStrike(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.MessageEntity.messageEntityStrike(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityTextUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi143.MessageEntity.messageEntityTextUrl(offset: _1!, length: _2!, url: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUnderline(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.MessageEntity.messageEntityUnderline(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUnknown(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.MessageEntity.messageEntityUnknown(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi143.MessageEntity.messageEntityUrl(offset: _1!, length: _2!) + } + else { + return nil + } + } + } + + public enum PhotoSize { + case photoCachedSize(type: String, location: SecretApi143.FileLocation, w: Int32, h: Int32, bytes: Buffer) + case photoSize(type: String, location: SecretApi143.FileLocation, w: Int32, h: Int32, size: Int32) + case photoSizeEmpty(type: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .photoCachedSize(let type, let location, let w, let h, let bytes): + if boxed { + buffer.appendInt32(-374917894) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeBytes(bytes, buffer: buffer, boxed: false) + break + case .photoSize(let type, let location, let w, let h, let size): + if boxed { + buffer.appendInt32(2009052699) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + break + case .photoSizeEmpty(let type): + if boxed { + buffer.appendInt32(236446268) + } + serializeString(type, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_photoCachedSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi143.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi143.parse(reader, signature: signature) as? SecretApi143.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi143.PhotoSize.photoCachedSize(type: _1!, location: _2!, w: _3!, h: _4!, bytes: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi143.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi143.parse(reader, signature: signature) as? SecretApi143.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi143.PhotoSize.photoSize(type: _1!, location: _2!, w: _3!, h: _4!, size: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSizeEmpty(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi143.PhotoSize.photoSizeEmpty(type: _1!) + } + else { + return nil + } + } + } + + public enum SendMessageAction { + case sendMessageCancelAction + case sendMessageChooseContactAction + case sendMessageGeoLocationAction + case sendMessageRecordAudioAction + case sendMessageRecordRoundAction + case sendMessageRecordVideoAction + case sendMessageTypingAction + case sendMessageUploadAudioAction + case sendMessageUploadDocumentAction + case sendMessageUploadPhotoAction + case sendMessageUploadRoundAction + case sendMessageUploadVideoAction + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sendMessageCancelAction: + if boxed { + buffer.appendInt32(-44119819) + } + break + case .sendMessageChooseContactAction: + if boxed { + buffer.appendInt32(1653390447) + } + break + case .sendMessageGeoLocationAction: + if boxed { + buffer.appendInt32(393186209) + } + break + case .sendMessageRecordAudioAction: + if boxed { + buffer.appendInt32(-718310409) + } + break + case .sendMessageRecordRoundAction: + if boxed { + buffer.appendInt32(-1997373508) + } + break + case .sendMessageRecordVideoAction: + if boxed { + buffer.appendInt32(-1584933265) + } + break + case .sendMessageTypingAction: + if boxed { + buffer.appendInt32(381645902) + } + break + case .sendMessageUploadAudioAction: + if boxed { + buffer.appendInt32(-424899985) + } + break + case .sendMessageUploadDocumentAction: + if boxed { + buffer.appendInt32(-1884362354) + } + break + case .sendMessageUploadPhotoAction: + if boxed { + buffer.appendInt32(-1727382502) + } + break + case .sendMessageUploadRoundAction: + if boxed { + buffer.appendInt32(-1150187996) + } + break + case .sendMessageUploadVideoAction: + if boxed { + buffer.appendInt32(-1845219337) + } + break + } + } + + fileprivate static func parse_sendMessageCancelAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi143.SendMessageAction.sendMessageCancelAction + } + fileprivate static func parse_sendMessageChooseContactAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi143.SendMessageAction.sendMessageChooseContactAction + } + fileprivate static func parse_sendMessageGeoLocationAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi143.SendMessageAction.sendMessageGeoLocationAction + } + fileprivate static func parse_sendMessageRecordAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi143.SendMessageAction.sendMessageRecordAudioAction + } + fileprivate static func parse_sendMessageRecordRoundAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi143.SendMessageAction.sendMessageRecordRoundAction + } + fileprivate static func parse_sendMessageRecordVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi143.SendMessageAction.sendMessageRecordVideoAction + } + fileprivate static func parse_sendMessageTypingAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi143.SendMessageAction.sendMessageTypingAction + } + fileprivate static func parse_sendMessageUploadAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi143.SendMessageAction.sendMessageUploadAudioAction + } + fileprivate static func parse_sendMessageUploadDocumentAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi143.SendMessageAction.sendMessageUploadDocumentAction + } + fileprivate static func parse_sendMessageUploadPhotoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi143.SendMessageAction.sendMessageUploadPhotoAction + } + fileprivate static func parse_sendMessageUploadRoundAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi143.SendMessageAction.sendMessageUploadRoundAction + } + fileprivate static func parse_sendMessageUploadVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi143.SendMessageAction.sendMessageUploadVideoAction + } + } + +} diff --git a/submodules/TelegramApi/Sources/SecretApiLayer144.swift b/submodules/TelegramApi/Sources/SecretApiLayer144.swift index 82f0b0bfe3..99b74b877c 100644 --- a/submodules/TelegramApi/Sources/SecretApiLayer144.swift +++ b/submodules/TelegramApi/Sources/SecretApiLayer144.swift @@ -5,60 +5,75 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[570911930] = { return $0.readInt64() } dict[571523412] = { return $0.readDouble() } dict[-1255641564] = { return parseString($0) } - dict[-1586283796] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } - dict[206520510] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } - dict[1700872964] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } - dict[-1967000459] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } - dict[1729750108] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } - dict[-217806717] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } - dict[1360072880] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } - dict[-204906213] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionRequestKey($0) } - dict[1877046107] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionAcceptKey($0) } - dict[-586814357] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionAbortKey($0) } - dict[-332526693] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionCommitKey($0) } - dict[-1473258141] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionNoop($0) } - dict[236446268] = { return SecretApi144.PhotoSize.parse_photoSizeEmpty($0) } - dict[2009052699] = { return SecretApi144.PhotoSize.parse_photoSize($0) } - dict[-374917894] = { return SecretApi144.PhotoSize.parse_photoCachedSize($0) } - dict[2086234950] = { return SecretApi144.FileLocation.parse_fileLocationUnavailable($0) } - dict[1406570614] = { return SecretApi144.FileLocation.parse_fileLocation($0) } - dict[467867529] = { return SecretApi144.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } - dict[1930838368] = { return SecretApi144.DecryptedMessage.parse_decryptedMessageService($0) } + dict[-1132882121] = { return SecretApi144.Bool.parse_boolFalse($0) } + dict[-1720552011] = { return SecretApi144.Bool.parse_boolTrue($0) } dict[-1848883596] = { return SecretApi144.DecryptedMessage.parse_decryptedMessage($0) } - dict[1815593308] = { return SecretApi144.DocumentAttribute.parse_documentAttributeImageSize($0) } + dict[1930838368] = { return SecretApi144.DecryptedMessage.parse_decryptedMessageService($0) } + dict[-586814357] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionAbortKey($0) } + dict[1877046107] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionAcceptKey($0) } + dict[-332526693] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionCommitKey($0) } + dict[1700872964] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } + dict[1729750108] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } + dict[-1473258141] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionNoop($0) } + dict[-217806717] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } + dict[206520510] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } + dict[-204906213] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionRequestKey($0) } + dict[1360072880] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } + dict[-1967000459] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } + dict[-1586283796] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } + dict[-860719551] = { return SecretApi144.DecryptedMessageAction.parse_decryptedMessageActionTyping($0) } + dict[467867529] = { return SecretApi144.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } + dict[1474341323] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } + dict[1485441687] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } + dict[1790809986] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } + dict[144661578] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } + dict[-90853155] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaExternalDocument($0) } + dict[893913689] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } + dict[-235238024] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } + dict[-1978796689] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaVenue($0) } + dict[-1760785394] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } + dict[-452652584] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaWebPage($0) } dict[297109817] = { return SecretApi144.DocumentAttribute.parse_documentAttributeAnimated($0) } - dict[358154344] = { return SecretApi144.DocumentAttribute.parse_documentAttributeFilename($0) } - dict[978674434] = { return SecretApi144.DocumentAttribute.parse_documentAttributeSticker($0) } dict[-1739392570] = { return SecretApi144.DocumentAttribute.parse_documentAttributeAudio($0) } + dict[358154344] = { return SecretApi144.DocumentAttribute.parse_documentAttributeFilename($0) } + dict[1815593308] = { return SecretApi144.DocumentAttribute.parse_documentAttributeImageSize($0) } + dict[978674434] = { return SecretApi144.DocumentAttribute.parse_documentAttributeSticker($0) } dict[250621158] = { return SecretApi144.DocumentAttribute.parse_documentAttributeVideo($0) } - dict[-2044933984] = { return SecretApi144.InputStickerSet.parse_inputStickerSetShortName($0) } + dict[1406570614] = { return SecretApi144.FileLocation.parse_fileLocation($0) } + dict[2086234950] = { return SecretApi144.FileLocation.parse_fileLocationUnavailable($0) } dict[-4838507] = { return SecretApi144.InputStickerSet.parse_inputStickerSetEmpty($0) } - dict[-1148011883] = { return SecretApi144.MessageEntity.parse_messageEntityUnknown($0) } - dict[-100378723] = { return SecretApi144.MessageEntity.parse_messageEntityMention($0) } - dict[1868782349] = { return SecretApi144.MessageEntity.parse_messageEntityHashtag($0) } - dict[1827637959] = { return SecretApi144.MessageEntity.parse_messageEntityBotCommand($0) } - dict[1859134776] = { return SecretApi144.MessageEntity.parse_messageEntityUrl($0) } - dict[1692693954] = { return SecretApi144.MessageEntity.parse_messageEntityEmail($0) } + dict[-2044933984] = { return SecretApi144.InputStickerSet.parse_inputStickerSetShortName($0) } + dict[34469328] = { return SecretApi144.MessageEntity.parse_messageEntityBlockquote($0) } dict[-1117713463] = { return SecretApi144.MessageEntity.parse_messageEntityBold($0) } - dict[-2106619040] = { return SecretApi144.MessageEntity.parse_messageEntityItalic($0) } + dict[1827637959] = { return SecretApi144.MessageEntity.parse_messageEntityBotCommand($0) } dict[681706865] = { return SecretApi144.MessageEntity.parse_messageEntityCode($0) } + dict[-925956616] = { return SecretApi144.MessageEntity.parse_messageEntityCustomEmoji($0) } + dict[1692693954] = { return SecretApi144.MessageEntity.parse_messageEntityEmail($0) } + dict[1868782349] = { return SecretApi144.MessageEntity.parse_messageEntityHashtag($0) } + dict[-2106619040] = { return SecretApi144.MessageEntity.parse_messageEntityItalic($0) } + dict[-100378723] = { return SecretApi144.MessageEntity.parse_messageEntityMention($0) } dict[1938967520] = { return SecretApi144.MessageEntity.parse_messageEntityPre($0) } + dict[852137487] = { return SecretApi144.MessageEntity.parse_messageEntitySpoiler($0) } + dict[-1090087980] = { return SecretApi144.MessageEntity.parse_messageEntityStrike($0) } dict[1990644519] = { return SecretApi144.MessageEntity.parse_messageEntityTextUrl($0) } dict[-1672577397] = { return SecretApi144.MessageEntity.parse_messageEntityUnderline($0) } - dict[-1090087980] = { return SecretApi144.MessageEntity.parse_messageEntityStrike($0) } - dict[34469328] = { return SecretApi144.MessageEntity.parse_messageEntityBlockquote($0) } - dict[Int32(bitPattern: 0xc8cf05f8 as UInt32)] = { return SecretApi144.MessageEntity.parse_messageEntityCustomEmoji($0) } - dict[Int32(bitPattern: 0x32ca960f as UInt32)] = { return SecretApi144.MessageEntity.parse_messageEntitySpoiler($0) } - dict[144661578] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } - dict[893913689] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } - dict[1485441687] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } - dict[1474341323] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } - dict[-90853155] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaExternalDocument($0) } - dict[-235238024] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } - dict[Int32(bitPattern: 0x6abd9782 as UInt32)] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } - dict[-1760785394] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } - dict[-1978796689] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaVenue($0) } - dict[-452652584] = { return SecretApi144.DecryptedMessageMedia.parse_decryptedMessageMediaWebPage($0) } + dict[-1148011883] = { return SecretApi144.MessageEntity.parse_messageEntityUnknown($0) } + dict[1859134776] = { return SecretApi144.MessageEntity.parse_messageEntityUrl($0) } + dict[-374917894] = { return SecretApi144.PhotoSize.parse_photoCachedSize($0) } + dict[2009052699] = { return SecretApi144.PhotoSize.parse_photoSize($0) } + dict[236446268] = { return SecretApi144.PhotoSize.parse_photoSizeEmpty($0) } + dict[-44119819] = { return SecretApi144.SendMessageAction.parse_sendMessageCancelAction($0) } + dict[1653390447] = { return SecretApi144.SendMessageAction.parse_sendMessageChooseContactAction($0) } + dict[393186209] = { return SecretApi144.SendMessageAction.parse_sendMessageGeoLocationAction($0) } + dict[-718310409] = { return SecretApi144.SendMessageAction.parse_sendMessageRecordAudioAction($0) } + dict[-1997373508] = { return SecretApi144.SendMessageAction.parse_sendMessageRecordRoundAction($0) } + dict[-1584933265] = { return SecretApi144.SendMessageAction.parse_sendMessageRecordVideoAction($0) } + dict[381645902] = { return SecretApi144.SendMessageAction.parse_sendMessageTypingAction($0) } + dict[-424899985] = { return SecretApi144.SendMessageAction.parse_sendMessageUploadAudioAction($0) } + dict[-1884362354] = { return SecretApi144.SendMessageAction.parse_sendMessageUploadDocumentAction($0) } + dict[-1727382502] = { return SecretApi144.SendMessageAction.parse_sendMessageUploadPhotoAction($0) } + dict[-1150187996] = { return SecretApi144.SendMessageAction.parse_sendMessageUploadRoundAction($0) } + dict[-1845219337] = { return SecretApi144.SendMessageAction.parse_sendMessageUploadVideoAction($0) } return dict }() @@ -70,18 +85,18 @@ public struct SecretApi144 { } return nil } - - fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { - if let parser = parsers[signature] { - return parser(reader) - } - else { - telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") - return nil - } + + fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { + if let parser = parsers[signature] { + return parser(reader) } - - fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { + else { + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + return nil + } + } + + fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { if let count = reader.readInt32() { var array = [T]() var i: Int32 = 0 @@ -107,226 +122,293 @@ public struct SecretApi144 { } return nil } - + public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) { switch object { - case let _1 as SecretApi144.DecryptedMessageAction: - _1.serialize(buffer, boxed) - case let _1 as SecretApi144.PhotoSize: - _1.serialize(buffer, boxed) - case let _1 as SecretApi144.FileLocation: - _1.serialize(buffer, boxed) - case let _1 as SecretApi144.DecryptedMessageLayer: - _1.serialize(buffer, boxed) - case let _1 as SecretApi144.DecryptedMessage: - _1.serialize(buffer, boxed) - case let _1 as SecretApi144.DocumentAttribute: - _1.serialize(buffer, boxed) - case let _1 as SecretApi144.InputStickerSet: - _1.serialize(buffer, boxed) - case let _1 as SecretApi144.MessageEntity: - _1.serialize(buffer, boxed) - case let _1 as SecretApi144.DecryptedMessageMedia: - _1.serialize(buffer, boxed) - default: + case let _1 as SecretApi144.Bool: + _1.serialize(buffer, boxed) + case let _1 as SecretApi144.DecryptedMessage: + _1.serialize(buffer, boxed) + case let _1 as SecretApi144.DecryptedMessageAction: + _1.serialize(buffer, boxed) + case let _1 as SecretApi144.DecryptedMessageLayer: + _1.serialize(buffer, boxed) + case let _1 as SecretApi144.DecryptedMessageMedia: + _1.serialize(buffer, boxed) + case let _1 as SecretApi144.DocumentAttribute: + _1.serialize(buffer, boxed) + case let _1 as SecretApi144.FileLocation: + _1.serialize(buffer, boxed) + case let _1 as SecretApi144.InputStickerSet: + _1.serialize(buffer, boxed) + case let _1 as SecretApi144.MessageEntity: + _1.serialize(buffer, boxed) + case let _1 as SecretApi144.PhotoSize: + _1.serialize(buffer, boxed) + case let _1 as SecretApi144.SendMessageAction: + _1.serialize(buffer, boxed) + default: + break + } + } + + public enum Bool { + case boolFalse + case boolTrue + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boolFalse: + if boxed { + buffer.appendInt32(-1132882121) + } break + case .boolTrue: + if boxed { + buffer.appendInt32(-1720552011) + } + break + } + } + + fileprivate static func parse_boolFalse(_ reader: BufferReader) -> Bool? { + return SecretApi144.Bool.boolFalse + } + fileprivate static func parse_boolTrue(_ reader: BufferReader) -> Bool? { + return SecretApi144.Bool.boolTrue + } + } + + public enum DecryptedMessage { + case decryptedMessage(flags: Int32, randomId: Int64, ttl: Int32, message: String, media: SecretApi144.DecryptedMessageMedia?, entities: [SecretApi144.MessageEntity]?, viaBotName: String?, replyToRandomId: Int64?, groupedId: Int64?) + case decryptedMessageService(randomId: Int64, action: SecretApi144.DecryptedMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessage(let flags, let randomId, let ttl, let message, let media, let entities, let viaBotName, let replyToRandomId, let groupedId): + if boxed { + buffer.appendInt32(-1848883596) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(randomId, buffer: buffer, boxed: false) + serializeInt32(ttl, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 9) != 0 { + media!.serialize(buffer, true) + } + if Int(flags) & Int(1 << 7) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + } + } + if Int(flags) & Int(1 << 11) != 0 { + serializeString(viaBotName!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 3) != 0 { + serializeInt64(replyToRandomId!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 17) != 0 { + serializeInt64(groupedId!, buffer: buffer, boxed: false) + } + break + case .decryptedMessageService(let randomId, let action): + if boxed { + buffer.appendInt32(1930838368) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: SecretApi144.DecryptedMessageMedia? + if Int(_1 ?? 0) & Int(1 << 9) != 0 { + if let signature = reader.readInt32() { + _5 = SecretApi144.parse(reader, signature: signature) as? SecretApi144.DecryptedMessageMedia + } + } + var _6: [SecretApi144.MessageEntity]? + if Int(_1 ?? 0) & Int(1 << 7) != 0 { + if let _ = reader.readInt32() { + _6 = SecretApi144.parseVector(reader, elementSignature: 0, elementType: SecretApi144.MessageEntity.self) + } + } + var _7: String? + if Int(_1 ?? 0) & Int(1 << 11) != 0 { + _7 = parseString(reader) + } + var _8: Int64? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _8 = reader.readInt64() + } + var _9: Int64? + if Int(_1 ?? 0) & Int(1 << 17) != 0 { + _9 = reader.readInt64() + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi144.DecryptedMessage.decryptedMessage(flags: _1!, randomId: _2!, ttl: _3!, message: _4!, media: _5, entities: _6, viaBotName: _7, replyToRandomId: _8, groupedId: _9) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: SecretApi144.DecryptedMessageAction? + if let signature = reader.readInt32() { + _2 = SecretApi144.parse(reader, signature: signature) as? SecretApi144.DecryptedMessageAction + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) + } + else { + return nil + } } } public enum DecryptedMessageAction { - case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) - case decryptedMessageActionReadMessages(randomIds: [Int64]) - case decryptedMessageActionDeleteMessages(randomIds: [Int64]) - case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) - case decryptedMessageActionFlushHistory - case decryptedMessageActionNotifyLayer(layer: Int32) - case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) - case decryptedMessageActionRequestKey(exchangeId: Int64, gA: Buffer) - case decryptedMessageActionAcceptKey(exchangeId: Int64, gB: Buffer, keyFingerprint: Int64) case decryptedMessageActionAbortKey(exchangeId: Int64) + case decryptedMessageActionAcceptKey(exchangeId: Int64, gB: Buffer, keyFingerprint: Int64) case decryptedMessageActionCommitKey(exchangeId: Int64, keyFingerprint: Int64) + case decryptedMessageActionDeleteMessages(randomIds: [Int64]) + case decryptedMessageActionFlushHistory case decryptedMessageActionNoop - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageActionSetMessageTTL(let ttlSeconds): - if boxed { - buffer.appendInt32(-1586283796) - } - serializeInt32(ttlSeconds, buffer: buffer, boxed: false) - break - case .decryptedMessageActionReadMessages(let randomIds): - if boxed { - buffer.appendInt32(206520510) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionDeleteMessages(let randomIds): - if boxed { - buffer.appendInt32(1700872964) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionScreenshotMessages(let randomIds): - if boxed { - buffer.appendInt32(-1967000459) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionFlushHistory: - if boxed { - buffer.appendInt32(1729750108) - } - - break - case .decryptedMessageActionNotifyLayer(let layer): - if boxed { - buffer.appendInt32(-217806717) - } - serializeInt32(layer, buffer: buffer, boxed: false) - break - case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): - if boxed { - buffer.appendInt32(1360072880) - } - serializeInt32(startSeqNo, buffer: buffer, boxed: false) - serializeInt32(endSeqNo, buffer: buffer, boxed: false) - break - case .decryptedMessageActionRequestKey(let exchangeId, let gA): - if boxed { - buffer.appendInt32(-204906213) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - serializeBytes(gA, buffer: buffer, boxed: false) - break - case .decryptedMessageActionAcceptKey(let exchangeId, let gB, let keyFingerprint): - if boxed { - buffer.appendInt32(1877046107) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - serializeBytes(gB, buffer: buffer, boxed: false) - serializeInt64(keyFingerprint, buffer: buffer, boxed: false) - break - case .decryptedMessageActionAbortKey(let exchangeId): - if boxed { - buffer.appendInt32(-586814357) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - break - case .decryptedMessageActionCommitKey(let exchangeId, let keyFingerprint): - if boxed { - buffer.appendInt32(-332526693) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - serializeInt64(keyFingerprint, buffer: buffer, boxed: false) - break - case .decryptedMessageActionNoop: - if boxed { - buffer.appendInt32(-1473258141) - } - - break - } - } - fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return SecretApi144.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) - } - else { - return nil + case decryptedMessageActionNotifyLayer(layer: Int32) + case decryptedMessageActionReadMessages(randomIds: [Int64]) + case decryptedMessageActionRequestKey(exchangeId: Int64, gA: Buffer) + case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) + case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) + case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) + case decryptedMessageActionTyping(action: SecretApi144.SendMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageActionAbortKey(let exchangeId): + if boxed { + buffer.appendInt32(-586814357) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + break + case .decryptedMessageActionAcceptKey(let exchangeId, let gB, let keyFingerprint): + if boxed { + buffer.appendInt32(1877046107) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gB, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionCommitKey(let exchangeId, let keyFingerprint): + if boxed { + buffer.appendInt32(-332526693) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionDeleteMessages(let randomIds): + if boxed { + buffer.appendInt32(1700872964) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionFlushHistory: + if boxed { + buffer.appendInt32(1729750108) + } + break + case .decryptedMessageActionNoop: + if boxed { + buffer.appendInt32(-1473258141) + } + break + case .decryptedMessageActionNotifyLayer(let layer): + if boxed { + buffer.appendInt32(-217806717) + } + serializeInt32(layer, buffer: buffer, boxed: false) + break + case .decryptedMessageActionReadMessages(let randomIds): + if boxed { + buffer.appendInt32(206520510) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionRequestKey(let exchangeId, let gA): + if boxed { + buffer.appendInt32(-204906213) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gA, buffer: buffer, boxed: false) + break + case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): + if boxed { + buffer.appendInt32(1360072880) + } + serializeInt32(startSeqNo, buffer: buffer, boxed: false) + serializeInt32(endSeqNo, buffer: buffer, boxed: false) + break + case .decryptedMessageActionScreenshotMessages(let randomIds): + if boxed { + buffer.appendInt32(-1967000459) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionSetMessageTTL(let ttlSeconds): + if boxed { + buffer.appendInt32(-1586283796) + } + serializeInt32(ttlSeconds, buffer: buffer, boxed: false) + break + case .decryptedMessageActionTyping(let action): + if boxed { + buffer.appendInt32(-860719551) + } + action.serialize(buffer, true) + break } } - fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi144.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi144.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi144.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi144.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi144.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi144.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { - return SecretApi144.DecryptedMessageAction.decryptedMessageActionFlushHistory - } - fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return SecretApi144.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionRequestKey(_ reader: BufferReader) -> DecryptedMessageAction? { + + fileprivate static func parse_decryptedMessageActionAbortKey(_ reader: BufferReader) -> DecryptedMessageAction? { var _1: Int64? _1 = reader.readInt64() - var _2: Buffer? - _2 = parseBytes(reader) let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.DecryptedMessageAction.decryptedMessageActionRequestKey(exchangeId: _1!, gA: _2!) + if _c1 { + return SecretApi144.DecryptedMessageAction.decryptedMessageActionAbortKey(exchangeId: _1!) } else { return nil @@ -349,17 +431,6 @@ public struct SecretApi144 { return nil } } - fileprivate static func parse_decryptedMessageActionAbortKey(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return SecretApi144.DecryptedMessageAction.decryptedMessageActionAbortKey(exchangeId: _1!) - } - else { - return nil - } - } fileprivate static func parse_decryptedMessageActionCommitKey(_ reader: BufferReader) -> DecryptedMessageAction? { var _1: Int64? _1 = reader.readInt64() @@ -374,196 +445,134 @@ public struct SecretApi144 { return nil } } + fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi144.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi144.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi144.DecryptedMessageAction.decryptedMessageActionFlushHistory + } fileprivate static func parse_decryptedMessageActionNoop(_ reader: BufferReader) -> DecryptedMessageAction? { return SecretApi144.DecryptedMessageAction.decryptedMessageActionNoop } - - - } - - public enum PhotoSize { - case photoSizeEmpty(type: String) - case photoSize(type: String, location: SecretApi144.FileLocation, w: Int32, h: Int32, size: Int32) - case photoCachedSize(type: String, location: SecretApi144.FileLocation, w: Int32, h: Int32, bytes: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .photoSizeEmpty(let type): - if boxed { - buffer.appendInt32(236446268) - } - serializeString(type, buffer: buffer, boxed: false) - break - case .photoSize(let type, let location, let w, let h, let size): - if boxed { - buffer.appendInt32(2009052699) - } - serializeString(type, buffer: buffer, boxed: false) - location.serialize(buffer, true) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - break - case .photoCachedSize(let type, let location, let w, let h, let bytes): - if boxed { - buffer.appendInt32(-374917894) - } - serializeString(type, buffer: buffer, boxed: false) - location.serialize(buffer, true) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeBytes(bytes, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_photoSizeEmpty(_ reader: BufferReader) -> PhotoSize? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return SecretApi144.PhotoSize.photoSizeEmpty(type: _1!) - } - else { - return nil - } - } - fileprivate static func parse_photoSize(_ reader: BufferReader) -> PhotoSize? { - var _1: String? - _1 = parseString(reader) - var _2: SecretApi144.FileLocation? - if let signature = reader.readInt32() { - _2 = SecretApi144.parse(reader, signature: signature) as? SecretApi144.FileLocation - } - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi144.PhotoSize.photoSize(type: _1!, location: _2!, w: _3!, h: _4!, size: _5!) - } - else { - return nil - } - } - fileprivate static func parse_photoCachedSize(_ reader: BufferReader) -> PhotoSize? { - var _1: String? - _1 = parseString(reader) - var _2: SecretApi144.FileLocation? - if let signature = reader.readInt32() { - _2 = SecretApi144.parse(reader, signature: signature) as? SecretApi144.FileLocation - } - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Buffer? - _5 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi144.PhotoSize.photoCachedSize(type: _1!, location: _2!, w: _3!, h: _4!, bytes: _5!) - } - else { - return nil - } - } - - - } - - public enum FileLocation { - case fileLocationUnavailable(volumeId: Int64, localId: Int32, secret: Int64) - case fileLocation(dcId: Int32, volumeId: Int64, localId: Int32, secret: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .fileLocationUnavailable(let volumeId, let localId, let secret): - if boxed { - buffer.appendInt32(2086234950) - } - serializeInt64(volumeId, buffer: buffer, boxed: false) - serializeInt32(localId, buffer: buffer, boxed: false) - serializeInt64(secret, buffer: buffer, boxed: false) - break - case .fileLocation(let dcId, let volumeId, let localId, let secret): - if boxed { - buffer.appendInt32(1406570614) - } - serializeInt32(dcId, buffer: buffer, boxed: false) - serializeInt64(volumeId, buffer: buffer, boxed: false) - serializeInt32(localId, buffer: buffer, boxed: false) - serializeInt64(secret, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_fileLocationUnavailable(_ reader: BufferReader) -> FileLocation? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi144.FileLocation.fileLocationUnavailable(volumeId: _1!, localId: _2!, secret: _3!) - } - else { - return nil - } - } - fileprivate static func parse_fileLocation(_ reader: BufferReader) -> FileLocation? { + fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { var _1: Int32? _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int64? - _4 = reader.readInt64() let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return SecretApi144.FileLocation.fileLocation(dcId: _1!, volumeId: _2!, localId: _3!, secret: _4!) + if _c1 { + return SecretApi144.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi144.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi144.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionRequestKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.DecryptedMessageAction.decryptedMessageActionRequestKey(exchangeId: _1!, gA: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi144.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi144.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi144.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionTyping(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: SecretApi144.SendMessageAction? + if let signature = reader.readInt32() { + _1 = SecretApi144.parse(reader, signature: signature) as? SecretApi144.SendMessageAction + } + let _c1 = _1 != nil + if _c1 { + return SecretApi144.DecryptedMessageAction.decryptedMessageActionTyping(action: _1!) } else { return nil } } - - } public enum DecryptedMessageLayer { case decryptedMessageLayer(randomBytes: Buffer, layer: Int32, inSeqNo: Int32, outSeqNo: Int32, message: SecretApi144.DecryptedMessage) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): - if boxed { - buffer.appendInt32(467867529) - } - serializeBytes(randomBytes, buffer: buffer, boxed: false) - serializeInt32(layer, buffer: buffer, boxed: false) - serializeInt32(inSeqNo, buffer: buffer, boxed: false) - serializeInt32(outSeqNo, buffer: buffer, boxed: false) - message.serialize(buffer, true) - break - } - } + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): + if boxed { + buffer.appendInt32(467867529) + } + serializeBytes(randomBytes, buffer: buffer, boxed: false) + serializeInt32(layer, buffer: buffer, boxed: false) + serializeInt32(inSeqNo, buffer: buffer, boxed: false) + serializeInt32(outSeqNo, buffer: buffer, boxed: false) + message.serialize(buffer, true) + break + } + } + fileprivate static func parse_decryptedMessageLayer(_ reader: BufferReader) -> DecryptedMessageLayer? { var _1: Buffer? _1 = parseBytes(reader) @@ -589,802 +598,156 @@ public struct SecretApi144 { return nil } } - - - } - - public enum DecryptedMessage { - case decryptedMessageService(randomId: Int64, action: SecretApi144.DecryptedMessageAction) - case decryptedMessage(flags: Int32, randomId: Int64, ttl: Int32, message: String, media: SecretApi144.DecryptedMessageMedia?, entities: [SecretApi144.MessageEntity]?, viaBotName: String?, replyToRandomId: Int64?, groupedId: Int64?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageService(let randomId, let action): - if boxed { - buffer.appendInt32(1930838368) - } - serializeInt64(randomId, buffer: buffer, boxed: false) - action.serialize(buffer, true) - break - case .decryptedMessage(let flags, let randomId, let ttl, let message, let media, let entities, let viaBotName, let replyToRandomId, let groupedId): - if boxed { - buffer.appendInt32(-1848883596) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(randomId, buffer: buffer, boxed: false) - serializeInt32(ttl, buffer: buffer, boxed: false) - serializeString(message, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 9) != 0 {media!.serialize(buffer, true)} - if Int(flags) & Int(1 << 7) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(entities!.count)) - for item in entities! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 11) != 0 {serializeString(viaBotName!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {serializeInt64(replyToRandomId!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 17) != 0 {serializeInt64(groupedId!, buffer: buffer, boxed: false)} - break - } - } - fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { - var _1: Int64? - _1 = reader.readInt64() - var _2: SecretApi144.DecryptedMessageAction? - if let signature = reader.readInt32() { - _2 = SecretApi144.parse(reader, signature: signature) as? SecretApi144.DecryptedMessageAction - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() - var _4: String? - _4 = parseString(reader) - var _5: SecretApi144.DecryptedMessageMedia? - if Int(_1!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { - _5 = SecretApi144.parse(reader, signature: signature) as? SecretApi144.DecryptedMessageMedia - } } - var _6: [SecretApi144.MessageEntity]? - if Int(_1!) & Int(1 << 7) != 0 {if let _ = reader.readInt32() { - _6 = SecretApi144.parseVector(reader, elementSignature: 0, elementType: SecretApi144.MessageEntity.self) - } } - var _7: String? - if Int(_1!) & Int(1 << 11) != 0 {_7 = parseString(reader) } - var _8: Int64? - if Int(_1!) & Int(1 << 3) != 0 {_8 = reader.readInt64() } - var _9: Int64? - if Int(_1!) & Int(1 << 17) != 0 {_9 = reader.readInt64() } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 9) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 7) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 17) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return SecretApi144.DecryptedMessage.decryptedMessage(flags: _1!, randomId: _2!, ttl: _3!, message: _4!, media: _5, entities: _6, viaBotName: _7, replyToRandomId: _8, groupedId: _9) - } - else { - return nil - } - } - - - } - - public enum DocumentAttribute { - case documentAttributeImageSize(w: Int32, h: Int32) - case documentAttributeAnimated - case documentAttributeFilename(fileName: String) - case documentAttributeSticker(alt: String, stickerset: SecretApi144.InputStickerSet) - case documentAttributeAudio(flags: Int32, duration: Int32, title: String?, performer: String?, waveform: Buffer?) - case documentAttributeVideo(flags: Int32, duration: Int32, w: Int32, h: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .documentAttributeImageSize(let w, let h): - if boxed { - buffer.appendInt32(1815593308) - } - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - break - case .documentAttributeAnimated: - if boxed { - buffer.appendInt32(297109817) - } - - break - case .documentAttributeFilename(let fileName): - if boxed { - buffer.appendInt32(358154344) - } - serializeString(fileName, buffer: buffer, boxed: false) - break - case .documentAttributeSticker(let alt, let stickerset): - if boxed { - buffer.appendInt32(978674434) - } - serializeString(alt, buffer: buffer, boxed: false) - stickerset.serialize(buffer, true) - break - case .documentAttributeAudio(let flags, let duration, let title, let performer, let waveform): - if boxed { - buffer.appendInt32(-1739392570) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(duration, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(title!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(performer!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {serializeBytes(waveform!, buffer: buffer, boxed: false)} - break - case .documentAttributeVideo(let flags, let duration, let w, let h): - if boxed { - buffer.appendInt32(250621158) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(duration, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_documentAttributeImageSize(_ reader: BufferReader) -> DocumentAttribute? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.DocumentAttribute.documentAttributeImageSize(w: _1!, h: _2!) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeAnimated(_ reader: BufferReader) -> DocumentAttribute? { - return SecretApi144.DocumentAttribute.documentAttributeAnimated - } - fileprivate static func parse_documentAttributeFilename(_ reader: BufferReader) -> DocumentAttribute? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return SecretApi144.DocumentAttribute.documentAttributeFilename(fileName: _1!) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeSticker(_ reader: BufferReader) -> DocumentAttribute? { - var _1: String? - _1 = parseString(reader) - var _2: SecretApi144.InputStickerSet? - if let signature = reader.readInt32() { - _2 = SecretApi144.parse(reader, signature: signature) as? SecretApi144.InputStickerSet - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.DocumentAttribute.documentAttributeSticker(alt: _1!, stickerset: _2!) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeAudio(_ reader: BufferReader) -> DocumentAttribute? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } - var _4: String? - if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } - var _5: Buffer? - if Int(_1!) & Int(1 << 2) != 0 {_5 = parseBytes(reader) } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi144.DocumentAttribute.documentAttributeAudio(flags: _1!, duration: _2!, title: _3, performer: _4, waveform: _5) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeVideo(_ reader: BufferReader) -> DocumentAttribute? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return SecretApi144.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!) - } - else { - return nil - } - } - - - } - - public enum InputStickerSet { - case inputStickerSetShortName(shortName: String) - case inputStickerSetEmpty - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputStickerSetShortName(let shortName): - if boxed { - buffer.appendInt32(-2044933984) - } - serializeString(shortName, buffer: buffer, boxed: false) - break - case .inputStickerSetEmpty: - if boxed { - buffer.appendInt32(-4838507) - } - - break - } - } - fileprivate static func parse_inputStickerSetShortName(_ reader: BufferReader) -> InputStickerSet? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return SecretApi144.InputStickerSet.inputStickerSetShortName(shortName: _1!) - } - else { - return nil - } - } - fileprivate static func parse_inputStickerSetEmpty(_ reader: BufferReader) -> InputStickerSet? { - return SecretApi144.InputStickerSet.inputStickerSetEmpty - } - - - } - - public enum MessageEntity { - case messageEntityUnknown(offset: Int32, length: Int32) - case messageEntityMention(offset: Int32, length: Int32) - case messageEntityHashtag(offset: Int32, length: Int32) - case messageEntityBotCommand(offset: Int32, length: Int32) - case messageEntityUrl(offset: Int32, length: Int32) - case messageEntityEmail(offset: Int32, length: Int32) - case messageEntityBold(offset: Int32, length: Int32) - case messageEntityItalic(offset: Int32, length: Int32) - case messageEntityCode(offset: Int32, length: Int32) - case messageEntityPre(offset: Int32, length: Int32, language: String) - case messageEntityTextUrl(offset: Int32, length: Int32, url: String) - case messageEntityUnderline(offset: Int32, length: Int32) - case messageEntityStrike(offset: Int32, length: Int32) - case messageEntityBlockquote(offset: Int32, length: Int32) - case messageEntityCustomEmoji(offset: Int32, length: Int32, documentId: Int64) - case messageEntitySpoiler(offset: Int32, length: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .messageEntityUnknown(let offset, let length): - if boxed { - buffer.appendInt32(-1148011883) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityMention(let offset, let length): - if boxed { - buffer.appendInt32(-100378723) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityHashtag(let offset, let length): - if boxed { - buffer.appendInt32(1868782349) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityBotCommand(let offset, let length): - if boxed { - buffer.appendInt32(1827637959) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityUrl(let offset, let length): - if boxed { - buffer.appendInt32(1859134776) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityEmail(let offset, let length): - if boxed { - buffer.appendInt32(1692693954) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityBold(let offset, let length): - if boxed { - buffer.appendInt32(-1117713463) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityItalic(let offset, let length): - if boxed { - buffer.appendInt32(-2106619040) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityCode(let offset, let length): - if boxed { - buffer.appendInt32(681706865) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityPre(let offset, let length, let language): - if boxed { - buffer.appendInt32(1938967520) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - serializeString(language, buffer: buffer, boxed: false) - break - case .messageEntityTextUrl(let offset, let length, let url): - if boxed { - buffer.appendInt32(1990644519) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - serializeString(url, buffer: buffer, boxed: false) - break - case .messageEntityUnderline(let offset, let length): - if boxed { - buffer.appendInt32(-1672577397) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityStrike(let offset, let length): - if boxed { - buffer.appendInt32(-1090087980) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityBlockquote(let offset, let length): - if boxed { - buffer.appendInt32(34469328) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityCustomEmoji(let offset, let length, let documentId): - if boxed { - buffer.appendInt32(Int32(bitPattern: 0xc8cf05f8 as UInt32)) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - serializeInt64(documentId, buffer: buffer, boxed: false) - break - case .messageEntitySpoiler(let offset, let length): - if boxed { - buffer.appendInt32(Int32(bitPattern: 0x32ca960f as UInt32)) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_messageEntityUnknown(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.MessageEntity.messageEntityUnknown(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityMention(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.MessageEntity.messageEntityMention(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityHashtag(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.MessageEntity.messageEntityHashtag(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityBotCommand(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.MessageEntity.messageEntityBotCommand(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityUrl(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.MessageEntity.messageEntityUrl(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.MessageEntity.messageEntityEmail(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityBold(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.MessageEntity.messageEntityBold(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityItalic(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.MessageEntity.messageEntityItalic(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityCode(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.MessageEntity.messageEntityCode(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityPre(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi144.MessageEntity.messageEntityPre(offset: _1!, length: _2!, language: _3!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityTextUrl(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi144.MessageEntity.messageEntityTextUrl(offset: _1!, length: _2!, url: _3!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityUnderline(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.MessageEntity.messageEntityUnderline(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityStrike(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.MessageEntity.messageEntityStrike(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityBlockquote(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.MessageEntity.messageEntityBlockquote(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityCustomEmoji(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi144.MessageEntity.messageEntityCustomEmoji(offset: _1!, length: _2!, documentId: _3!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntitySpoiler(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.MessageEntity.messageEntitySpoiler(offset: _1!, length: _2!) - } - else { - return nil - } - } } public enum DecryptedMessageMedia { - case decryptedMessageMediaEmpty - case decryptedMessageMediaGeoPoint(lat: Double, long: Double) - case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) case decryptedMessageMediaAudio(duration: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer) - case decryptedMessageMediaExternalDocument(id: Int64, accessHash: Int64, date: Int32, mimeType: String, size: Int32, thumb: SecretApi144.PhotoSize, dcId: Int32, attributes: [SecretApi144.DocumentAttribute]) - case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) case decryptedMessageMediaDocument(thumb: Buffer, thumbW: Int32, thumbH: Int32, mimeType: String, size: Int64, key: Buffer, iv: Buffer, attributes: [SecretApi144.DocumentAttribute], caption: String) - case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaEmpty + case decryptedMessageMediaExternalDocument(id: Int64, accessHash: Int64, date: Int32, mimeType: String, size: Int32, thumb: SecretApi144.PhotoSize, dcId: Int32, attributes: [SecretApi144.DocumentAttribute]) + case decryptedMessageMediaGeoPoint(lat: Double, long: Double) + case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) case decryptedMessageMediaVenue(lat: Double, long: Double, title: String, address: String, provider: String, venueId: String) + case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) case decryptedMessageMediaWebPage(url: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageMediaEmpty: - if boxed { - buffer.appendInt32(144661578) - } - - break - case .decryptedMessageMediaGeoPoint(let lat, let long): - if boxed { - buffer.appendInt32(893913689) - } - serializeDouble(lat, buffer: buffer, boxed: false) - serializeDouble(long, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): - if boxed { - buffer.appendInt32(1485441687) - } - serializeString(phoneNumber, buffer: buffer, boxed: false) - serializeString(firstName, buffer: buffer, boxed: false) - serializeString(lastName, buffer: buffer, boxed: false) - serializeInt32(userId, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): - if boxed { - buffer.appendInt32(1474341323) - } - serializeInt32(duration, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaExternalDocument(let id, let accessHash, let date, let mimeType, let size, let thumb, let dcId, let attributes): - if boxed { - buffer.appendInt32(-90853155) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - thumb.serialize(buffer, true) - serializeInt32(dcId, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(attributes.count)) - for item in attributes { - item.serialize(buffer, true) - } - break - case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv, let caption): - if boxed { - buffer.appendInt32(-235238024) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - serializeString(caption, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let mimeType, let size, let key, let iv, let attributes, let caption): - if boxed { - buffer.appendInt32(Int32(bitPattern: 0x6abd9782 as UInt32)) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt64(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(attributes.count)) - for item in attributes { - item.serialize(buffer, true) - } - serializeString(caption, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv, let caption): - if boxed { - buffer.appendInt32(-1760785394) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeInt32(duration, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - serializeString(caption, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaVenue(let lat, let long, let title, let address, let provider, let venueId): - if boxed { - buffer.appendInt32(-1978796689) - } - serializeDouble(lat, buffer: buffer, boxed: false) - serializeDouble(long, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - serializeString(address, buffer: buffer, boxed: false) - serializeString(provider, buffer: buffer, boxed: false) - serializeString(venueId, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaWebPage(let url): - if boxed { - buffer.appendInt32(-452652584) - } - serializeString(url, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { - return SecretApi144.DecryptedMessageMedia.decryptedMessageMediaEmpty + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(1474341323) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): + if boxed { + buffer.appendInt32(1485441687) + } + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + serializeInt32(userId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let mimeType, let size, let key, let iv, let attributes, let caption): + if boxed { + buffer.appendInt32(1790809986) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt64(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaEmpty: + if boxed { + buffer.appendInt32(144661578) + } + break + case .decryptedMessageMediaExternalDocument(let id, let accessHash, let date, let mimeType, let size, let thumb, let dcId, let attributes): + if boxed { + buffer.appendInt32(-90853155) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + thumb.serialize(buffer, true) + serializeInt32(dcId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + break + case .decryptedMessageMediaGeoPoint(let lat, let long): + if boxed { + buffer.appendInt32(893913689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-235238024) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVenue(let lat, let long, let title, let address, let provider, let venueId): + if boxed { + buffer.appendInt32(-1978796689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(address, buffer: buffer, boxed: false) + serializeString(provider, buffer: buffer, boxed: false) + serializeString(venueId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-1760785394) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaWebPage(let url): + if boxed { + buffer.appendInt32(-452652584) + } + serializeString(url, buffer: buffer, boxed: false) + break + } } - fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Double? - _1 = reader.readDouble() - var _2: Double? - _2 = reader.readDouble() + + fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + var _4: Buffer? + _4 = parseBytes(reader) + var _5: Buffer? + _5 = parseBytes(reader) let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi144.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi144.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) } else { return nil @@ -1410,29 +773,46 @@ public struct SecretApi144 { return nil } } - fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) + fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() var _3: Int32? _3 = reader.readInt32() - var _4: Buffer? - _4 = parseBytes(reader) - var _5: Buffer? - _5 = parseBytes(reader) + var _4: String? + _4 = parseString(reader) + var _5: Int64? + _5 = reader.readInt64() + var _6: Buffer? + _6 = parseBytes(reader) + var _7: Buffer? + _7 = parseBytes(reader) + var _8: [SecretApi144.DocumentAttribute]? + if let _ = reader.readInt32() { + _8 = SecretApi144.parseVector(reader, elementSignature: 0, elementType: SecretApi144.DocumentAttribute.self) + } + var _9: String? + _9 = parseString(reader) let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi144.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi144.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, mimeType: _4!, size: _5!, key: _6!, iv: _7!, attributes: _8!, caption: _9!) } else { return nil } } + fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { + return SecretApi144.DecryptedMessageMedia.decryptedMessageMediaEmpty + } fileprivate static func parse_decryptedMessageMediaExternalDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: Int64? _1 = reader.readInt64() @@ -1469,6 +849,20 @@ public struct SecretApi144 { return nil } } + fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + } + else { + return nil + } + } fileprivate static func parse_decryptedMessageMediaPhoto(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: Buffer? _1 = parseBytes(reader) @@ -1504,38 +898,27 @@ public struct SecretApi144 { return nil } } - fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Buffer? - _1 = parseBytes(reader) - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() + fileprivate static func parse_decryptedMessageMediaVenue(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + var _3: String? + _3 = parseString(reader) var _4: String? _4 = parseString(reader) - var _5: Int64? - _5 = reader.readInt64() - var _6: Buffer? - _6 = parseBytes(reader) - var _7: Buffer? - _7 = parseBytes(reader) - var _8: [SecretApi144.DocumentAttribute]? - if let _ = reader.readInt32() { - _8 = SecretApi144.parseVector(reader, elementSignature: 0, elementType: SecretApi144.DocumentAttribute.self) - } - var _9: String? - _9 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: String? + _6 = parseString(reader) let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return SecretApi144.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, mimeType: _4!, size: _5!, key: _6!, iv: _7!, attributes: _8!, caption: _9!) + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return SecretApi144.DecryptedMessageMedia.decryptedMessageMediaVenue(lat: _1!, long: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!) } else { return nil @@ -1582,32 +965,6 @@ public struct SecretApi144 { return nil } } - fileprivate static func parse_decryptedMessageMediaVenue(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Double? - _1 = reader.readDouble() - var _2: Double? - _2 = reader.readDouble() - var _3: String? - _3 = parseString(reader) - var _4: String? - _4 = parseString(reader) - var _5: String? - _5 = parseString(reader) - var _6: String? - _6 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return SecretApi144.DecryptedMessageMedia.decryptedMessageMediaVenue(lat: _1!, long: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!) - } - else { - return nil - } - } fileprivate static func parse_decryptedMessageMediaWebPage(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: String? _1 = parseString(reader) @@ -1619,12 +976,854 @@ public struct SecretApi144 { return nil } } - - } - public struct functions { - + public enum DocumentAttribute { + case documentAttributeAnimated + case documentAttributeAudio(flags: Int32, duration: Int32, title: String?, performer: String?, waveform: Buffer?) + case documentAttributeFilename(fileName: String) + case documentAttributeImageSize(w: Int32, h: Int32) + case documentAttributeSticker(alt: String, stickerset: SecretApi144.InputStickerSet) + case documentAttributeVideo(flags: Int32, duration: Int32, w: Int32, h: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .documentAttributeAnimated: + if boxed { + buffer.appendInt32(297109817) + } + break + case .documentAttributeAudio(let flags, let duration, let title, let performer, let waveform): + if boxed { + buffer.appendInt32(-1739392570) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 { + serializeString(title!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 1) != 0 { + serializeString(performer!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 2) != 0 { + serializeBytes(waveform!, buffer: buffer, boxed: false) + } + break + case .documentAttributeFilename(let fileName): + if boxed { + buffer.appendInt32(358154344) + } + serializeString(fileName, buffer: buffer, boxed: false) + break + case .documentAttributeImageSize(let w, let h): + if boxed { + buffer.appendInt32(1815593308) + } + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + case .documentAttributeSticker(let alt, let stickerset): + if boxed { + buffer.appendInt32(978674434) + } + serializeString(alt, buffer: buffer, boxed: false) + stickerset.serialize(buffer, true) + break + case .documentAttributeVideo(let flags, let duration, let w, let h): + if boxed { + buffer.appendInt32(250621158) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_documentAttributeAnimated(_ reader: BufferReader) -> DocumentAttribute? { + return SecretApi144.DocumentAttribute.documentAttributeAnimated + } + fileprivate static func parse_documentAttributeAudio(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _3 = parseString(reader) + } + var _4: String? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _4 = parseString(reader) + } + var _5: Buffer? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _5 = parseBytes(reader) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi144.DocumentAttribute.documentAttributeAudio(flags: _1!, duration: _2!, title: _3, performer: _4, waveform: _5) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeFilename(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi144.DocumentAttribute.documentAttributeFilename(fileName: _1!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeImageSize(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.DocumentAttribute.documentAttributeImageSize(w: _1!, h: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeSticker(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi144.InputStickerSet? + if let signature = reader.readInt32() { + _2 = SecretApi144.parse(reader, signature: signature) as? SecretApi144.InputStickerSet + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.DocumentAttribute.documentAttributeSticker(alt: _1!, stickerset: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeVideo(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi144.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!) + } + else { + return nil + } + } + } + + public enum FileLocation { + case fileLocation(dcId: Int32, volumeId: Int64, localId: Int32, secret: Int64) + case fileLocationUnavailable(volumeId: Int64, localId: Int32, secret: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .fileLocation(let dcId, let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(1406570614) + } + serializeInt32(dcId, buffer: buffer, boxed: false) + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + case .fileLocationUnavailable(let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(2086234950) + } + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_fileLocation(_ reader: BufferReader) -> FileLocation? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int64? + _4 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi144.FileLocation.fileLocation(dcId: _1!, volumeId: _2!, localId: _3!, secret: _4!) + } + else { + return nil + } + } + fileprivate static func parse_fileLocationUnavailable(_ reader: BufferReader) -> FileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi144.FileLocation.fileLocationUnavailable(volumeId: _1!, localId: _2!, secret: _3!) + } + else { + return nil + } + } + } + + public enum InputStickerSet { + case inputStickerSetEmpty + case inputStickerSetShortName(shortName: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputStickerSetEmpty: + if boxed { + buffer.appendInt32(-4838507) + } + break + case .inputStickerSetShortName(let shortName): + if boxed { + buffer.appendInt32(-2044933984) + } + serializeString(shortName, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_inputStickerSetEmpty(_ reader: BufferReader) -> InputStickerSet? { + return SecretApi144.InputStickerSet.inputStickerSetEmpty + } + fileprivate static func parse_inputStickerSetShortName(_ reader: BufferReader) -> InputStickerSet? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi144.InputStickerSet.inputStickerSetShortName(shortName: _1!) + } + else { + return nil + } + } + } + + public enum MessageEntity { + case messageEntityBlockquote(offset: Int32, length: Int32) + case messageEntityBold(offset: Int32, length: Int32) + case messageEntityBotCommand(offset: Int32, length: Int32) + case messageEntityCode(offset: Int32, length: Int32) + case messageEntityCustomEmoji(offset: Int32, length: Int32, documentId: Int64) + case messageEntityEmail(offset: Int32, length: Int32) + case messageEntityHashtag(offset: Int32, length: Int32) + case messageEntityItalic(offset: Int32, length: Int32) + case messageEntityMention(offset: Int32, length: Int32) + case messageEntityPre(offset: Int32, length: Int32, language: String) + case messageEntitySpoiler(offset: Int32, length: Int32) + case messageEntityStrike(offset: Int32, length: Int32) + case messageEntityTextUrl(offset: Int32, length: Int32, url: String) + case messageEntityUnderline(offset: Int32, length: Int32) + case messageEntityUnknown(offset: Int32, length: Int32) + case messageEntityUrl(offset: Int32, length: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageEntityBlockquote(let offset, let length): + if boxed { + buffer.appendInt32(34469328) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityBold(let offset, let length): + if boxed { + buffer.appendInt32(-1117713463) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityBotCommand(let offset, let length): + if boxed { + buffer.appendInt32(1827637959) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityCode(let offset, let length): + if boxed { + buffer.appendInt32(681706865) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityCustomEmoji(let offset, let length, let documentId): + if boxed { + buffer.appendInt32(-925956616) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeInt64(documentId, buffer: buffer, boxed: false) + break + case .messageEntityEmail(let offset, let length): + if boxed { + buffer.appendInt32(1692693954) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityHashtag(let offset, let length): + if boxed { + buffer.appendInt32(1868782349) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityItalic(let offset, let length): + if boxed { + buffer.appendInt32(-2106619040) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityMention(let offset, let length): + if boxed { + buffer.appendInt32(-100378723) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityPre(let offset, let length, let language): + if boxed { + buffer.appendInt32(1938967520) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(language, buffer: buffer, boxed: false) + break + case .messageEntitySpoiler(let offset, let length): + if boxed { + buffer.appendInt32(852137487) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityStrike(let offset, let length): + if boxed { + buffer.appendInt32(-1090087980) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityTextUrl(let offset, let length, let url): + if boxed { + buffer.appendInt32(1990644519) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + break + case .messageEntityUnderline(let offset, let length): + if boxed { + buffer.appendInt32(-1672577397) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityUnknown(let offset, let length): + if boxed { + buffer.appendInt32(-1148011883) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityUrl(let offset, let length): + if boxed { + buffer.appendInt32(1859134776) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_messageEntityBlockquote(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.MessageEntity.messageEntityBlockquote(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityBold(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.MessageEntity.messageEntityBold(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityBotCommand(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.MessageEntity.messageEntityBotCommand(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityCode(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.MessageEntity.messageEntityCode(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityCustomEmoji(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi144.MessageEntity.messageEntityCustomEmoji(offset: _1!, length: _2!, documentId: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.MessageEntity.messageEntityEmail(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityHashtag(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.MessageEntity.messageEntityHashtag(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityItalic(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.MessageEntity.messageEntityItalic(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityMention(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.MessageEntity.messageEntityMention(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityPre(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi144.MessageEntity.messageEntityPre(offset: _1!, length: _2!, language: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntitySpoiler(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.MessageEntity.messageEntitySpoiler(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityStrike(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.MessageEntity.messageEntityStrike(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityTextUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi144.MessageEntity.messageEntityTextUrl(offset: _1!, length: _2!, url: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUnderline(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.MessageEntity.messageEntityUnderline(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUnknown(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.MessageEntity.messageEntityUnknown(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi144.MessageEntity.messageEntityUrl(offset: _1!, length: _2!) + } + else { + return nil + } + } + } + + public enum PhotoSize { + case photoCachedSize(type: String, location: SecretApi144.FileLocation, w: Int32, h: Int32, bytes: Buffer) + case photoSize(type: String, location: SecretApi144.FileLocation, w: Int32, h: Int32, size: Int32) + case photoSizeEmpty(type: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .photoCachedSize(let type, let location, let w, let h, let bytes): + if boxed { + buffer.appendInt32(-374917894) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeBytes(bytes, buffer: buffer, boxed: false) + break + case .photoSize(let type, let location, let w, let h, let size): + if boxed { + buffer.appendInt32(2009052699) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + break + case .photoSizeEmpty(let type): + if boxed { + buffer.appendInt32(236446268) + } + serializeString(type, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_photoCachedSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi144.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi144.parse(reader, signature: signature) as? SecretApi144.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi144.PhotoSize.photoCachedSize(type: _1!, location: _2!, w: _3!, h: _4!, bytes: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi144.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi144.parse(reader, signature: signature) as? SecretApi144.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi144.PhotoSize.photoSize(type: _1!, location: _2!, w: _3!, h: _4!, size: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSizeEmpty(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi144.PhotoSize.photoSizeEmpty(type: _1!) + } + else { + return nil + } + } + } + + public enum SendMessageAction { + case sendMessageCancelAction + case sendMessageChooseContactAction + case sendMessageGeoLocationAction + case sendMessageRecordAudioAction + case sendMessageRecordRoundAction + case sendMessageRecordVideoAction + case sendMessageTypingAction + case sendMessageUploadAudioAction + case sendMessageUploadDocumentAction + case sendMessageUploadPhotoAction + case sendMessageUploadRoundAction + case sendMessageUploadVideoAction + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sendMessageCancelAction: + if boxed { + buffer.appendInt32(-44119819) + } + break + case .sendMessageChooseContactAction: + if boxed { + buffer.appendInt32(1653390447) + } + break + case .sendMessageGeoLocationAction: + if boxed { + buffer.appendInt32(393186209) + } + break + case .sendMessageRecordAudioAction: + if boxed { + buffer.appendInt32(-718310409) + } + break + case .sendMessageRecordRoundAction: + if boxed { + buffer.appendInt32(-1997373508) + } + break + case .sendMessageRecordVideoAction: + if boxed { + buffer.appendInt32(-1584933265) + } + break + case .sendMessageTypingAction: + if boxed { + buffer.appendInt32(381645902) + } + break + case .sendMessageUploadAudioAction: + if boxed { + buffer.appendInt32(-424899985) + } + break + case .sendMessageUploadDocumentAction: + if boxed { + buffer.appendInt32(-1884362354) + } + break + case .sendMessageUploadPhotoAction: + if boxed { + buffer.appendInt32(-1727382502) + } + break + case .sendMessageUploadRoundAction: + if boxed { + buffer.appendInt32(-1150187996) + } + break + case .sendMessageUploadVideoAction: + if boxed { + buffer.appendInt32(-1845219337) + } + break + } + } + + fileprivate static func parse_sendMessageCancelAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi144.SendMessageAction.sendMessageCancelAction + } + fileprivate static func parse_sendMessageChooseContactAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi144.SendMessageAction.sendMessageChooseContactAction + } + fileprivate static func parse_sendMessageGeoLocationAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi144.SendMessageAction.sendMessageGeoLocationAction + } + fileprivate static func parse_sendMessageRecordAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi144.SendMessageAction.sendMessageRecordAudioAction + } + fileprivate static func parse_sendMessageRecordRoundAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi144.SendMessageAction.sendMessageRecordRoundAction + } + fileprivate static func parse_sendMessageRecordVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi144.SendMessageAction.sendMessageRecordVideoAction + } + fileprivate static func parse_sendMessageTypingAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi144.SendMessageAction.sendMessageTypingAction + } + fileprivate static func parse_sendMessageUploadAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi144.SendMessageAction.sendMessageUploadAudioAction + } + fileprivate static func parse_sendMessageUploadDocumentAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi144.SendMessageAction.sendMessageUploadDocumentAction + } + fileprivate static func parse_sendMessageUploadPhotoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi144.SendMessageAction.sendMessageUploadPhotoAction + } + fileprivate static func parse_sendMessageUploadRoundAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi144.SendMessageAction.sendMessageUploadRoundAction + } + fileprivate static func parse_sendMessageUploadVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi144.SendMessageAction.sendMessageUploadVideoAction + } } } diff --git a/submodules/TelegramApi/Sources/SecretApiLayer17.swift b/submodules/TelegramApi/Sources/SecretApiLayer17.swift new file mode 100644 index 0000000000..b198ac471f --- /dev/null +++ b/submodules/TelegramApi/Sources/SecretApiLayer17.swift @@ -0,0 +1,760 @@ + +fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { + var dict: [Int32 : (BufferReader) -> Any?] = [:] + dict[-1471112230] = { return $0.readInt32() } + dict[570911930] = { return $0.readInt64() } + dict[571523412] = { return $0.readDouble() } + dict[-1255641564] = { return parseString($0) } + dict[-1132882121] = { return SecretApi17.Bool.parse_boolFalse($0) } + dict[-1720552011] = { return SecretApi17.Bool.parse_boolTrue($0) } + dict[541931640] = { return SecretApi17.DecryptedMessage.parse_decryptedMessage($0) } + dict[1930838368] = { return SecretApi17.DecryptedMessage.parse_decryptedMessageService($0) } + dict[1700872964] = { return SecretApi17.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } + dict[1729750108] = { return SecretApi17.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } + dict[-217806717] = { return SecretApi17.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } + dict[206520510] = { return SecretApi17.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } + dict[1360072880] = { return SecretApi17.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } + dict[-1967000459] = { return SecretApi17.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } + dict[-1586283796] = { return SecretApi17.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } + dict[-860719551] = { return SecretApi17.DecryptedMessageAction.parse_decryptedMessageActionTyping($0) } + dict[467867529] = { return SecretApi17.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } + dict[1474341323] = { return SecretApi17.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } + dict[1485441687] = { return SecretApi17.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } + dict[-1332395189] = { return SecretApi17.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } + dict[144661578] = { return SecretApi17.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } + dict[893913689] = { return SecretApi17.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } + dict[846826124] = { return SecretApi17.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } + dict[1380598109] = { return SecretApi17.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } + dict[-44119819] = { return SecretApi17.SendMessageAction.parse_sendMessageCancelAction($0) } + dict[1653390447] = { return SecretApi17.SendMessageAction.parse_sendMessageChooseContactAction($0) } + dict[393186209] = { return SecretApi17.SendMessageAction.parse_sendMessageGeoLocationAction($0) } + dict[-718310409] = { return SecretApi17.SendMessageAction.parse_sendMessageRecordAudioAction($0) } + dict[-1584933265] = { return SecretApi17.SendMessageAction.parse_sendMessageRecordVideoAction($0) } + dict[381645902] = { return SecretApi17.SendMessageAction.parse_sendMessageTypingAction($0) } + dict[-424899985] = { return SecretApi17.SendMessageAction.parse_sendMessageUploadAudioAction($0) } + dict[-1884362354] = { return SecretApi17.SendMessageAction.parse_sendMessageUploadDocumentAction($0) } + dict[-1727382502] = { return SecretApi17.SendMessageAction.parse_sendMessageUploadPhotoAction($0) } + dict[-1845219337] = { return SecretApi17.SendMessageAction.parse_sendMessageUploadVideoAction($0) } + return dict +}() + +public struct SecretApi17 { + public static func parse(_ buffer: Buffer) -> Any? { + let reader = BufferReader(buffer) + if let signature = reader.readInt32() { + return parse(reader, signature: signature) + } + return nil + } + + fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { + if let parser = parsers[signature] { + return parser(reader) + } + else { + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + return nil + } + } + + fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { + if let count = reader.readInt32() { + var array = [T]() + var i: Int32 = 0 + while i < count { + var signature = elementSignature + if elementSignature == 0 { + if let unboxedSignature = reader.readInt32() { + signature = unboxedSignature + } + else { + return nil + } + } + if let item = SecretApi17.parse(reader, signature: signature) as? T { + array.append(item) + } + else { + return nil + } + i += 1 + } + return array + } + return nil + } + + public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) { + switch object { + case let _1 as SecretApi17.Bool: + _1.serialize(buffer, boxed) + case let _1 as SecretApi17.DecryptedMessage: + _1.serialize(buffer, boxed) + case let _1 as SecretApi17.DecryptedMessageAction: + _1.serialize(buffer, boxed) + case let _1 as SecretApi17.DecryptedMessageLayer: + _1.serialize(buffer, boxed) + case let _1 as SecretApi17.DecryptedMessageMedia: + _1.serialize(buffer, boxed) + case let _1 as SecretApi17.SendMessageAction: + _1.serialize(buffer, boxed) + default: + break + } + } + + public enum Bool { + case boolFalse + case boolTrue + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boolFalse: + if boxed { + buffer.appendInt32(-1132882121) + } + break + case .boolTrue: + if boxed { + buffer.appendInt32(-1720552011) + } + break + } + } + + fileprivate static func parse_boolFalse(_ reader: BufferReader) -> Bool? { + return SecretApi17.Bool.boolFalse + } + fileprivate static func parse_boolTrue(_ reader: BufferReader) -> Bool? { + return SecretApi17.Bool.boolTrue + } + } + + public enum DecryptedMessage { + case decryptedMessage(randomId: Int64, ttl: Int32, message: String, media: SecretApi17.DecryptedMessageMedia) + case decryptedMessageService(randomId: Int64, action: SecretApi17.DecryptedMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessage(let randomId, let ttl, let message, let media): + if boxed { + buffer.appendInt32(541931640) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + serializeInt32(ttl, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + media.serialize(buffer, true) + break + case .decryptedMessageService(let randomId, let action): + if boxed { + buffer.appendInt32(1930838368) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: SecretApi17.DecryptedMessageMedia? + if let signature = reader.readInt32() { + _4 = SecretApi17.parse(reader, signature: signature) as? SecretApi17.DecryptedMessageMedia + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi17.DecryptedMessage.decryptedMessage(randomId: _1!, ttl: _2!, message: _3!, media: _4!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: SecretApi17.DecryptedMessageAction? + if let signature = reader.readInt32() { + _2 = SecretApi17.parse(reader, signature: signature) as? SecretApi17.DecryptedMessageAction + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi17.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageAction { + case decryptedMessageActionDeleteMessages(randomIds: [Int64]) + case decryptedMessageActionFlushHistory + case decryptedMessageActionNotifyLayer(layer: Int32) + case decryptedMessageActionReadMessages(randomIds: [Int64]) + case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) + case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) + case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) + case decryptedMessageActionTyping(action: SecretApi17.SendMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageActionDeleteMessages(let randomIds): + if boxed { + buffer.appendInt32(1700872964) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionFlushHistory: + if boxed { + buffer.appendInt32(1729750108) + } + break + case .decryptedMessageActionNotifyLayer(let layer): + if boxed { + buffer.appendInt32(-217806717) + } + serializeInt32(layer, buffer: buffer, boxed: false) + break + case .decryptedMessageActionReadMessages(let randomIds): + if boxed { + buffer.appendInt32(206520510) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): + if boxed { + buffer.appendInt32(1360072880) + } + serializeInt32(startSeqNo, buffer: buffer, boxed: false) + serializeInt32(endSeqNo, buffer: buffer, boxed: false) + break + case .decryptedMessageActionScreenshotMessages(let randomIds): + if boxed { + buffer.appendInt32(-1967000459) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionSetMessageTTL(let ttlSeconds): + if boxed { + buffer.appendInt32(-1586283796) + } + serializeInt32(ttlSeconds, buffer: buffer, boxed: false) + break + case .decryptedMessageActionTyping(let action): + if boxed { + buffer.appendInt32(-860719551) + } + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi17.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi17.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi17.DecryptedMessageAction.decryptedMessageActionFlushHistory + } + fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi17.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi17.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi17.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi17.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi17.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi17.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi17.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionTyping(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: SecretApi17.SendMessageAction? + if let signature = reader.readInt32() { + _1 = SecretApi17.parse(reader, signature: signature) as? SecretApi17.SendMessageAction + } + let _c1 = _1 != nil + if _c1 { + return SecretApi17.DecryptedMessageAction.decryptedMessageActionTyping(action: _1!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageLayer { + case decryptedMessageLayer(randomBytes: Buffer, layer: Int32, inSeqNo: Int32, outSeqNo: Int32, message: SecretApi17.DecryptedMessage) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): + if boxed { + buffer.appendInt32(467867529) + } + serializeBytes(randomBytes, buffer: buffer, boxed: false) + serializeInt32(layer, buffer: buffer, boxed: false) + serializeInt32(inSeqNo, buffer: buffer, boxed: false) + serializeInt32(outSeqNo, buffer: buffer, boxed: false) + message.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessageLayer(_ reader: BufferReader) -> DecryptedMessageLayer? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: SecretApi17.DecryptedMessage? + if let signature = reader.readInt32() { + _5 = SecretApi17.parse(reader, signature: signature) as? SecretApi17.DecryptedMessage + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi17.DecryptedMessageLayer.decryptedMessageLayer(randomBytes: _1!, layer: _2!, inSeqNo: _3!, outSeqNo: _4!, message: _5!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageMedia { + case decryptedMessageMediaAudio(duration: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) + case decryptedMessageMediaDocument(thumb: Buffer, thumbW: Int32, thumbH: Int32, fileName: String, mimeType: String, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaEmpty + case decryptedMessageMediaGeoPoint(lat: Double, long: Double) + case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(1474341323) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): + if boxed { + buffer.appendInt32(1485441687) + } + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + serializeInt32(userId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let fileName, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(-1332395189) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeString(fileName, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaEmpty: + if boxed { + buffer.appendInt32(144661578) + } + break + case .decryptedMessageMediaGeoPoint(let lat, let long): + if boxed { + buffer.appendInt32(893913689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv): + if boxed { + buffer.appendInt32(846826124) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv): + if boxed { + buffer.appendInt32(1380598109) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + var _4: Buffer? + _4 = parseBytes(reader) + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi17.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaContact(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi17.DecryptedMessageMedia.decryptedMessageMediaContact(phoneNumber: _1!, firstName: _2!, lastName: _3!, userId: _4!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: Int32? + _6 = reader.readInt32() + var _7: Buffer? + _7 = parseBytes(reader) + var _8: Buffer? + _8 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi17.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, fileName: _4!, mimeType: _5!, size: _6!, key: _7!, iv: _8!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { + return SecretApi17.DecryptedMessageMedia.decryptedMessageMediaEmpty + } + fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi17.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaPhoto(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: Buffer? + _7 = parseBytes(reader) + var _8: Buffer? + _8 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi17.DecryptedMessageMedia.decryptedMessageMediaPhoto(thumb: _1!, thumbW: _2!, thumbH: _3!, w: _4!, h: _5!, size: _6!, key: _7!, iv: _8!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaVideo(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: String? + _5 = parseString(reader) + var _6: Int32? + _6 = reader.readInt32() + var _7: Int32? + _7 = reader.readInt32() + var _8: Int32? + _8 = reader.readInt32() + var _9: Buffer? + _9 = parseBytes(reader) + var _10: Buffer? + _10 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + let _c10 = _10 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { + return SecretApi17.DecryptedMessageMedia.decryptedMessageMediaVideo(thumb: _1!, thumbW: _2!, thumbH: _3!, duration: _4!, mimeType: _5!, w: _6!, h: _7!, size: _8!, key: _9!, iv: _10!) + } + else { + return nil + } + } + } + + public enum SendMessageAction { + case sendMessageCancelAction + case sendMessageChooseContactAction + case sendMessageGeoLocationAction + case sendMessageRecordAudioAction + case sendMessageRecordVideoAction + case sendMessageTypingAction + case sendMessageUploadAudioAction + case sendMessageUploadDocumentAction + case sendMessageUploadPhotoAction + case sendMessageUploadVideoAction + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sendMessageCancelAction: + if boxed { + buffer.appendInt32(-44119819) + } + break + case .sendMessageChooseContactAction: + if boxed { + buffer.appendInt32(1653390447) + } + break + case .sendMessageGeoLocationAction: + if boxed { + buffer.appendInt32(393186209) + } + break + case .sendMessageRecordAudioAction: + if boxed { + buffer.appendInt32(-718310409) + } + break + case .sendMessageRecordVideoAction: + if boxed { + buffer.appendInt32(-1584933265) + } + break + case .sendMessageTypingAction: + if boxed { + buffer.appendInt32(381645902) + } + break + case .sendMessageUploadAudioAction: + if boxed { + buffer.appendInt32(-424899985) + } + break + case .sendMessageUploadDocumentAction: + if boxed { + buffer.appendInt32(-1884362354) + } + break + case .sendMessageUploadPhotoAction: + if boxed { + buffer.appendInt32(-1727382502) + } + break + case .sendMessageUploadVideoAction: + if boxed { + buffer.appendInt32(-1845219337) + } + break + } + } + + fileprivate static func parse_sendMessageCancelAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi17.SendMessageAction.sendMessageCancelAction + } + fileprivate static func parse_sendMessageChooseContactAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi17.SendMessageAction.sendMessageChooseContactAction + } + fileprivate static func parse_sendMessageGeoLocationAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi17.SendMessageAction.sendMessageGeoLocationAction + } + fileprivate static func parse_sendMessageRecordAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi17.SendMessageAction.sendMessageRecordAudioAction + } + fileprivate static func parse_sendMessageRecordVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi17.SendMessageAction.sendMessageRecordVideoAction + } + fileprivate static func parse_sendMessageTypingAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi17.SendMessageAction.sendMessageTypingAction + } + fileprivate static func parse_sendMessageUploadAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi17.SendMessageAction.sendMessageUploadAudioAction + } + fileprivate static func parse_sendMessageUploadDocumentAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi17.SendMessageAction.sendMessageUploadDocumentAction + } + fileprivate static func parse_sendMessageUploadPhotoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi17.SendMessageAction.sendMessageUploadPhotoAction + } + fileprivate static func parse_sendMessageUploadVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi17.SendMessageAction.sendMessageUploadVideoAction + } + } + +} diff --git a/submodules/TelegramApi/Sources/SecretApiLayer20.swift b/submodules/TelegramApi/Sources/SecretApiLayer20.swift new file mode 100644 index 0000000000..a77d3f85d0 --- /dev/null +++ b/submodules/TelegramApi/Sources/SecretApiLayer20.swift @@ -0,0 +1,862 @@ + +fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { + var dict: [Int32 : (BufferReader) -> Any?] = [:] + dict[-1471112230] = { return $0.readInt32() } + dict[570911930] = { return $0.readInt64() } + dict[571523412] = { return $0.readDouble() } + dict[-1255641564] = { return parseString($0) } + dict[-1132882121] = { return SecretApi20.Bool.parse_boolFalse($0) } + dict[-1720552011] = { return SecretApi20.Bool.parse_boolTrue($0) } + dict[541931640] = { return SecretApi20.DecryptedMessage.parse_decryptedMessage($0) } + dict[1930838368] = { return SecretApi20.DecryptedMessage.parse_decryptedMessageService($0) } + dict[-586814357] = { return SecretApi20.DecryptedMessageAction.parse_decryptedMessageActionAbortKey($0) } + dict[1877046107] = { return SecretApi20.DecryptedMessageAction.parse_decryptedMessageActionAcceptKey($0) } + dict[-332526693] = { return SecretApi20.DecryptedMessageAction.parse_decryptedMessageActionCommitKey($0) } + dict[1700872964] = { return SecretApi20.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } + dict[1729750108] = { return SecretApi20.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } + dict[-1473258141] = { return SecretApi20.DecryptedMessageAction.parse_decryptedMessageActionNoop($0) } + dict[-217806717] = { return SecretApi20.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } + dict[206520510] = { return SecretApi20.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } + dict[-204906213] = { return SecretApi20.DecryptedMessageAction.parse_decryptedMessageActionRequestKey($0) } + dict[1360072880] = { return SecretApi20.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } + dict[-1967000459] = { return SecretApi20.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } + dict[-1586283796] = { return SecretApi20.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } + dict[-860719551] = { return SecretApi20.DecryptedMessageAction.parse_decryptedMessageActionTyping($0) } + dict[467867529] = { return SecretApi20.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } + dict[1474341323] = { return SecretApi20.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } + dict[1485441687] = { return SecretApi20.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } + dict[-1332395189] = { return SecretApi20.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } + dict[144661578] = { return SecretApi20.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } + dict[893913689] = { return SecretApi20.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } + dict[846826124] = { return SecretApi20.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } + dict[1380598109] = { return SecretApi20.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } + dict[-44119819] = { return SecretApi20.SendMessageAction.parse_sendMessageCancelAction($0) } + dict[1653390447] = { return SecretApi20.SendMessageAction.parse_sendMessageChooseContactAction($0) } + dict[393186209] = { return SecretApi20.SendMessageAction.parse_sendMessageGeoLocationAction($0) } + dict[-718310409] = { return SecretApi20.SendMessageAction.parse_sendMessageRecordAudioAction($0) } + dict[-1584933265] = { return SecretApi20.SendMessageAction.parse_sendMessageRecordVideoAction($0) } + dict[381645902] = { return SecretApi20.SendMessageAction.parse_sendMessageTypingAction($0) } + dict[-424899985] = { return SecretApi20.SendMessageAction.parse_sendMessageUploadAudioAction($0) } + dict[-1884362354] = { return SecretApi20.SendMessageAction.parse_sendMessageUploadDocumentAction($0) } + dict[-1727382502] = { return SecretApi20.SendMessageAction.parse_sendMessageUploadPhotoAction($0) } + dict[-1845219337] = { return SecretApi20.SendMessageAction.parse_sendMessageUploadVideoAction($0) } + return dict +}() + +public struct SecretApi20 { + public static func parse(_ buffer: Buffer) -> Any? { + let reader = BufferReader(buffer) + if let signature = reader.readInt32() { + return parse(reader, signature: signature) + } + return nil + } + + fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { + if let parser = parsers[signature] { + return parser(reader) + } + else { + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + return nil + } + } + + fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { + if let count = reader.readInt32() { + var array = [T]() + var i: Int32 = 0 + while i < count { + var signature = elementSignature + if elementSignature == 0 { + if let unboxedSignature = reader.readInt32() { + signature = unboxedSignature + } + else { + return nil + } + } + if let item = SecretApi20.parse(reader, signature: signature) as? T { + array.append(item) + } + else { + return nil + } + i += 1 + } + return array + } + return nil + } + + public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) { + switch object { + case let _1 as SecretApi20.Bool: + _1.serialize(buffer, boxed) + case let _1 as SecretApi20.DecryptedMessage: + _1.serialize(buffer, boxed) + case let _1 as SecretApi20.DecryptedMessageAction: + _1.serialize(buffer, boxed) + case let _1 as SecretApi20.DecryptedMessageLayer: + _1.serialize(buffer, boxed) + case let _1 as SecretApi20.DecryptedMessageMedia: + _1.serialize(buffer, boxed) + case let _1 as SecretApi20.SendMessageAction: + _1.serialize(buffer, boxed) + default: + break + } + } + + public enum Bool { + case boolFalse + case boolTrue + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boolFalse: + if boxed { + buffer.appendInt32(-1132882121) + } + break + case .boolTrue: + if boxed { + buffer.appendInt32(-1720552011) + } + break + } + } + + fileprivate static func parse_boolFalse(_ reader: BufferReader) -> Bool? { + return SecretApi20.Bool.boolFalse + } + fileprivate static func parse_boolTrue(_ reader: BufferReader) -> Bool? { + return SecretApi20.Bool.boolTrue + } + } + + public enum DecryptedMessage { + case decryptedMessage(randomId: Int64, ttl: Int32, message: String, media: SecretApi20.DecryptedMessageMedia) + case decryptedMessageService(randomId: Int64, action: SecretApi20.DecryptedMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessage(let randomId, let ttl, let message, let media): + if boxed { + buffer.appendInt32(541931640) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + serializeInt32(ttl, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + media.serialize(buffer, true) + break + case .decryptedMessageService(let randomId, let action): + if boxed { + buffer.appendInt32(1930838368) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: SecretApi20.DecryptedMessageMedia? + if let signature = reader.readInt32() { + _4 = SecretApi20.parse(reader, signature: signature) as? SecretApi20.DecryptedMessageMedia + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi20.DecryptedMessage.decryptedMessage(randomId: _1!, ttl: _2!, message: _3!, media: _4!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: SecretApi20.DecryptedMessageAction? + if let signature = reader.readInt32() { + _2 = SecretApi20.parse(reader, signature: signature) as? SecretApi20.DecryptedMessageAction + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi20.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageAction { + case decryptedMessageActionAbortKey(exchangeId: Int64) + case decryptedMessageActionAcceptKey(exchangeId: Int64, gB: Buffer, keyFingerprint: Int64) + case decryptedMessageActionCommitKey(exchangeId: Int64, keyFingerprint: Int64) + case decryptedMessageActionDeleteMessages(randomIds: [Int64]) + case decryptedMessageActionFlushHistory + case decryptedMessageActionNoop + case decryptedMessageActionNotifyLayer(layer: Int32) + case decryptedMessageActionReadMessages(randomIds: [Int64]) + case decryptedMessageActionRequestKey(exchangeId: Int64, gA: Buffer) + case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) + case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) + case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) + case decryptedMessageActionTyping(action: SecretApi20.SendMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageActionAbortKey(let exchangeId): + if boxed { + buffer.appendInt32(-586814357) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + break + case .decryptedMessageActionAcceptKey(let exchangeId, let gB, let keyFingerprint): + if boxed { + buffer.appendInt32(1877046107) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gB, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionCommitKey(let exchangeId, let keyFingerprint): + if boxed { + buffer.appendInt32(-332526693) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionDeleteMessages(let randomIds): + if boxed { + buffer.appendInt32(1700872964) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionFlushHistory: + if boxed { + buffer.appendInt32(1729750108) + } + break + case .decryptedMessageActionNoop: + if boxed { + buffer.appendInt32(-1473258141) + } + break + case .decryptedMessageActionNotifyLayer(let layer): + if boxed { + buffer.appendInt32(-217806717) + } + serializeInt32(layer, buffer: buffer, boxed: false) + break + case .decryptedMessageActionReadMessages(let randomIds): + if boxed { + buffer.appendInt32(206520510) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionRequestKey(let exchangeId, let gA): + if boxed { + buffer.appendInt32(-204906213) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gA, buffer: buffer, boxed: false) + break + case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): + if boxed { + buffer.appendInt32(1360072880) + } + serializeInt32(startSeqNo, buffer: buffer, boxed: false) + serializeInt32(endSeqNo, buffer: buffer, boxed: false) + break + case .decryptedMessageActionScreenshotMessages(let randomIds): + if boxed { + buffer.appendInt32(-1967000459) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionSetMessageTTL(let ttlSeconds): + if boxed { + buffer.appendInt32(-1586283796) + } + serializeInt32(ttlSeconds, buffer: buffer, boxed: false) + break + case .decryptedMessageActionTyping(let action): + if boxed { + buffer.appendInt32(-860719551) + } + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessageActionAbortKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return SecretApi20.DecryptedMessageAction.decryptedMessageActionAbortKey(exchangeId: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionAcceptKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi20.DecryptedMessageAction.decryptedMessageActionAcceptKey(exchangeId: _1!, gB: _2!, keyFingerprint: _3!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionCommitKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi20.DecryptedMessageAction.decryptedMessageActionCommitKey(exchangeId: _1!, keyFingerprint: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi20.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi20.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi20.DecryptedMessageAction.decryptedMessageActionFlushHistory + } + fileprivate static func parse_decryptedMessageActionNoop(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi20.DecryptedMessageAction.decryptedMessageActionNoop + } + fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi20.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi20.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi20.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionRequestKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi20.DecryptedMessageAction.decryptedMessageActionRequestKey(exchangeId: _1!, gA: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi20.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi20.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi20.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi20.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionTyping(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: SecretApi20.SendMessageAction? + if let signature = reader.readInt32() { + _1 = SecretApi20.parse(reader, signature: signature) as? SecretApi20.SendMessageAction + } + let _c1 = _1 != nil + if _c1 { + return SecretApi20.DecryptedMessageAction.decryptedMessageActionTyping(action: _1!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageLayer { + case decryptedMessageLayer(randomBytes: Buffer, layer: Int32, inSeqNo: Int32, outSeqNo: Int32, message: SecretApi20.DecryptedMessage) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): + if boxed { + buffer.appendInt32(467867529) + } + serializeBytes(randomBytes, buffer: buffer, boxed: false) + serializeInt32(layer, buffer: buffer, boxed: false) + serializeInt32(inSeqNo, buffer: buffer, boxed: false) + serializeInt32(outSeqNo, buffer: buffer, boxed: false) + message.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessageLayer(_ reader: BufferReader) -> DecryptedMessageLayer? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: SecretApi20.DecryptedMessage? + if let signature = reader.readInt32() { + _5 = SecretApi20.parse(reader, signature: signature) as? SecretApi20.DecryptedMessage + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi20.DecryptedMessageLayer.decryptedMessageLayer(randomBytes: _1!, layer: _2!, inSeqNo: _3!, outSeqNo: _4!, message: _5!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageMedia { + case decryptedMessageMediaAudio(duration: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) + case decryptedMessageMediaDocument(thumb: Buffer, thumbW: Int32, thumbH: Int32, fileName: String, mimeType: String, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaEmpty + case decryptedMessageMediaGeoPoint(lat: Double, long: Double) + case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(1474341323) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): + if boxed { + buffer.appendInt32(1485441687) + } + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + serializeInt32(userId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let fileName, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(-1332395189) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeString(fileName, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaEmpty: + if boxed { + buffer.appendInt32(144661578) + } + break + case .decryptedMessageMediaGeoPoint(let lat, let long): + if boxed { + buffer.appendInt32(893913689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv): + if boxed { + buffer.appendInt32(846826124) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv): + if boxed { + buffer.appendInt32(1380598109) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + var _4: Buffer? + _4 = parseBytes(reader) + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi20.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaContact(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi20.DecryptedMessageMedia.decryptedMessageMediaContact(phoneNumber: _1!, firstName: _2!, lastName: _3!, userId: _4!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: Int32? + _6 = reader.readInt32() + var _7: Buffer? + _7 = parseBytes(reader) + var _8: Buffer? + _8 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi20.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, fileName: _4!, mimeType: _5!, size: _6!, key: _7!, iv: _8!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { + return SecretApi20.DecryptedMessageMedia.decryptedMessageMediaEmpty + } + fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi20.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaPhoto(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: Buffer? + _7 = parseBytes(reader) + var _8: Buffer? + _8 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi20.DecryptedMessageMedia.decryptedMessageMediaPhoto(thumb: _1!, thumbW: _2!, thumbH: _3!, w: _4!, h: _5!, size: _6!, key: _7!, iv: _8!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaVideo(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: String? + _5 = parseString(reader) + var _6: Int32? + _6 = reader.readInt32() + var _7: Int32? + _7 = reader.readInt32() + var _8: Int32? + _8 = reader.readInt32() + var _9: Buffer? + _9 = parseBytes(reader) + var _10: Buffer? + _10 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + let _c10 = _10 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { + return SecretApi20.DecryptedMessageMedia.decryptedMessageMediaVideo(thumb: _1!, thumbW: _2!, thumbH: _3!, duration: _4!, mimeType: _5!, w: _6!, h: _7!, size: _8!, key: _9!, iv: _10!) + } + else { + return nil + } + } + } + + public enum SendMessageAction { + case sendMessageCancelAction + case sendMessageChooseContactAction + case sendMessageGeoLocationAction + case sendMessageRecordAudioAction + case sendMessageRecordVideoAction + case sendMessageTypingAction + case sendMessageUploadAudioAction + case sendMessageUploadDocumentAction + case sendMessageUploadPhotoAction + case sendMessageUploadVideoAction + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sendMessageCancelAction: + if boxed { + buffer.appendInt32(-44119819) + } + break + case .sendMessageChooseContactAction: + if boxed { + buffer.appendInt32(1653390447) + } + break + case .sendMessageGeoLocationAction: + if boxed { + buffer.appendInt32(393186209) + } + break + case .sendMessageRecordAudioAction: + if boxed { + buffer.appendInt32(-718310409) + } + break + case .sendMessageRecordVideoAction: + if boxed { + buffer.appendInt32(-1584933265) + } + break + case .sendMessageTypingAction: + if boxed { + buffer.appendInt32(381645902) + } + break + case .sendMessageUploadAudioAction: + if boxed { + buffer.appendInt32(-424899985) + } + break + case .sendMessageUploadDocumentAction: + if boxed { + buffer.appendInt32(-1884362354) + } + break + case .sendMessageUploadPhotoAction: + if boxed { + buffer.appendInt32(-1727382502) + } + break + case .sendMessageUploadVideoAction: + if boxed { + buffer.appendInt32(-1845219337) + } + break + } + } + + fileprivate static func parse_sendMessageCancelAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi20.SendMessageAction.sendMessageCancelAction + } + fileprivate static func parse_sendMessageChooseContactAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi20.SendMessageAction.sendMessageChooseContactAction + } + fileprivate static func parse_sendMessageGeoLocationAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi20.SendMessageAction.sendMessageGeoLocationAction + } + fileprivate static func parse_sendMessageRecordAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi20.SendMessageAction.sendMessageRecordAudioAction + } + fileprivate static func parse_sendMessageRecordVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi20.SendMessageAction.sendMessageRecordVideoAction + } + fileprivate static func parse_sendMessageTypingAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi20.SendMessageAction.sendMessageTypingAction + } + fileprivate static func parse_sendMessageUploadAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi20.SendMessageAction.sendMessageUploadAudioAction + } + fileprivate static func parse_sendMessageUploadDocumentAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi20.SendMessageAction.sendMessageUploadDocumentAction + } + fileprivate static func parse_sendMessageUploadPhotoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi20.SendMessageAction.sendMessageUploadPhotoAction + } + fileprivate static func parse_sendMessageUploadVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi20.SendMessageAction.sendMessageUploadVideoAction + } + } + +} diff --git a/submodules/TelegramApi/Sources/SecretApiLayer23.swift b/submodules/TelegramApi/Sources/SecretApiLayer23.swift new file mode 100644 index 0000000000..c8c876e439 --- /dev/null +++ b/submodules/TelegramApi/Sources/SecretApiLayer23.swift @@ -0,0 +1,1209 @@ + +fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { + var dict: [Int32 : (BufferReader) -> Any?] = [:] + dict[-1471112230] = { return $0.readInt32() } + dict[570911930] = { return $0.readInt64() } + dict[571523412] = { return $0.readDouble() } + dict[-1255641564] = { return parseString($0) } + dict[-1132882121] = { return SecretApi23.Bool.parse_boolFalse($0) } + dict[-1720552011] = { return SecretApi23.Bool.parse_boolTrue($0) } + dict[541931640] = { return SecretApi23.DecryptedMessage.parse_decryptedMessage($0) } + dict[1930838368] = { return SecretApi23.DecryptedMessage.parse_decryptedMessageService($0) } + dict[-586814357] = { return SecretApi23.DecryptedMessageAction.parse_decryptedMessageActionAbortKey($0) } + dict[1877046107] = { return SecretApi23.DecryptedMessageAction.parse_decryptedMessageActionAcceptKey($0) } + dict[-332526693] = { return SecretApi23.DecryptedMessageAction.parse_decryptedMessageActionCommitKey($0) } + dict[1700872964] = { return SecretApi23.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } + dict[1729750108] = { return SecretApi23.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } + dict[-1473258141] = { return SecretApi23.DecryptedMessageAction.parse_decryptedMessageActionNoop($0) } + dict[-217806717] = { return SecretApi23.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } + dict[206520510] = { return SecretApi23.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } + dict[-204906213] = { return SecretApi23.DecryptedMessageAction.parse_decryptedMessageActionRequestKey($0) } + dict[1360072880] = { return SecretApi23.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } + dict[-1967000459] = { return SecretApi23.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } + dict[-1586283796] = { return SecretApi23.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } + dict[-860719551] = { return SecretApi23.DecryptedMessageAction.parse_decryptedMessageActionTyping($0) } + dict[467867529] = { return SecretApi23.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } + dict[1474341323] = { return SecretApi23.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } + dict[1485441687] = { return SecretApi23.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } + dict[-1332395189] = { return SecretApi23.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } + dict[144661578] = { return SecretApi23.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } + dict[-90853155] = { return SecretApi23.DecryptedMessageMedia.parse_decryptedMessageMediaExternalDocument($0) } + dict[893913689] = { return SecretApi23.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } + dict[846826124] = { return SecretApi23.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } + dict[1380598109] = { return SecretApi23.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } + dict[297109817] = { return SecretApi23.DocumentAttribute.parse_documentAttributeAnimated($0) } + dict[85215461] = { return SecretApi23.DocumentAttribute.parse_documentAttributeAudio($0) } + dict[358154344] = { return SecretApi23.DocumentAttribute.parse_documentAttributeFilename($0) } + dict[1815593308] = { return SecretApi23.DocumentAttribute.parse_documentAttributeImageSize($0) } + dict[-83208409] = { return SecretApi23.DocumentAttribute.parse_documentAttributeSticker($0) } + dict[1494273227] = { return SecretApi23.DocumentAttribute.parse_documentAttributeVideo($0) } + dict[1406570614] = { return SecretApi23.FileLocation.parse_fileLocation($0) } + dict[2086234950] = { return SecretApi23.FileLocation.parse_fileLocationUnavailable($0) } + dict[-374917894] = { return SecretApi23.PhotoSize.parse_photoCachedSize($0) } + dict[2009052699] = { return SecretApi23.PhotoSize.parse_photoSize($0) } + dict[236446268] = { return SecretApi23.PhotoSize.parse_photoSizeEmpty($0) } + dict[-44119819] = { return SecretApi23.SendMessageAction.parse_sendMessageCancelAction($0) } + dict[1653390447] = { return SecretApi23.SendMessageAction.parse_sendMessageChooseContactAction($0) } + dict[393186209] = { return SecretApi23.SendMessageAction.parse_sendMessageGeoLocationAction($0) } + dict[-718310409] = { return SecretApi23.SendMessageAction.parse_sendMessageRecordAudioAction($0) } + dict[-1584933265] = { return SecretApi23.SendMessageAction.parse_sendMessageRecordVideoAction($0) } + dict[381645902] = { return SecretApi23.SendMessageAction.parse_sendMessageTypingAction($0) } + dict[-424899985] = { return SecretApi23.SendMessageAction.parse_sendMessageUploadAudioAction($0) } + dict[-1884362354] = { return SecretApi23.SendMessageAction.parse_sendMessageUploadDocumentAction($0) } + dict[-1727382502] = { return SecretApi23.SendMessageAction.parse_sendMessageUploadPhotoAction($0) } + dict[-1845219337] = { return SecretApi23.SendMessageAction.parse_sendMessageUploadVideoAction($0) } + return dict +}() + +public struct SecretApi23 { + public static func parse(_ buffer: Buffer) -> Any? { + let reader = BufferReader(buffer) + if let signature = reader.readInt32() { + return parse(reader, signature: signature) + } + return nil + } + + fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { + if let parser = parsers[signature] { + return parser(reader) + } + else { + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + return nil + } + } + + fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { + if let count = reader.readInt32() { + var array = [T]() + var i: Int32 = 0 + while i < count { + var signature = elementSignature + if elementSignature == 0 { + if let unboxedSignature = reader.readInt32() { + signature = unboxedSignature + } + else { + return nil + } + } + if let item = SecretApi23.parse(reader, signature: signature) as? T { + array.append(item) + } + else { + return nil + } + i += 1 + } + return array + } + return nil + } + + public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) { + switch object { + case let _1 as SecretApi23.Bool: + _1.serialize(buffer, boxed) + case let _1 as SecretApi23.DecryptedMessage: + _1.serialize(buffer, boxed) + case let _1 as SecretApi23.DecryptedMessageAction: + _1.serialize(buffer, boxed) + case let _1 as SecretApi23.DecryptedMessageLayer: + _1.serialize(buffer, boxed) + case let _1 as SecretApi23.DecryptedMessageMedia: + _1.serialize(buffer, boxed) + case let _1 as SecretApi23.DocumentAttribute: + _1.serialize(buffer, boxed) + case let _1 as SecretApi23.FileLocation: + _1.serialize(buffer, boxed) + case let _1 as SecretApi23.PhotoSize: + _1.serialize(buffer, boxed) + case let _1 as SecretApi23.SendMessageAction: + _1.serialize(buffer, boxed) + default: + break + } + } + + public enum Bool { + case boolFalse + case boolTrue + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boolFalse: + if boxed { + buffer.appendInt32(-1132882121) + } + break + case .boolTrue: + if boxed { + buffer.appendInt32(-1720552011) + } + break + } + } + + fileprivate static func parse_boolFalse(_ reader: BufferReader) -> Bool? { + return SecretApi23.Bool.boolFalse + } + fileprivate static func parse_boolTrue(_ reader: BufferReader) -> Bool? { + return SecretApi23.Bool.boolTrue + } + } + + public enum DecryptedMessage { + case decryptedMessage(randomId: Int64, ttl: Int32, message: String, media: SecretApi23.DecryptedMessageMedia) + case decryptedMessageService(randomId: Int64, action: SecretApi23.DecryptedMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessage(let randomId, let ttl, let message, let media): + if boxed { + buffer.appendInt32(541931640) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + serializeInt32(ttl, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + media.serialize(buffer, true) + break + case .decryptedMessageService(let randomId, let action): + if boxed { + buffer.appendInt32(1930838368) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + var _4: SecretApi23.DecryptedMessageMedia? + if let signature = reader.readInt32() { + _4 = SecretApi23.parse(reader, signature: signature) as? SecretApi23.DecryptedMessageMedia + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi23.DecryptedMessage.decryptedMessage(randomId: _1!, ttl: _2!, message: _3!, media: _4!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: SecretApi23.DecryptedMessageAction? + if let signature = reader.readInt32() { + _2 = SecretApi23.parse(reader, signature: signature) as? SecretApi23.DecryptedMessageAction + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi23.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageAction { + case decryptedMessageActionAbortKey(exchangeId: Int64) + case decryptedMessageActionAcceptKey(exchangeId: Int64, gB: Buffer, keyFingerprint: Int64) + case decryptedMessageActionCommitKey(exchangeId: Int64, keyFingerprint: Int64) + case decryptedMessageActionDeleteMessages(randomIds: [Int64]) + case decryptedMessageActionFlushHistory + case decryptedMessageActionNoop + case decryptedMessageActionNotifyLayer(layer: Int32) + case decryptedMessageActionReadMessages(randomIds: [Int64]) + case decryptedMessageActionRequestKey(exchangeId: Int64, gA: Buffer) + case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) + case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) + case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) + case decryptedMessageActionTyping(action: SecretApi23.SendMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageActionAbortKey(let exchangeId): + if boxed { + buffer.appendInt32(-586814357) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + break + case .decryptedMessageActionAcceptKey(let exchangeId, let gB, let keyFingerprint): + if boxed { + buffer.appendInt32(1877046107) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gB, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionCommitKey(let exchangeId, let keyFingerprint): + if boxed { + buffer.appendInt32(-332526693) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionDeleteMessages(let randomIds): + if boxed { + buffer.appendInt32(1700872964) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionFlushHistory: + if boxed { + buffer.appendInt32(1729750108) + } + break + case .decryptedMessageActionNoop: + if boxed { + buffer.appendInt32(-1473258141) + } + break + case .decryptedMessageActionNotifyLayer(let layer): + if boxed { + buffer.appendInt32(-217806717) + } + serializeInt32(layer, buffer: buffer, boxed: false) + break + case .decryptedMessageActionReadMessages(let randomIds): + if boxed { + buffer.appendInt32(206520510) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionRequestKey(let exchangeId, let gA): + if boxed { + buffer.appendInt32(-204906213) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gA, buffer: buffer, boxed: false) + break + case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): + if boxed { + buffer.appendInt32(1360072880) + } + serializeInt32(startSeqNo, buffer: buffer, boxed: false) + serializeInt32(endSeqNo, buffer: buffer, boxed: false) + break + case .decryptedMessageActionScreenshotMessages(let randomIds): + if boxed { + buffer.appendInt32(-1967000459) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionSetMessageTTL(let ttlSeconds): + if boxed { + buffer.appendInt32(-1586283796) + } + serializeInt32(ttlSeconds, buffer: buffer, boxed: false) + break + case .decryptedMessageActionTyping(let action): + if boxed { + buffer.appendInt32(-860719551) + } + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessageActionAbortKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return SecretApi23.DecryptedMessageAction.decryptedMessageActionAbortKey(exchangeId: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionAcceptKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi23.DecryptedMessageAction.decryptedMessageActionAcceptKey(exchangeId: _1!, gB: _2!, keyFingerprint: _3!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionCommitKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi23.DecryptedMessageAction.decryptedMessageActionCommitKey(exchangeId: _1!, keyFingerprint: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi23.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi23.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi23.DecryptedMessageAction.decryptedMessageActionFlushHistory + } + fileprivate static func parse_decryptedMessageActionNoop(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi23.DecryptedMessageAction.decryptedMessageActionNoop + } + fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi23.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi23.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi23.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionRequestKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi23.DecryptedMessageAction.decryptedMessageActionRequestKey(exchangeId: _1!, gA: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi23.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi23.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi23.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi23.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionTyping(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: SecretApi23.SendMessageAction? + if let signature = reader.readInt32() { + _1 = SecretApi23.parse(reader, signature: signature) as? SecretApi23.SendMessageAction + } + let _c1 = _1 != nil + if _c1 { + return SecretApi23.DecryptedMessageAction.decryptedMessageActionTyping(action: _1!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageLayer { + case decryptedMessageLayer(randomBytes: Buffer, layer: Int32, inSeqNo: Int32, outSeqNo: Int32, message: SecretApi23.DecryptedMessage) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): + if boxed { + buffer.appendInt32(467867529) + } + serializeBytes(randomBytes, buffer: buffer, boxed: false) + serializeInt32(layer, buffer: buffer, boxed: false) + serializeInt32(inSeqNo, buffer: buffer, boxed: false) + serializeInt32(outSeqNo, buffer: buffer, boxed: false) + message.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessageLayer(_ reader: BufferReader) -> DecryptedMessageLayer? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: SecretApi23.DecryptedMessage? + if let signature = reader.readInt32() { + _5 = SecretApi23.parse(reader, signature: signature) as? SecretApi23.DecryptedMessage + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi23.DecryptedMessageLayer.decryptedMessageLayer(randomBytes: _1!, layer: _2!, inSeqNo: _3!, outSeqNo: _4!, message: _5!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageMedia { + case decryptedMessageMediaAudio(duration: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) + case decryptedMessageMediaDocument(thumb: Buffer, thumbW: Int32, thumbH: Int32, fileName: String, mimeType: String, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaEmpty + case decryptedMessageMediaExternalDocument(id: Int64, accessHash: Int64, date: Int32, mimeType: String, size: Int32, thumb: SecretApi23.PhotoSize, dcId: Int32, attributes: [SecretApi23.DocumentAttribute]) + case decryptedMessageMediaGeoPoint(lat: Double, long: Double) + case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(1474341323) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): + if boxed { + buffer.appendInt32(1485441687) + } + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + serializeInt32(userId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let fileName, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(-1332395189) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeString(fileName, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaEmpty: + if boxed { + buffer.appendInt32(144661578) + } + break + case .decryptedMessageMediaExternalDocument(let id, let accessHash, let date, let mimeType, let size, let thumb, let dcId, let attributes): + if boxed { + buffer.appendInt32(-90853155) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + thumb.serialize(buffer, true) + serializeInt32(dcId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + break + case .decryptedMessageMediaGeoPoint(let lat, let long): + if boxed { + buffer.appendInt32(893913689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv): + if boxed { + buffer.appendInt32(846826124) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv): + if boxed { + buffer.appendInt32(1380598109) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + var _4: Buffer? + _4 = parseBytes(reader) + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi23.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaContact(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi23.DecryptedMessageMedia.decryptedMessageMediaContact(phoneNumber: _1!, firstName: _2!, lastName: _3!, userId: _4!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: Int32? + _6 = reader.readInt32() + var _7: Buffer? + _7 = parseBytes(reader) + var _8: Buffer? + _8 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi23.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, fileName: _4!, mimeType: _5!, size: _6!, key: _7!, iv: _8!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { + return SecretApi23.DecryptedMessageMedia.decryptedMessageMediaEmpty + } + fileprivate static func parse_decryptedMessageMediaExternalDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: Int32? + _5 = reader.readInt32() + var _6: SecretApi23.PhotoSize? + if let signature = reader.readInt32() { + _6 = SecretApi23.parse(reader, signature: signature) as? SecretApi23.PhotoSize + } + var _7: Int32? + _7 = reader.readInt32() + var _8: [SecretApi23.DocumentAttribute]? + if let _ = reader.readInt32() { + _8 = SecretApi23.parseVector(reader, elementSignature: 0, elementType: SecretApi23.DocumentAttribute.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi23.DecryptedMessageMedia.decryptedMessageMediaExternalDocument(id: _1!, accessHash: _2!, date: _3!, mimeType: _4!, size: _5!, thumb: _6!, dcId: _7!, attributes: _8!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi23.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaPhoto(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: Buffer? + _7 = parseBytes(reader) + var _8: Buffer? + _8 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi23.DecryptedMessageMedia.decryptedMessageMediaPhoto(thumb: _1!, thumbW: _2!, thumbH: _3!, w: _4!, h: _5!, size: _6!, key: _7!, iv: _8!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaVideo(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: String? + _5 = parseString(reader) + var _6: Int32? + _6 = reader.readInt32() + var _7: Int32? + _7 = reader.readInt32() + var _8: Int32? + _8 = reader.readInt32() + var _9: Buffer? + _9 = parseBytes(reader) + var _10: Buffer? + _10 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + let _c10 = _10 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { + return SecretApi23.DecryptedMessageMedia.decryptedMessageMediaVideo(thumb: _1!, thumbW: _2!, thumbH: _3!, duration: _4!, mimeType: _5!, w: _6!, h: _7!, size: _8!, key: _9!, iv: _10!) + } + else { + return nil + } + } + } + + public enum DocumentAttribute { + case documentAttributeAnimated + case documentAttributeAudio(duration: Int32) + case documentAttributeFilename(fileName: String) + case documentAttributeImageSize(w: Int32, h: Int32) + case documentAttributeSticker + case documentAttributeVideo(duration: Int32, w: Int32, h: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .documentAttributeAnimated: + if boxed { + buffer.appendInt32(297109817) + } + break + case .documentAttributeAudio(let duration): + if boxed { + buffer.appendInt32(85215461) + } + serializeInt32(duration, buffer: buffer, boxed: false) + break + case .documentAttributeFilename(let fileName): + if boxed { + buffer.appendInt32(358154344) + } + serializeString(fileName, buffer: buffer, boxed: false) + break + case .documentAttributeImageSize(let w, let h): + if boxed { + buffer.appendInt32(1815593308) + } + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + case .documentAttributeSticker: + if boxed { + buffer.appendInt32(-83208409) + } + break + case .documentAttributeVideo(let duration, let w, let h): + if boxed { + buffer.appendInt32(1494273227) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_documentAttributeAnimated(_ reader: BufferReader) -> DocumentAttribute? { + return SecretApi23.DocumentAttribute.documentAttributeAnimated + } + fileprivate static func parse_documentAttributeAudio(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi23.DocumentAttribute.documentAttributeAudio(duration: _1!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeFilename(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi23.DocumentAttribute.documentAttributeFilename(fileName: _1!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeImageSize(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi23.DocumentAttribute.documentAttributeImageSize(w: _1!, h: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeSticker(_ reader: BufferReader) -> DocumentAttribute? { + return SecretApi23.DocumentAttribute.documentAttributeSticker + } + fileprivate static func parse_documentAttributeVideo(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi23.DocumentAttribute.documentAttributeVideo(duration: _1!, w: _2!, h: _3!) + } + else { + return nil + } + } + } + + public enum FileLocation { + case fileLocation(dcId: Int32, volumeId: Int64, localId: Int32, secret: Int64) + case fileLocationUnavailable(volumeId: Int64, localId: Int32, secret: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .fileLocation(let dcId, let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(1406570614) + } + serializeInt32(dcId, buffer: buffer, boxed: false) + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + case .fileLocationUnavailable(let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(2086234950) + } + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_fileLocation(_ reader: BufferReader) -> FileLocation? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int64? + _4 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi23.FileLocation.fileLocation(dcId: _1!, volumeId: _2!, localId: _3!, secret: _4!) + } + else { + return nil + } + } + fileprivate static func parse_fileLocationUnavailable(_ reader: BufferReader) -> FileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi23.FileLocation.fileLocationUnavailable(volumeId: _1!, localId: _2!, secret: _3!) + } + else { + return nil + } + } + } + + public enum PhotoSize { + case photoCachedSize(type: String, location: SecretApi23.FileLocation, w: Int32, h: Int32, bytes: Buffer) + case photoSize(type: String, location: SecretApi23.FileLocation, w: Int32, h: Int32, size: Int32) + case photoSizeEmpty(type: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .photoCachedSize(let type, let location, let w, let h, let bytes): + if boxed { + buffer.appendInt32(-374917894) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeBytes(bytes, buffer: buffer, boxed: false) + break + case .photoSize(let type, let location, let w, let h, let size): + if boxed { + buffer.appendInt32(2009052699) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + break + case .photoSizeEmpty(let type): + if boxed { + buffer.appendInt32(236446268) + } + serializeString(type, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_photoCachedSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi23.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi23.parse(reader, signature: signature) as? SecretApi23.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi23.PhotoSize.photoCachedSize(type: _1!, location: _2!, w: _3!, h: _4!, bytes: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi23.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi23.parse(reader, signature: signature) as? SecretApi23.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi23.PhotoSize.photoSize(type: _1!, location: _2!, w: _3!, h: _4!, size: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSizeEmpty(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi23.PhotoSize.photoSizeEmpty(type: _1!) + } + else { + return nil + } + } + } + + public enum SendMessageAction { + case sendMessageCancelAction + case sendMessageChooseContactAction + case sendMessageGeoLocationAction + case sendMessageRecordAudioAction + case sendMessageRecordVideoAction + case sendMessageTypingAction + case sendMessageUploadAudioAction + case sendMessageUploadDocumentAction + case sendMessageUploadPhotoAction + case sendMessageUploadVideoAction + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sendMessageCancelAction: + if boxed { + buffer.appendInt32(-44119819) + } + break + case .sendMessageChooseContactAction: + if boxed { + buffer.appendInt32(1653390447) + } + break + case .sendMessageGeoLocationAction: + if boxed { + buffer.appendInt32(393186209) + } + break + case .sendMessageRecordAudioAction: + if boxed { + buffer.appendInt32(-718310409) + } + break + case .sendMessageRecordVideoAction: + if boxed { + buffer.appendInt32(-1584933265) + } + break + case .sendMessageTypingAction: + if boxed { + buffer.appendInt32(381645902) + } + break + case .sendMessageUploadAudioAction: + if boxed { + buffer.appendInt32(-424899985) + } + break + case .sendMessageUploadDocumentAction: + if boxed { + buffer.appendInt32(-1884362354) + } + break + case .sendMessageUploadPhotoAction: + if boxed { + buffer.appendInt32(-1727382502) + } + break + case .sendMessageUploadVideoAction: + if boxed { + buffer.appendInt32(-1845219337) + } + break + } + } + + fileprivate static func parse_sendMessageCancelAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi23.SendMessageAction.sendMessageCancelAction + } + fileprivate static func parse_sendMessageChooseContactAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi23.SendMessageAction.sendMessageChooseContactAction + } + fileprivate static func parse_sendMessageGeoLocationAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi23.SendMessageAction.sendMessageGeoLocationAction + } + fileprivate static func parse_sendMessageRecordAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi23.SendMessageAction.sendMessageRecordAudioAction + } + fileprivate static func parse_sendMessageRecordVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi23.SendMessageAction.sendMessageRecordVideoAction + } + fileprivate static func parse_sendMessageTypingAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi23.SendMessageAction.sendMessageTypingAction + } + fileprivate static func parse_sendMessageUploadAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi23.SendMessageAction.sendMessageUploadAudioAction + } + fileprivate static func parse_sendMessageUploadDocumentAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi23.SendMessageAction.sendMessageUploadDocumentAction + } + fileprivate static func parse_sendMessageUploadPhotoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi23.SendMessageAction.sendMessageUploadPhotoAction + } + fileprivate static func parse_sendMessageUploadVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi23.SendMessageAction.sendMessageUploadVideoAction + } + } + +} diff --git a/submodules/TelegramApi/Sources/SecretApiLayer45.swift b/submodules/TelegramApi/Sources/SecretApiLayer45.swift new file mode 100644 index 0000000000..032b2d04fc --- /dev/null +++ b/submodules/TelegramApi/Sources/SecretApiLayer45.swift @@ -0,0 +1,1658 @@ + +fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { + var dict: [Int32 : (BufferReader) -> Any?] = [:] + dict[-1471112230] = { return $0.readInt32() } + dict[570911930] = { return $0.readInt64() } + dict[571523412] = { return $0.readDouble() } + dict[-1255641564] = { return parseString($0) } + dict[-1132882121] = { return SecretApi45.Bool.parse_boolFalse($0) } + dict[-1720552011] = { return SecretApi45.Bool.parse_boolTrue($0) } + dict[917541342] = { return SecretApi45.DecryptedMessage.parse_decryptedMessage($0) } + dict[1930838368] = { return SecretApi45.DecryptedMessage.parse_decryptedMessageService($0) } + dict[-586814357] = { return SecretApi45.DecryptedMessageAction.parse_decryptedMessageActionAbortKey($0) } + dict[1877046107] = { return SecretApi45.DecryptedMessageAction.parse_decryptedMessageActionAcceptKey($0) } + dict[-332526693] = { return SecretApi45.DecryptedMessageAction.parse_decryptedMessageActionCommitKey($0) } + dict[1700872964] = { return SecretApi45.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } + dict[1729750108] = { return SecretApi45.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } + dict[-1473258141] = { return SecretApi45.DecryptedMessageAction.parse_decryptedMessageActionNoop($0) } + dict[-217806717] = { return SecretApi45.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } + dict[206520510] = { return SecretApi45.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } + dict[-204906213] = { return SecretApi45.DecryptedMessageAction.parse_decryptedMessageActionRequestKey($0) } + dict[1360072880] = { return SecretApi45.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } + dict[-1967000459] = { return SecretApi45.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } + dict[-1586283796] = { return SecretApi45.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } + dict[-860719551] = { return SecretApi45.DecryptedMessageAction.parse_decryptedMessageActionTyping($0) } + dict[467867529] = { return SecretApi45.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } + dict[1474341323] = { return SecretApi45.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } + dict[1485441687] = { return SecretApi45.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } + dict[2063502050] = { return SecretApi45.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } + dict[144661578] = { return SecretApi45.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } + dict[-90853155] = { return SecretApi45.DecryptedMessageMedia.parse_decryptedMessageMediaExternalDocument($0) } + dict[893913689] = { return SecretApi45.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } + dict[-235238024] = { return SecretApi45.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } + dict[-1978796689] = { return SecretApi45.DecryptedMessageMedia.parse_decryptedMessageMediaVenue($0) } + dict[-1760785394] = { return SecretApi45.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } + dict[-452652584] = { return SecretApi45.DecryptedMessageMedia.parse_decryptedMessageMediaWebPage($0) } + dict[297109817] = { return SecretApi45.DocumentAttribute.parse_documentAttributeAnimated($0) } + dict[-556656416] = { return SecretApi45.DocumentAttribute.parse_documentAttributeAudio($0) } + dict[358154344] = { return SecretApi45.DocumentAttribute.parse_documentAttributeFilename($0) } + dict[1815593308] = { return SecretApi45.DocumentAttribute.parse_documentAttributeImageSize($0) } + dict[978674434] = { return SecretApi45.DocumentAttribute.parse_documentAttributeSticker($0) } + dict[1494273227] = { return SecretApi45.DocumentAttribute.parse_documentAttributeVideo($0) } + dict[1406570614] = { return SecretApi45.FileLocation.parse_fileLocation($0) } + dict[2086234950] = { return SecretApi45.FileLocation.parse_fileLocationUnavailable($0) } + dict[-4838507] = { return SecretApi45.InputStickerSet.parse_inputStickerSetEmpty($0) } + dict[-2044933984] = { return SecretApi45.InputStickerSet.parse_inputStickerSetShortName($0) } + dict[-1117713463] = { return SecretApi45.MessageEntity.parse_messageEntityBold($0) } + dict[1827637959] = { return SecretApi45.MessageEntity.parse_messageEntityBotCommand($0) } + dict[681706865] = { return SecretApi45.MessageEntity.parse_messageEntityCode($0) } + dict[1692693954] = { return SecretApi45.MessageEntity.parse_messageEntityEmail($0) } + dict[1868782349] = { return SecretApi45.MessageEntity.parse_messageEntityHashtag($0) } + dict[-2106619040] = { return SecretApi45.MessageEntity.parse_messageEntityItalic($0) } + dict[-100378723] = { return SecretApi45.MessageEntity.parse_messageEntityMention($0) } + dict[1938967520] = { return SecretApi45.MessageEntity.parse_messageEntityPre($0) } + dict[1990644519] = { return SecretApi45.MessageEntity.parse_messageEntityTextUrl($0) } + dict[-1148011883] = { return SecretApi45.MessageEntity.parse_messageEntityUnknown($0) } + dict[1859134776] = { return SecretApi45.MessageEntity.parse_messageEntityUrl($0) } + dict[-374917894] = { return SecretApi45.PhotoSize.parse_photoCachedSize($0) } + dict[2009052699] = { return SecretApi45.PhotoSize.parse_photoSize($0) } + dict[236446268] = { return SecretApi45.PhotoSize.parse_photoSizeEmpty($0) } + dict[-44119819] = { return SecretApi45.SendMessageAction.parse_sendMessageCancelAction($0) } + dict[1653390447] = { return SecretApi45.SendMessageAction.parse_sendMessageChooseContactAction($0) } + dict[393186209] = { return SecretApi45.SendMessageAction.parse_sendMessageGeoLocationAction($0) } + dict[-718310409] = { return SecretApi45.SendMessageAction.parse_sendMessageRecordAudioAction($0) } + dict[-1584933265] = { return SecretApi45.SendMessageAction.parse_sendMessageRecordVideoAction($0) } + dict[381645902] = { return SecretApi45.SendMessageAction.parse_sendMessageTypingAction($0) } + dict[-424899985] = { return SecretApi45.SendMessageAction.parse_sendMessageUploadAudioAction($0) } + dict[-1884362354] = { return SecretApi45.SendMessageAction.parse_sendMessageUploadDocumentAction($0) } + dict[-1727382502] = { return SecretApi45.SendMessageAction.parse_sendMessageUploadPhotoAction($0) } + dict[-1845219337] = { return SecretApi45.SendMessageAction.parse_sendMessageUploadVideoAction($0) } + return dict +}() + +public struct SecretApi45 { + public static func parse(_ buffer: Buffer) -> Any? { + let reader = BufferReader(buffer) + if let signature = reader.readInt32() { + return parse(reader, signature: signature) + } + return nil + } + + fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { + if let parser = parsers[signature] { + return parser(reader) + } + else { + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + return nil + } + } + + fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { + if let count = reader.readInt32() { + var array = [T]() + var i: Int32 = 0 + while i < count { + var signature = elementSignature + if elementSignature == 0 { + if let unboxedSignature = reader.readInt32() { + signature = unboxedSignature + } + else { + return nil + } + } + if let item = SecretApi45.parse(reader, signature: signature) as? T { + array.append(item) + } + else { + return nil + } + i += 1 + } + return array + } + return nil + } + + public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) { + switch object { + case let _1 as SecretApi45.Bool: + _1.serialize(buffer, boxed) + case let _1 as SecretApi45.DecryptedMessage: + _1.serialize(buffer, boxed) + case let _1 as SecretApi45.DecryptedMessageAction: + _1.serialize(buffer, boxed) + case let _1 as SecretApi45.DecryptedMessageLayer: + _1.serialize(buffer, boxed) + case let _1 as SecretApi45.DecryptedMessageMedia: + _1.serialize(buffer, boxed) + case let _1 as SecretApi45.DocumentAttribute: + _1.serialize(buffer, boxed) + case let _1 as SecretApi45.FileLocation: + _1.serialize(buffer, boxed) + case let _1 as SecretApi45.InputStickerSet: + _1.serialize(buffer, boxed) + case let _1 as SecretApi45.MessageEntity: + _1.serialize(buffer, boxed) + case let _1 as SecretApi45.PhotoSize: + _1.serialize(buffer, boxed) + case let _1 as SecretApi45.SendMessageAction: + _1.serialize(buffer, boxed) + default: + break + } + } + + public enum Bool { + case boolFalse + case boolTrue + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boolFalse: + if boxed { + buffer.appendInt32(-1132882121) + } + break + case .boolTrue: + if boxed { + buffer.appendInt32(-1720552011) + } + break + } + } + + fileprivate static func parse_boolFalse(_ reader: BufferReader) -> Bool? { + return SecretApi45.Bool.boolFalse + } + fileprivate static func parse_boolTrue(_ reader: BufferReader) -> Bool? { + return SecretApi45.Bool.boolTrue + } + } + + public enum DecryptedMessage { + case decryptedMessage(flags: Int32, randomId: Int64, ttl: Int32, message: String, media: SecretApi45.DecryptedMessageMedia?, entities: [SecretApi45.MessageEntity]?, viaBotName: String?, replyToRandomId: Int64?) + case decryptedMessageService(randomId: Int64, action: SecretApi45.DecryptedMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessage(let flags, let randomId, let ttl, let message, let media, let entities, let viaBotName, let replyToRandomId): + if boxed { + buffer.appendInt32(917541342) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(randomId, buffer: buffer, boxed: false) + serializeInt32(ttl, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 9) != 0 { + media!.serialize(buffer, true) + } + if Int(flags) & Int(1 << 7) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + } + } + if Int(flags) & Int(1 << 11) != 0 { + serializeString(viaBotName!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 3) != 0 { + serializeInt64(replyToRandomId!, buffer: buffer, boxed: false) + } + break + case .decryptedMessageService(let randomId, let action): + if boxed { + buffer.appendInt32(1930838368) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: SecretApi45.DecryptedMessageMedia? + if Int(_1 ?? 0) & Int(1 << 9) != 0 { + if let signature = reader.readInt32() { + _5 = SecretApi45.parse(reader, signature: signature) as? SecretApi45.DecryptedMessageMedia + } + } + var _6: [SecretApi45.MessageEntity]? + if Int(_1 ?? 0) & Int(1 << 7) != 0 { + if let _ = reader.readInt32() { + _6 = SecretApi45.parseVector(reader, elementSignature: 0, elementType: SecretApi45.MessageEntity.self) + } + } + var _7: String? + if Int(_1 ?? 0) & Int(1 << 11) != 0 { + _7 = parseString(reader) + } + var _8: Int64? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _8 = reader.readInt64() + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi45.DecryptedMessage.decryptedMessage(flags: _1!, randomId: _2!, ttl: _3!, message: _4!, media: _5, entities: _6, viaBotName: _7, replyToRandomId: _8) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: SecretApi45.DecryptedMessageAction? + if let signature = reader.readInt32() { + _2 = SecretApi45.parse(reader, signature: signature) as? SecretApi45.DecryptedMessageAction + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageAction { + case decryptedMessageActionAbortKey(exchangeId: Int64) + case decryptedMessageActionAcceptKey(exchangeId: Int64, gB: Buffer, keyFingerprint: Int64) + case decryptedMessageActionCommitKey(exchangeId: Int64, keyFingerprint: Int64) + case decryptedMessageActionDeleteMessages(randomIds: [Int64]) + case decryptedMessageActionFlushHistory + case decryptedMessageActionNoop + case decryptedMessageActionNotifyLayer(layer: Int32) + case decryptedMessageActionReadMessages(randomIds: [Int64]) + case decryptedMessageActionRequestKey(exchangeId: Int64, gA: Buffer) + case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) + case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) + case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) + case decryptedMessageActionTyping(action: SecretApi45.SendMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageActionAbortKey(let exchangeId): + if boxed { + buffer.appendInt32(-586814357) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + break + case .decryptedMessageActionAcceptKey(let exchangeId, let gB, let keyFingerprint): + if boxed { + buffer.appendInt32(1877046107) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gB, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionCommitKey(let exchangeId, let keyFingerprint): + if boxed { + buffer.appendInt32(-332526693) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionDeleteMessages(let randomIds): + if boxed { + buffer.appendInt32(1700872964) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionFlushHistory: + if boxed { + buffer.appendInt32(1729750108) + } + break + case .decryptedMessageActionNoop: + if boxed { + buffer.appendInt32(-1473258141) + } + break + case .decryptedMessageActionNotifyLayer(let layer): + if boxed { + buffer.appendInt32(-217806717) + } + serializeInt32(layer, buffer: buffer, boxed: false) + break + case .decryptedMessageActionReadMessages(let randomIds): + if boxed { + buffer.appendInt32(206520510) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionRequestKey(let exchangeId, let gA): + if boxed { + buffer.appendInt32(-204906213) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gA, buffer: buffer, boxed: false) + break + case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): + if boxed { + buffer.appendInt32(1360072880) + } + serializeInt32(startSeqNo, buffer: buffer, boxed: false) + serializeInt32(endSeqNo, buffer: buffer, boxed: false) + break + case .decryptedMessageActionScreenshotMessages(let randomIds): + if boxed { + buffer.appendInt32(-1967000459) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionSetMessageTTL(let ttlSeconds): + if boxed { + buffer.appendInt32(-1586283796) + } + serializeInt32(ttlSeconds, buffer: buffer, boxed: false) + break + case .decryptedMessageActionTyping(let action): + if boxed { + buffer.appendInt32(-860719551) + } + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessageActionAbortKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return SecretApi45.DecryptedMessageAction.decryptedMessageActionAbortKey(exchangeId: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionAcceptKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi45.DecryptedMessageAction.decryptedMessageActionAcceptKey(exchangeId: _1!, gB: _2!, keyFingerprint: _3!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionCommitKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.DecryptedMessageAction.decryptedMessageActionCommitKey(exchangeId: _1!, keyFingerprint: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi45.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi45.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi45.DecryptedMessageAction.decryptedMessageActionFlushHistory + } + fileprivate static func parse_decryptedMessageActionNoop(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi45.DecryptedMessageAction.decryptedMessageActionNoop + } + fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi45.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi45.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi45.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionRequestKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.DecryptedMessageAction.decryptedMessageActionRequestKey(exchangeId: _1!, gA: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi45.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi45.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi45.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionTyping(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: SecretApi45.SendMessageAction? + if let signature = reader.readInt32() { + _1 = SecretApi45.parse(reader, signature: signature) as? SecretApi45.SendMessageAction + } + let _c1 = _1 != nil + if _c1 { + return SecretApi45.DecryptedMessageAction.decryptedMessageActionTyping(action: _1!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageLayer { + case decryptedMessageLayer(randomBytes: Buffer, layer: Int32, inSeqNo: Int32, outSeqNo: Int32, message: SecretApi45.DecryptedMessage) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): + if boxed { + buffer.appendInt32(467867529) + } + serializeBytes(randomBytes, buffer: buffer, boxed: false) + serializeInt32(layer, buffer: buffer, boxed: false) + serializeInt32(inSeqNo, buffer: buffer, boxed: false) + serializeInt32(outSeqNo, buffer: buffer, boxed: false) + message.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessageLayer(_ reader: BufferReader) -> DecryptedMessageLayer? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: SecretApi45.DecryptedMessage? + if let signature = reader.readInt32() { + _5 = SecretApi45.parse(reader, signature: signature) as? SecretApi45.DecryptedMessage + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi45.DecryptedMessageLayer.decryptedMessageLayer(randomBytes: _1!, layer: _2!, inSeqNo: _3!, outSeqNo: _4!, message: _5!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageMedia { + case decryptedMessageMediaAudio(duration: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) + case decryptedMessageMediaDocument(thumb: Buffer, thumbW: Int32, thumbH: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer, attributes: [SecretApi45.DocumentAttribute], caption: String) + case decryptedMessageMediaEmpty + case decryptedMessageMediaExternalDocument(id: Int64, accessHash: Int64, date: Int32, mimeType: String, size: Int32, thumb: SecretApi45.PhotoSize, dcId: Int32, attributes: [SecretApi45.DocumentAttribute]) + case decryptedMessageMediaGeoPoint(lat: Double, long: Double) + case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaVenue(lat: Double, long: Double, title: String, address: String, provider: String, venueId: String) + case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaWebPage(url: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(1474341323) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): + if boxed { + buffer.appendInt32(1485441687) + } + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + serializeInt32(userId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let mimeType, let size, let key, let iv, let attributes, let caption): + if boxed { + buffer.appendInt32(2063502050) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaEmpty: + if boxed { + buffer.appendInt32(144661578) + } + break + case .decryptedMessageMediaExternalDocument(let id, let accessHash, let date, let mimeType, let size, let thumb, let dcId, let attributes): + if boxed { + buffer.appendInt32(-90853155) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + thumb.serialize(buffer, true) + serializeInt32(dcId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + break + case .decryptedMessageMediaGeoPoint(let lat, let long): + if boxed { + buffer.appendInt32(893913689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-235238024) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVenue(let lat, let long, let title, let address, let provider, let venueId): + if boxed { + buffer.appendInt32(-1978796689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(address, buffer: buffer, boxed: false) + serializeString(provider, buffer: buffer, boxed: false) + serializeString(venueId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-1760785394) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaWebPage(let url): + if boxed { + buffer.appendInt32(-452652584) + } + serializeString(url, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + var _4: Buffer? + _4 = parseBytes(reader) + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi45.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaContact(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi45.DecryptedMessageMedia.decryptedMessageMediaContact(phoneNumber: _1!, firstName: _2!, lastName: _3!, userId: _4!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: Int32? + _5 = reader.readInt32() + var _6: Buffer? + _6 = parseBytes(reader) + var _7: Buffer? + _7 = parseBytes(reader) + var _8: [SecretApi45.DocumentAttribute]? + if let _ = reader.readInt32() { + _8 = SecretApi45.parseVector(reader, elementSignature: 0, elementType: SecretApi45.DocumentAttribute.self) + } + var _9: String? + _9 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi45.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, mimeType: _4!, size: _5!, key: _6!, iv: _7!, attributes: _8!, caption: _9!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { + return SecretApi45.DecryptedMessageMedia.decryptedMessageMediaEmpty + } + fileprivate static func parse_decryptedMessageMediaExternalDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: Int32? + _5 = reader.readInt32() + var _6: SecretApi45.PhotoSize? + if let signature = reader.readInt32() { + _6 = SecretApi45.parse(reader, signature: signature) as? SecretApi45.PhotoSize + } + var _7: Int32? + _7 = reader.readInt32() + var _8: [SecretApi45.DocumentAttribute]? + if let _ = reader.readInt32() { + _8 = SecretApi45.parseVector(reader, elementSignature: 0, elementType: SecretApi45.DocumentAttribute.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi45.DecryptedMessageMedia.decryptedMessageMediaExternalDocument(id: _1!, accessHash: _2!, date: _3!, mimeType: _4!, size: _5!, thumb: _6!, dcId: _7!, attributes: _8!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaPhoto(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: Buffer? + _7 = parseBytes(reader) + var _8: Buffer? + _8 = parseBytes(reader) + var _9: String? + _9 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi45.DecryptedMessageMedia.decryptedMessageMediaPhoto(thumb: _1!, thumbW: _2!, thumbH: _3!, w: _4!, h: _5!, size: _6!, key: _7!, iv: _8!, caption: _9!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaVenue(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + var _3: String? + _3 = parseString(reader) + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: String? + _6 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return SecretApi45.DecryptedMessageMedia.decryptedMessageMediaVenue(lat: _1!, long: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaVideo(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: String? + _5 = parseString(reader) + var _6: Int32? + _6 = reader.readInt32() + var _7: Int32? + _7 = reader.readInt32() + var _8: Int32? + _8 = reader.readInt32() + var _9: Buffer? + _9 = parseBytes(reader) + var _10: Buffer? + _10 = parseBytes(reader) + var _11: String? + _11 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + let _c10 = _10 != nil + let _c11 = _11 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { + return SecretApi45.DecryptedMessageMedia.decryptedMessageMediaVideo(thumb: _1!, thumbW: _2!, thumbH: _3!, duration: _4!, mimeType: _5!, w: _6!, h: _7!, size: _8!, key: _9!, iv: _10!, caption: _11!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaWebPage(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi45.DecryptedMessageMedia.decryptedMessageMediaWebPage(url: _1!) + } + else { + return nil + } + } + } + + public enum DocumentAttribute { + case documentAttributeAnimated + case documentAttributeAudio(duration: Int32, title: String, performer: String) + case documentAttributeFilename(fileName: String) + case documentAttributeImageSize(w: Int32, h: Int32) + case documentAttributeSticker(alt: String, stickerset: SecretApi45.InputStickerSet) + case documentAttributeVideo(duration: Int32, w: Int32, h: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .documentAttributeAnimated: + if boxed { + buffer.appendInt32(297109817) + } + break + case .documentAttributeAudio(let duration, let title, let performer): + if boxed { + buffer.appendInt32(-556656416) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(performer, buffer: buffer, boxed: false) + break + case .documentAttributeFilename(let fileName): + if boxed { + buffer.appendInt32(358154344) + } + serializeString(fileName, buffer: buffer, boxed: false) + break + case .documentAttributeImageSize(let w, let h): + if boxed { + buffer.appendInt32(1815593308) + } + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + case .documentAttributeSticker(let alt, let stickerset): + if boxed { + buffer.appendInt32(978674434) + } + serializeString(alt, buffer: buffer, boxed: false) + stickerset.serialize(buffer, true) + break + case .documentAttributeVideo(let duration, let w, let h): + if boxed { + buffer.appendInt32(1494273227) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_documentAttributeAnimated(_ reader: BufferReader) -> DocumentAttribute? { + return SecretApi45.DocumentAttribute.documentAttributeAnimated + } + fileprivate static func parse_documentAttributeAudio(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi45.DocumentAttribute.documentAttributeAudio(duration: _1!, title: _2!, performer: _3!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeFilename(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi45.DocumentAttribute.documentAttributeFilename(fileName: _1!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeImageSize(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.DocumentAttribute.documentAttributeImageSize(w: _1!, h: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeSticker(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi45.InputStickerSet? + if let signature = reader.readInt32() { + _2 = SecretApi45.parse(reader, signature: signature) as? SecretApi45.InputStickerSet + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.DocumentAttribute.documentAttributeSticker(alt: _1!, stickerset: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeVideo(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi45.DocumentAttribute.documentAttributeVideo(duration: _1!, w: _2!, h: _3!) + } + else { + return nil + } + } + } + + public enum FileLocation { + case fileLocation(dcId: Int32, volumeId: Int64, localId: Int32, secret: Int64) + case fileLocationUnavailable(volumeId: Int64, localId: Int32, secret: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .fileLocation(let dcId, let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(1406570614) + } + serializeInt32(dcId, buffer: buffer, boxed: false) + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + case .fileLocationUnavailable(let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(2086234950) + } + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_fileLocation(_ reader: BufferReader) -> FileLocation? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int64? + _4 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi45.FileLocation.fileLocation(dcId: _1!, volumeId: _2!, localId: _3!, secret: _4!) + } + else { + return nil + } + } + fileprivate static func parse_fileLocationUnavailable(_ reader: BufferReader) -> FileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi45.FileLocation.fileLocationUnavailable(volumeId: _1!, localId: _2!, secret: _3!) + } + else { + return nil + } + } + } + + public enum InputStickerSet { + case inputStickerSetEmpty + case inputStickerSetShortName(shortName: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputStickerSetEmpty: + if boxed { + buffer.appendInt32(-4838507) + } + break + case .inputStickerSetShortName(let shortName): + if boxed { + buffer.appendInt32(-2044933984) + } + serializeString(shortName, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_inputStickerSetEmpty(_ reader: BufferReader) -> InputStickerSet? { + return SecretApi45.InputStickerSet.inputStickerSetEmpty + } + fileprivate static func parse_inputStickerSetShortName(_ reader: BufferReader) -> InputStickerSet? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi45.InputStickerSet.inputStickerSetShortName(shortName: _1!) + } + else { + return nil + } + } + } + + public enum MessageEntity { + case messageEntityBold(offset: Int32, length: Int32) + case messageEntityBotCommand(offset: Int32, length: Int32) + case messageEntityCode(offset: Int32, length: Int32) + case messageEntityEmail(offset: Int32, length: Int32) + case messageEntityHashtag(offset: Int32, length: Int32) + case messageEntityItalic(offset: Int32, length: Int32) + case messageEntityMention(offset: Int32, length: Int32) + case messageEntityPre(offset: Int32, length: Int32, language: String) + case messageEntityTextUrl(offset: Int32, length: Int32, url: String) + case messageEntityUnknown(offset: Int32, length: Int32) + case messageEntityUrl(offset: Int32, length: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageEntityBold(let offset, let length): + if boxed { + buffer.appendInt32(-1117713463) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityBotCommand(let offset, let length): + if boxed { + buffer.appendInt32(1827637959) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityCode(let offset, let length): + if boxed { + buffer.appendInt32(681706865) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityEmail(let offset, let length): + if boxed { + buffer.appendInt32(1692693954) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityHashtag(let offset, let length): + if boxed { + buffer.appendInt32(1868782349) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityItalic(let offset, let length): + if boxed { + buffer.appendInt32(-2106619040) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityMention(let offset, let length): + if boxed { + buffer.appendInt32(-100378723) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityPre(let offset, let length, let language): + if boxed { + buffer.appendInt32(1938967520) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(language, buffer: buffer, boxed: false) + break + case .messageEntityTextUrl(let offset, let length, let url): + if boxed { + buffer.appendInt32(1990644519) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + break + case .messageEntityUnknown(let offset, let length): + if boxed { + buffer.appendInt32(-1148011883) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityUrl(let offset, let length): + if boxed { + buffer.appendInt32(1859134776) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_messageEntityBold(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.MessageEntity.messageEntityBold(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityBotCommand(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.MessageEntity.messageEntityBotCommand(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityCode(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.MessageEntity.messageEntityCode(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.MessageEntity.messageEntityEmail(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityHashtag(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.MessageEntity.messageEntityHashtag(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityItalic(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.MessageEntity.messageEntityItalic(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityMention(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.MessageEntity.messageEntityMention(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityPre(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi45.MessageEntity.messageEntityPre(offset: _1!, length: _2!, language: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityTextUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi45.MessageEntity.messageEntityTextUrl(offset: _1!, length: _2!, url: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUnknown(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.MessageEntity.messageEntityUnknown(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi45.MessageEntity.messageEntityUrl(offset: _1!, length: _2!) + } + else { + return nil + } + } + } + + public enum PhotoSize { + case photoCachedSize(type: String, location: SecretApi45.FileLocation, w: Int32, h: Int32, bytes: Buffer) + case photoSize(type: String, location: SecretApi45.FileLocation, w: Int32, h: Int32, size: Int32) + case photoSizeEmpty(type: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .photoCachedSize(let type, let location, let w, let h, let bytes): + if boxed { + buffer.appendInt32(-374917894) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeBytes(bytes, buffer: buffer, boxed: false) + break + case .photoSize(let type, let location, let w, let h, let size): + if boxed { + buffer.appendInt32(2009052699) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + break + case .photoSizeEmpty(let type): + if boxed { + buffer.appendInt32(236446268) + } + serializeString(type, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_photoCachedSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi45.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi45.parse(reader, signature: signature) as? SecretApi45.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi45.PhotoSize.photoCachedSize(type: _1!, location: _2!, w: _3!, h: _4!, bytes: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi45.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi45.parse(reader, signature: signature) as? SecretApi45.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi45.PhotoSize.photoSize(type: _1!, location: _2!, w: _3!, h: _4!, size: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSizeEmpty(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi45.PhotoSize.photoSizeEmpty(type: _1!) + } + else { + return nil + } + } + } + + public enum SendMessageAction { + case sendMessageCancelAction + case sendMessageChooseContactAction + case sendMessageGeoLocationAction + case sendMessageRecordAudioAction + case sendMessageRecordVideoAction + case sendMessageTypingAction + case sendMessageUploadAudioAction + case sendMessageUploadDocumentAction + case sendMessageUploadPhotoAction + case sendMessageUploadVideoAction + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sendMessageCancelAction: + if boxed { + buffer.appendInt32(-44119819) + } + break + case .sendMessageChooseContactAction: + if boxed { + buffer.appendInt32(1653390447) + } + break + case .sendMessageGeoLocationAction: + if boxed { + buffer.appendInt32(393186209) + } + break + case .sendMessageRecordAudioAction: + if boxed { + buffer.appendInt32(-718310409) + } + break + case .sendMessageRecordVideoAction: + if boxed { + buffer.appendInt32(-1584933265) + } + break + case .sendMessageTypingAction: + if boxed { + buffer.appendInt32(381645902) + } + break + case .sendMessageUploadAudioAction: + if boxed { + buffer.appendInt32(-424899985) + } + break + case .sendMessageUploadDocumentAction: + if boxed { + buffer.appendInt32(-1884362354) + } + break + case .sendMessageUploadPhotoAction: + if boxed { + buffer.appendInt32(-1727382502) + } + break + case .sendMessageUploadVideoAction: + if boxed { + buffer.appendInt32(-1845219337) + } + break + } + } + + fileprivate static func parse_sendMessageCancelAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi45.SendMessageAction.sendMessageCancelAction + } + fileprivate static func parse_sendMessageChooseContactAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi45.SendMessageAction.sendMessageChooseContactAction + } + fileprivate static func parse_sendMessageGeoLocationAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi45.SendMessageAction.sendMessageGeoLocationAction + } + fileprivate static func parse_sendMessageRecordAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi45.SendMessageAction.sendMessageRecordAudioAction + } + fileprivate static func parse_sendMessageRecordVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi45.SendMessageAction.sendMessageRecordVideoAction + } + fileprivate static func parse_sendMessageTypingAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi45.SendMessageAction.sendMessageTypingAction + } + fileprivate static func parse_sendMessageUploadAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi45.SendMessageAction.sendMessageUploadAudioAction + } + fileprivate static func parse_sendMessageUploadDocumentAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi45.SendMessageAction.sendMessageUploadDocumentAction + } + fileprivate static func parse_sendMessageUploadPhotoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi45.SendMessageAction.sendMessageUploadPhotoAction + } + fileprivate static func parse_sendMessageUploadVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi45.SendMessageAction.sendMessageUploadVideoAction + } + } + +} diff --git a/submodules/TelegramApi/Sources/SecretApiLayer46.swift b/submodules/TelegramApi/Sources/SecretApiLayer46.swift index 2b0bca2502..4986c5444a 100644 --- a/submodules/TelegramApi/Sources/SecretApiLayer46.swift +++ b/submodules/TelegramApi/Sources/SecretApiLayer46.swift @@ -5,55 +5,68 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[570911930] = { return $0.readInt64() } dict[571523412] = { return $0.readDouble() } dict[-1255641564] = { return parseString($0) } - dict[-1586283796] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } - dict[206520510] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } - dict[1700872964] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } - dict[-1967000459] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } - dict[1729750108] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } - dict[-217806717] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } - dict[1360072880] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } - dict[-204906213] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionRequestKey($0) } - dict[1877046107] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionAcceptKey($0) } - dict[-586814357] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionAbortKey($0) } - dict[-332526693] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionCommitKey($0) } - dict[-1473258141] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionNoop($0) } - dict[236446268] = { return SecretApi46.PhotoSize.parse_photoSizeEmpty($0) } - dict[2009052699] = { return SecretApi46.PhotoSize.parse_photoSize($0) } - dict[-374917894] = { return SecretApi46.PhotoSize.parse_photoCachedSize($0) } - dict[2086234950] = { return SecretApi46.FileLocation.parse_fileLocationUnavailable($0) } - dict[1406570614] = { return SecretApi46.FileLocation.parse_fileLocation($0) } - dict[467867529] = { return SecretApi46.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } - dict[1930838368] = { return SecretApi46.DecryptedMessage.parse_decryptedMessageService($0) } + dict[-1132882121] = { return SecretApi46.Bool.parse_boolFalse($0) } + dict[-1720552011] = { return SecretApi46.Bool.parse_boolTrue($0) } dict[917541342] = { return SecretApi46.DecryptedMessage.parse_decryptedMessage($0) } - dict[1815593308] = { return SecretApi46.DocumentAttribute.parse_documentAttributeImageSize($0) } + dict[1930838368] = { return SecretApi46.DecryptedMessage.parse_decryptedMessageService($0) } + dict[-586814357] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionAbortKey($0) } + dict[1877046107] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionAcceptKey($0) } + dict[-332526693] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionCommitKey($0) } + dict[1700872964] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } + dict[1729750108] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } + dict[-1473258141] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionNoop($0) } + dict[-217806717] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } + dict[206520510] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } + dict[-204906213] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionRequestKey($0) } + dict[1360072880] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } + dict[-1967000459] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } + dict[-1586283796] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } + dict[-860719551] = { return SecretApi46.DecryptedMessageAction.parse_decryptedMessageActionTyping($0) } + dict[467867529] = { return SecretApi46.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } + dict[1474341323] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } + dict[1485441687] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } + dict[2063502050] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } + dict[144661578] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } + dict[-90853155] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaExternalDocument($0) } + dict[893913689] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } + dict[-235238024] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } + dict[-1978796689] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaVenue($0) } + dict[-1760785394] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } + dict[-452652584] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaWebPage($0) } dict[297109817] = { return SecretApi46.DocumentAttribute.parse_documentAttributeAnimated($0) } - dict[1494273227] = { return SecretApi46.DocumentAttribute.parse_documentAttributeVideo($0) } - dict[358154344] = { return SecretApi46.DocumentAttribute.parse_documentAttributeFilename($0) } - dict[978674434] = { return SecretApi46.DocumentAttribute.parse_documentAttributeSticker($0) } dict[-1739392570] = { return SecretApi46.DocumentAttribute.parse_documentAttributeAudio($0) } - dict[-2044933984] = { return SecretApi46.InputStickerSet.parse_inputStickerSetShortName($0) } + dict[358154344] = { return SecretApi46.DocumentAttribute.parse_documentAttributeFilename($0) } + dict[1815593308] = { return SecretApi46.DocumentAttribute.parse_documentAttributeImageSize($0) } + dict[978674434] = { return SecretApi46.DocumentAttribute.parse_documentAttributeSticker($0) } + dict[1494273227] = { return SecretApi46.DocumentAttribute.parse_documentAttributeVideo($0) } + dict[1406570614] = { return SecretApi46.FileLocation.parse_fileLocation($0) } + dict[2086234950] = { return SecretApi46.FileLocation.parse_fileLocationUnavailable($0) } dict[-4838507] = { return SecretApi46.InputStickerSet.parse_inputStickerSetEmpty($0) } - dict[-1148011883] = { return SecretApi46.MessageEntity.parse_messageEntityUnknown($0) } - dict[-100378723] = { return SecretApi46.MessageEntity.parse_messageEntityMention($0) } - dict[1868782349] = { return SecretApi46.MessageEntity.parse_messageEntityHashtag($0) } - dict[1827637959] = { return SecretApi46.MessageEntity.parse_messageEntityBotCommand($0) } - dict[1859134776] = { return SecretApi46.MessageEntity.parse_messageEntityUrl($0) } - dict[1692693954] = { return SecretApi46.MessageEntity.parse_messageEntityEmail($0) } + dict[-2044933984] = { return SecretApi46.InputStickerSet.parse_inputStickerSetShortName($0) } dict[-1117713463] = { return SecretApi46.MessageEntity.parse_messageEntityBold($0) } - dict[-2106619040] = { return SecretApi46.MessageEntity.parse_messageEntityItalic($0) } + dict[1827637959] = { return SecretApi46.MessageEntity.parse_messageEntityBotCommand($0) } dict[681706865] = { return SecretApi46.MessageEntity.parse_messageEntityCode($0) } + dict[1692693954] = { return SecretApi46.MessageEntity.parse_messageEntityEmail($0) } + dict[1868782349] = { return SecretApi46.MessageEntity.parse_messageEntityHashtag($0) } + dict[-2106619040] = { return SecretApi46.MessageEntity.parse_messageEntityItalic($0) } + dict[-100378723] = { return SecretApi46.MessageEntity.parse_messageEntityMention($0) } dict[1938967520] = { return SecretApi46.MessageEntity.parse_messageEntityPre($0) } dict[1990644519] = { return SecretApi46.MessageEntity.parse_messageEntityTextUrl($0) } - dict[144661578] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } - dict[893913689] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } - dict[1485441687] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } - dict[1474341323] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } - dict[-90853155] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaExternalDocument($0) } - dict[-235238024] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } - dict[2063502050] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } - dict[-1760785394] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } - dict[-1978796689] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaVenue($0) } - dict[-452652584] = { return SecretApi46.DecryptedMessageMedia.parse_decryptedMessageMediaWebPage($0) } + dict[-1148011883] = { return SecretApi46.MessageEntity.parse_messageEntityUnknown($0) } + dict[1859134776] = { return SecretApi46.MessageEntity.parse_messageEntityUrl($0) } + dict[-374917894] = { return SecretApi46.PhotoSize.parse_photoCachedSize($0) } + dict[2009052699] = { return SecretApi46.PhotoSize.parse_photoSize($0) } + dict[236446268] = { return SecretApi46.PhotoSize.parse_photoSizeEmpty($0) } + dict[-44119819] = { return SecretApi46.SendMessageAction.parse_sendMessageCancelAction($0) } + dict[1653390447] = { return SecretApi46.SendMessageAction.parse_sendMessageChooseContactAction($0) } + dict[393186209] = { return SecretApi46.SendMessageAction.parse_sendMessageGeoLocationAction($0) } + dict[-718310409] = { return SecretApi46.SendMessageAction.parse_sendMessageRecordAudioAction($0) } + dict[-1584933265] = { return SecretApi46.SendMessageAction.parse_sendMessageRecordVideoAction($0) } + dict[381645902] = { return SecretApi46.SendMessageAction.parse_sendMessageTypingAction($0) } + dict[-424899985] = { return SecretApi46.SendMessageAction.parse_sendMessageUploadAudioAction($0) } + dict[-1884362354] = { return SecretApi46.SendMessageAction.parse_sendMessageUploadDocumentAction($0) } + dict[-1727382502] = { return SecretApi46.SendMessageAction.parse_sendMessageUploadPhotoAction($0) } + dict[-1845219337] = { return SecretApi46.SendMessageAction.parse_sendMessageUploadVideoAction($0) } return dict }() @@ -65,18 +78,18 @@ public struct SecretApi46 { } return nil } - - fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { - if let parser = parsers[signature] { - return parser(reader) - } - else { - telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") - return nil - } + + fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { + if let parser = parsers[signature] { + return parser(reader) } - - fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { + else { + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + return nil + } + } + + fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { if let count = reader.readInt32() { var array = [T]() var i: Int32 = 0 @@ -102,227 +115,285 @@ public struct SecretApi46 { } return nil } - + public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) { switch object { - case let _1 as SecretApi46.DecryptedMessageAction: - _1.serialize(buffer, boxed) - case let _1 as SecretApi46.PhotoSize: - _1.serialize(buffer, boxed) - case let _1 as SecretApi46.FileLocation: - _1.serialize(buffer, boxed) - case let _1 as SecretApi46.DecryptedMessageLayer: - _1.serialize(buffer, boxed) - case let _1 as SecretApi46.DecryptedMessage: - _1.serialize(buffer, boxed) - case let _1 as SecretApi46.DocumentAttribute: - _1.serialize(buffer, boxed) - case let _1 as SecretApi46.InputStickerSet: - _1.serialize(buffer, boxed) - case let _1 as SecretApi46.MessageEntity: - _1.serialize(buffer, boxed) - case let _1 as SecretApi46.DecryptedMessageMedia: - _1.serialize(buffer, boxed) - default: + case let _1 as SecretApi46.Bool: + _1.serialize(buffer, boxed) + case let _1 as SecretApi46.DecryptedMessage: + _1.serialize(buffer, boxed) + case let _1 as SecretApi46.DecryptedMessageAction: + _1.serialize(buffer, boxed) + case let _1 as SecretApi46.DecryptedMessageLayer: + _1.serialize(buffer, boxed) + case let _1 as SecretApi46.DecryptedMessageMedia: + _1.serialize(buffer, boxed) + case let _1 as SecretApi46.DocumentAttribute: + _1.serialize(buffer, boxed) + case let _1 as SecretApi46.FileLocation: + _1.serialize(buffer, boxed) + case let _1 as SecretApi46.InputStickerSet: + _1.serialize(buffer, boxed) + case let _1 as SecretApi46.MessageEntity: + _1.serialize(buffer, boxed) + case let _1 as SecretApi46.PhotoSize: + _1.serialize(buffer, boxed) + case let _1 as SecretApi46.SendMessageAction: + _1.serialize(buffer, boxed) + default: + break + } + } + + public enum Bool { + case boolFalse + case boolTrue + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boolFalse: + if boxed { + buffer.appendInt32(-1132882121) + } break + case .boolTrue: + if boxed { + buffer.appendInt32(-1720552011) + } + break + } + } + + fileprivate static func parse_boolFalse(_ reader: BufferReader) -> Bool? { + return SecretApi46.Bool.boolFalse + } + fileprivate static func parse_boolTrue(_ reader: BufferReader) -> Bool? { + return SecretApi46.Bool.boolTrue + } + } + + public enum DecryptedMessage { + case decryptedMessage(flags: Int32, randomId: Int64, ttl: Int32, message: String, media: SecretApi46.DecryptedMessageMedia?, entities: [SecretApi46.MessageEntity]?, viaBotName: String?, replyToRandomId: Int64?) + case decryptedMessageService(randomId: Int64, action: SecretApi46.DecryptedMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessage(let flags, let randomId, let ttl, let message, let media, let entities, let viaBotName, let replyToRandomId): + if boxed { + buffer.appendInt32(917541342) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(randomId, buffer: buffer, boxed: false) + serializeInt32(ttl, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 9) != 0 { + media!.serialize(buffer, true) + } + if Int(flags) & Int(1 << 7) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + } + } + if Int(flags) & Int(1 << 11) != 0 { + serializeString(viaBotName!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 3) != 0 { + serializeInt64(replyToRandomId!, buffer: buffer, boxed: false) + } + break + case .decryptedMessageService(let randomId, let action): + if boxed { + buffer.appendInt32(1930838368) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: SecretApi46.DecryptedMessageMedia? + if Int(_1 ?? 0) & Int(1 << 9) != 0 { + if let signature = reader.readInt32() { + _5 = SecretApi46.parse(reader, signature: signature) as? SecretApi46.DecryptedMessageMedia + } + } + var _6: [SecretApi46.MessageEntity]? + if Int(_1 ?? 0) & Int(1 << 7) != 0 { + if let _ = reader.readInt32() { + _6 = SecretApi46.parseVector(reader, elementSignature: 0, elementType: SecretApi46.MessageEntity.self) + } + } + var _7: String? + if Int(_1 ?? 0) & Int(1 << 11) != 0 { + _7 = parseString(reader) + } + var _8: Int64? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _8 = reader.readInt64() + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi46.DecryptedMessage.decryptedMessage(flags: _1!, randomId: _2!, ttl: _3!, message: _4!, media: _5, entities: _6, viaBotName: _7, replyToRandomId: _8) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: SecretApi46.DecryptedMessageAction? + if let signature = reader.readInt32() { + _2 = SecretApi46.parse(reader, signature: signature) as? SecretApi46.DecryptedMessageAction + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) + } + else { + return nil + } } } public enum DecryptedMessageAction { - case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) - case decryptedMessageActionReadMessages(randomIds: [Int64]) - case decryptedMessageActionDeleteMessages(randomIds: [Int64]) - case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) - case decryptedMessageActionFlushHistory - case decryptedMessageActionNotifyLayer(layer: Int32) - case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) - case decryptedMessageActionRequestKey(exchangeId: Int64, gA: Buffer) - case decryptedMessageActionAcceptKey(exchangeId: Int64, gB: Buffer, keyFingerprint: Int64) case decryptedMessageActionAbortKey(exchangeId: Int64) + case decryptedMessageActionAcceptKey(exchangeId: Int64, gB: Buffer, keyFingerprint: Int64) case decryptedMessageActionCommitKey(exchangeId: Int64, keyFingerprint: Int64) + case decryptedMessageActionDeleteMessages(randomIds: [Int64]) + case decryptedMessageActionFlushHistory case decryptedMessageActionNoop - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageActionSetMessageTTL(let ttlSeconds): - if boxed { - buffer.appendInt32(-1586283796) - } - serializeInt32(ttlSeconds, buffer: buffer, boxed: false) - break - case .decryptedMessageActionReadMessages(let randomIds): - if boxed { - buffer.appendInt32(206520510) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionDeleteMessages(let randomIds): - if boxed { - buffer.appendInt32(1700872964) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionScreenshotMessages(let randomIds): - if boxed { - buffer.appendInt32(-1967000459) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionFlushHistory: - if boxed { - buffer.appendInt32(1729750108) - } - - break - case .decryptedMessageActionNotifyLayer(let layer): - if boxed { - buffer.appendInt32(-217806717) - } - serializeInt32(layer, buffer: buffer, boxed: false) - break - case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): - if boxed { - buffer.appendInt32(1360072880) - } - serializeInt32(startSeqNo, buffer: buffer, boxed: false) - serializeInt32(endSeqNo, buffer: buffer, boxed: false) - break - case .decryptedMessageActionRequestKey(let exchangeId, let gA): - if boxed { - buffer.appendInt32(-204906213) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - serializeBytes(gA, buffer: buffer, boxed: false) - break - case .decryptedMessageActionAcceptKey(let exchangeId, let gB, let keyFingerprint): - if boxed { - buffer.appendInt32(1877046107) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - serializeBytes(gB, buffer: buffer, boxed: false) - serializeInt64(keyFingerprint, buffer: buffer, boxed: false) - break - case .decryptedMessageActionAbortKey(let exchangeId): - if boxed { - buffer.appendInt32(-586814357) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - break - case .decryptedMessageActionCommitKey(let exchangeId, let keyFingerprint): - if boxed { - buffer.appendInt32(-332526693) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - serializeInt64(keyFingerprint, buffer: buffer, boxed: false) - break - case .decryptedMessageActionNoop: - if boxed { - buffer.appendInt32(-1473258141) - } - - break - } - } - - fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return SecretApi46.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) - } - else { - return nil + case decryptedMessageActionNotifyLayer(layer: Int32) + case decryptedMessageActionReadMessages(randomIds: [Int64]) + case decryptedMessageActionRequestKey(exchangeId: Int64, gA: Buffer) + case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) + case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) + case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) + case decryptedMessageActionTyping(action: SecretApi46.SendMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageActionAbortKey(let exchangeId): + if boxed { + buffer.appendInt32(-586814357) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + break + case .decryptedMessageActionAcceptKey(let exchangeId, let gB, let keyFingerprint): + if boxed { + buffer.appendInt32(1877046107) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gB, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionCommitKey(let exchangeId, let keyFingerprint): + if boxed { + buffer.appendInt32(-332526693) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionDeleteMessages(let randomIds): + if boxed { + buffer.appendInt32(1700872964) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionFlushHistory: + if boxed { + buffer.appendInt32(1729750108) + } + break + case .decryptedMessageActionNoop: + if boxed { + buffer.appendInt32(-1473258141) + } + break + case .decryptedMessageActionNotifyLayer(let layer): + if boxed { + buffer.appendInt32(-217806717) + } + serializeInt32(layer, buffer: buffer, boxed: false) + break + case .decryptedMessageActionReadMessages(let randomIds): + if boxed { + buffer.appendInt32(206520510) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionRequestKey(let exchangeId, let gA): + if boxed { + buffer.appendInt32(-204906213) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gA, buffer: buffer, boxed: false) + break + case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): + if boxed { + buffer.appendInt32(1360072880) + } + serializeInt32(startSeqNo, buffer: buffer, boxed: false) + serializeInt32(endSeqNo, buffer: buffer, boxed: false) + break + case .decryptedMessageActionScreenshotMessages(let randomIds): + if boxed { + buffer.appendInt32(-1967000459) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionSetMessageTTL(let ttlSeconds): + if boxed { + buffer.appendInt32(-1586283796) + } + serializeInt32(ttlSeconds, buffer: buffer, boxed: false) + break + case .decryptedMessageActionTyping(let action): + if boxed { + buffer.appendInt32(-860719551) + } + action.serialize(buffer, true) + break } } - fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi46.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi46.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi46.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi46.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi46.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi46.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { - return SecretApi46.DecryptedMessageAction.decryptedMessageActionFlushHistory - } - fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return SecretApi46.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionRequestKey(_ reader: BufferReader) -> DecryptedMessageAction? { + + fileprivate static func parse_decryptedMessageActionAbortKey(_ reader: BufferReader) -> DecryptedMessageAction? { var _1: Int64? _1 = reader.readInt64() - var _2: Buffer? - _2 = parseBytes(reader) let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.DecryptedMessageAction.decryptedMessageActionRequestKey(exchangeId: _1!, gA: _2!) + if _c1 { + return SecretApi46.DecryptedMessageAction.decryptedMessageActionAbortKey(exchangeId: _1!) } else { return nil @@ -345,17 +416,6 @@ public struct SecretApi46 { return nil } } - fileprivate static func parse_decryptedMessageActionAbortKey(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return SecretApi46.DecryptedMessageAction.decryptedMessageActionAbortKey(exchangeId: _1!) - } - else { - return nil - } - } fileprivate static func parse_decryptedMessageActionCommitKey(_ reader: BufferReader) -> DecryptedMessageAction? { var _1: Int64? _1 = reader.readInt64() @@ -370,196 +430,134 @@ public struct SecretApi46 { return nil } } + fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi46.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi46.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi46.DecryptedMessageAction.decryptedMessageActionFlushHistory + } fileprivate static func parse_decryptedMessageActionNoop(_ reader: BufferReader) -> DecryptedMessageAction? { return SecretApi46.DecryptedMessageAction.decryptedMessageActionNoop } - - } - - public enum PhotoSize { - case photoSizeEmpty(type: String) - case photoSize(type: String, location: SecretApi46.FileLocation, w: Int32, h: Int32, size: Int32) - case photoCachedSize(type: String, location: SecretApi46.FileLocation, w: Int32, h: Int32, bytes: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .photoSizeEmpty(let type): - if boxed { - buffer.appendInt32(236446268) - } - serializeString(type, buffer: buffer, boxed: false) - break - case .photoSize(let type, let location, let w, let h, let size): - if boxed { - buffer.appendInt32(2009052699) - } - serializeString(type, buffer: buffer, boxed: false) - location.serialize(buffer, true) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - break - case .photoCachedSize(let type, let location, let w, let h, let bytes): - if boxed { - buffer.appendInt32(-374917894) - } - serializeString(type, buffer: buffer, boxed: false) - location.serialize(buffer, true) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeBytes(bytes, buffer: buffer, boxed: false) - break - } - } - - fileprivate static func parse_photoSizeEmpty(_ reader: BufferReader) -> PhotoSize? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return SecretApi46.PhotoSize.photoSizeEmpty(type: _1!) - } - else { - return nil - } - } - fileprivate static func parse_photoSize(_ reader: BufferReader) -> PhotoSize? { - var _1: String? - _1 = parseString(reader) - var _2: SecretApi46.FileLocation? - if let signature = reader.readInt32() { - _2 = SecretApi46.parse(reader, signature: signature) as? SecretApi46.FileLocation - } - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi46.PhotoSize.photoSize(type: _1!, location: _2!, w: _3!, h: _4!, size: _5!) - } - else { - return nil - } - } - fileprivate static func parse_photoCachedSize(_ reader: BufferReader) -> PhotoSize? { - var _1: String? - _1 = parseString(reader) - var _2: SecretApi46.FileLocation? - if let signature = reader.readInt32() { - _2 = SecretApi46.parse(reader, signature: signature) as? SecretApi46.FileLocation - } - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Buffer? - _5 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi46.PhotoSize.photoCachedSize(type: _1!, location: _2!, w: _3!, h: _4!, bytes: _5!) - } - else { - return nil - } - } - - } - - public enum FileLocation { - case fileLocationUnavailable(volumeId: Int64, localId: Int32, secret: Int64) - case fileLocation(dcId: Int32, volumeId: Int64, localId: Int32, secret: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .fileLocationUnavailable(let volumeId, let localId, let secret): - if boxed { - buffer.appendInt32(2086234950) - } - serializeInt64(volumeId, buffer: buffer, boxed: false) - serializeInt32(localId, buffer: buffer, boxed: false) - serializeInt64(secret, buffer: buffer, boxed: false) - break - case .fileLocation(let dcId, let volumeId, let localId, let secret): - if boxed { - buffer.appendInt32(1406570614) - } - serializeInt32(dcId, buffer: buffer, boxed: false) - serializeInt64(volumeId, buffer: buffer, boxed: false) - serializeInt32(localId, buffer: buffer, boxed: false) - serializeInt64(secret, buffer: buffer, boxed: false) - break - } - } - - fileprivate static func parse_fileLocationUnavailable(_ reader: BufferReader) -> FileLocation? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi46.FileLocation.fileLocationUnavailable(volumeId: _1!, localId: _2!, secret: _3!) - } - else { - return nil - } - } - fileprivate static func parse_fileLocation(_ reader: BufferReader) -> FileLocation? { + fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { var _1: Int32? _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int64? - _4 = reader.readInt64() let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return SecretApi46.FileLocation.fileLocation(dcId: _1!, volumeId: _2!, localId: _3!, secret: _4!) + if _c1 { + return SecretApi46.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi46.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi46.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionRequestKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.DecryptedMessageAction.decryptedMessageActionRequestKey(exchangeId: _1!, gA: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi46.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi46.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi46.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionTyping(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: SecretApi46.SendMessageAction? + if let signature = reader.readInt32() { + _1 = SecretApi46.parse(reader, signature: signature) as? SecretApi46.SendMessageAction + } + let _c1 = _1 != nil + if _c1 { + return SecretApi46.DecryptedMessageAction.decryptedMessageActionTyping(action: _1!) } else { return nil } } - } public enum DecryptedMessageLayer { case decryptedMessageLayer(randomBytes: Buffer, layer: Int32, inSeqNo: Int32, outSeqNo: Int32, message: SecretApi46.DecryptedMessage) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): - if boxed { - buffer.appendInt32(467867529) - } - serializeBytes(randomBytes, buffer: buffer, boxed: false) - serializeInt32(layer, buffer: buffer, boxed: false) - serializeInt32(inSeqNo, buffer: buffer, boxed: false) - serializeInt32(outSeqNo, buffer: buffer, boxed: false) - message.serialize(buffer, true) - break - } - } - + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): + if boxed { + buffer.appendInt32(467867529) + } + serializeBytes(randomBytes, buffer: buffer, boxed: false) + serializeInt32(layer, buffer: buffer, boxed: false) + serializeInt32(inSeqNo, buffer: buffer, boxed: false) + serializeInt32(outSeqNo, buffer: buffer, boxed: false) + message.serialize(buffer, true) + break + } + } + fileprivate static func parse_decryptedMessageLayer(_ reader: BufferReader) -> DecryptedMessageLayer? { var _1: Buffer? _1 = parseBytes(reader) @@ -585,682 +583,156 @@ public struct SecretApi46 { return nil } } - } - public enum DecryptedMessage { - case decryptedMessageService(randomId: Int64, action: SecretApi46.DecryptedMessageAction) - case decryptedMessage(flags: Int32, randomId: Int64, ttl: Int32, message: String, media: SecretApi46.DecryptedMessageMedia?, entities: [SecretApi46.MessageEntity]?, viaBotName: String?, replyToRandomId: Int64?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageService(let randomId, let action): - if boxed { - buffer.appendInt32(1930838368) - } - serializeInt64(randomId, buffer: buffer, boxed: false) - action.serialize(buffer, true) - break - case .decryptedMessage(let flags, let randomId, let ttl, let message, let media, let entities, let viaBotName, let replyToRandomId): - if boxed { - buffer.appendInt32(917541342) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(randomId, buffer: buffer, boxed: false) - serializeInt32(ttl, buffer: buffer, boxed: false) - serializeString(message, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 9) != 0 {media!.serialize(buffer, true)} - if Int(flags) & Int(1 << 7) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(entities!.count)) - for item in entities! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 11) != 0 {serializeString(viaBotName!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {serializeInt64(replyToRandomId!, buffer: buffer, boxed: false)} - break - } - } - - fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { - var _1: Int64? - _1 = reader.readInt64() - var _2: SecretApi46.DecryptedMessageAction? - if let signature = reader.readInt32() { - _2 = SecretApi46.parse(reader, signature: signature) as? SecretApi46.DecryptedMessageAction - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) - } - else { - return nil + public enum DecryptedMessageMedia { + case decryptedMessageMediaAudio(duration: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) + case decryptedMessageMediaDocument(thumb: Buffer, thumbW: Int32, thumbH: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer, attributes: [SecretApi46.DocumentAttribute], caption: String) + case decryptedMessageMediaEmpty + case decryptedMessageMediaExternalDocument(id: Int64, accessHash: Int64, date: Int32, mimeType: String, size: Int32, thumb: SecretApi46.PhotoSize, dcId: Int32, attributes: [SecretApi46.DocumentAttribute]) + case decryptedMessageMediaGeoPoint(lat: Double, long: Double) + case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaVenue(lat: Double, long: Double, title: String, address: String, provider: String, venueId: String) + case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaWebPage(url: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(1474341323) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): + if boxed { + buffer.appendInt32(1485441687) + } + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + serializeInt32(userId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let mimeType, let size, let key, let iv, let attributes, let caption): + if boxed { + buffer.appendInt32(2063502050) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaEmpty: + if boxed { + buffer.appendInt32(144661578) + } + break + case .decryptedMessageMediaExternalDocument(let id, let accessHash, let date, let mimeType, let size, let thumb, let dcId, let attributes): + if boxed { + buffer.appendInt32(-90853155) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + thumb.serialize(buffer, true) + serializeInt32(dcId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + break + case .decryptedMessageMediaGeoPoint(let lat, let long): + if boxed { + buffer.appendInt32(893913689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-235238024) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVenue(let lat, let long, let title, let address, let provider, let venueId): + if boxed { + buffer.appendInt32(-1978796689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(address, buffer: buffer, boxed: false) + serializeString(provider, buffer: buffer, boxed: false) + serializeString(venueId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-1760785394) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaWebPage(let url): + if boxed { + buffer.appendInt32(-452652584) + } + serializeString(url, buffer: buffer, boxed: false) + break } } - fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { + + fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: Int32? _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() + var _2: String? + _2 = parseString(reader) var _3: Int32? _3 = reader.readInt32() - var _4: String? - _4 = parseString(reader) - var _5: SecretApi46.DecryptedMessageMedia? - if Int(_1!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { - _5 = SecretApi46.parse(reader, signature: signature) as? SecretApi46.DecryptedMessageMedia - } } - var _6: [SecretApi46.MessageEntity]? - if Int(_1!) & Int(1 << 7) != 0 {if let _ = reader.readInt32() { - _6 = SecretApi46.parseVector(reader, elementSignature: 0, elementType: SecretApi46.MessageEntity.self) - } } - var _7: String? - if Int(_1!) & Int(1 << 11) != 0 {_7 = parseString(reader) } - var _8: Int64? - if Int(_1!) & Int(1 << 3) != 0 {_8 = reader.readInt64() } + var _4: Buffer? + _4 = parseBytes(reader) + var _5: Buffer? + _5 = parseBytes(reader) let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 9) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 7) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return SecretApi46.DecryptedMessage.decryptedMessage(flags: _1!, randomId: _2!, ttl: _3!, message: _4!, media: _5, entities: _6, viaBotName: _7, replyToRandomId: _8) - } - else { - return nil - } - } - - } - - public enum DocumentAttribute { - case documentAttributeImageSize(w: Int32, h: Int32) - case documentAttributeAnimated - case documentAttributeVideo(duration: Int32, w: Int32, h: Int32) - case documentAttributeFilename(fileName: String) - case documentAttributeSticker(alt: String, stickerset: SecretApi46.InputStickerSet) - case documentAttributeAudio(flags: Int32, duration: Int32, title: String?, performer: String?, waveform: Buffer?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .documentAttributeImageSize(let w, let h): - if boxed { - buffer.appendInt32(1815593308) - } - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - break - case .documentAttributeAnimated: - if boxed { - buffer.appendInt32(297109817) - } - - break - case .documentAttributeVideo(let duration, let w, let h): - if boxed { - buffer.appendInt32(1494273227) - } - serializeInt32(duration, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - break - case .documentAttributeFilename(let fileName): - if boxed { - buffer.appendInt32(358154344) - } - serializeString(fileName, buffer: buffer, boxed: false) - break - case .documentAttributeSticker(let alt, let stickerset): - if boxed { - buffer.appendInt32(978674434) - } - serializeString(alt, buffer: buffer, boxed: false) - stickerset.serialize(buffer, true) - break - case .documentAttributeAudio(let flags, let duration, let title, let performer, let waveform): - if boxed { - buffer.appendInt32(-1739392570) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(duration, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(title!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(performer!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {serializeBytes(waveform!, buffer: buffer, boxed: false)} - break - } - } - - fileprivate static func parse_documentAttributeImageSize(_ reader: BufferReader) -> DocumentAttribute? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.DocumentAttribute.documentAttributeImageSize(w: _1!, h: _2!) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeAnimated(_ reader: BufferReader) -> DocumentAttribute? { - return SecretApi46.DocumentAttribute.documentAttributeAnimated - } - fileprivate static func parse_documentAttributeVideo(_ reader: BufferReader) -> DocumentAttribute? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi46.DocumentAttribute.documentAttributeVideo(duration: _1!, w: _2!, h: _3!) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeFilename(_ reader: BufferReader) -> DocumentAttribute? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return SecretApi46.DocumentAttribute.documentAttributeFilename(fileName: _1!) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeSticker(_ reader: BufferReader) -> DocumentAttribute? { - var _1: String? - _1 = parseString(reader) - var _2: SecretApi46.InputStickerSet? - if let signature = reader.readInt32() { - _2 = SecretApi46.parse(reader, signature: signature) as? SecretApi46.InputStickerSet - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.DocumentAttribute.documentAttributeSticker(alt: _1!, stickerset: _2!) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeAudio(_ reader: BufferReader) -> DocumentAttribute? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } - var _4: String? - if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } - var _5: Buffer? - if Int(_1!) & Int(1 << 2) != 0 {_5 = parseBytes(reader) } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + let _c5 = _5 != nil if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi46.DocumentAttribute.documentAttributeAudio(flags: _1!, duration: _2!, title: _3, performer: _4, waveform: _5) - } - else { - return nil - } - } - - } - - public enum InputStickerSet { - case inputStickerSetShortName(shortName: String) - case inputStickerSetEmpty - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputStickerSetShortName(let shortName): - if boxed { - buffer.appendInt32(-2044933984) - } - serializeString(shortName, buffer: buffer, boxed: false) - break - case .inputStickerSetEmpty: - if boxed { - buffer.appendInt32(-4838507) - } - - break - } - } - - fileprivate static func parse_inputStickerSetShortName(_ reader: BufferReader) -> InputStickerSet? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return SecretApi46.InputStickerSet.inputStickerSetShortName(shortName: _1!) - } - else { - return nil - } - } - fileprivate static func parse_inputStickerSetEmpty(_ reader: BufferReader) -> InputStickerSet? { - return SecretApi46.InputStickerSet.inputStickerSetEmpty - } - - } - - public enum MessageEntity { - case messageEntityUnknown(offset: Int32, length: Int32) - case messageEntityMention(offset: Int32, length: Int32) - case messageEntityHashtag(offset: Int32, length: Int32) - case messageEntityBotCommand(offset: Int32, length: Int32) - case messageEntityUrl(offset: Int32, length: Int32) - case messageEntityEmail(offset: Int32, length: Int32) - case messageEntityBold(offset: Int32, length: Int32) - case messageEntityItalic(offset: Int32, length: Int32) - case messageEntityCode(offset: Int32, length: Int32) - case messageEntityPre(offset: Int32, length: Int32, language: String) - case messageEntityTextUrl(offset: Int32, length: Int32, url: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .messageEntityUnknown(let offset, let length): - if boxed { - buffer.appendInt32(-1148011883) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityMention(let offset, let length): - if boxed { - buffer.appendInt32(-100378723) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityHashtag(let offset, let length): - if boxed { - buffer.appendInt32(1868782349) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityBotCommand(let offset, let length): - if boxed { - buffer.appendInt32(1827637959) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityUrl(let offset, let length): - if boxed { - buffer.appendInt32(1859134776) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityEmail(let offset, let length): - if boxed { - buffer.appendInt32(1692693954) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityBold(let offset, let length): - if boxed { - buffer.appendInt32(-1117713463) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityItalic(let offset, let length): - if boxed { - buffer.appendInt32(-2106619040) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityCode(let offset, let length): - if boxed { - buffer.appendInt32(681706865) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityPre(let offset, let length, let language): - if boxed { - buffer.appendInt32(1938967520) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - serializeString(language, buffer: buffer, boxed: false) - break - case .messageEntityTextUrl(let offset, let length, let url): - if boxed { - buffer.appendInt32(1990644519) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - serializeString(url, buffer: buffer, boxed: false) - break - } - } - - fileprivate static func parse_messageEntityUnknown(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.MessageEntity.messageEntityUnknown(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityMention(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.MessageEntity.messageEntityMention(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityHashtag(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.MessageEntity.messageEntityHashtag(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityBotCommand(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.MessageEntity.messageEntityBotCommand(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityUrl(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.MessageEntity.messageEntityUrl(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.MessageEntity.messageEntityEmail(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityBold(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.MessageEntity.messageEntityBold(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityItalic(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.MessageEntity.messageEntityItalic(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityCode(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.MessageEntity.messageEntityCode(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityPre(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi46.MessageEntity.messageEntityPre(offset: _1!, length: _2!, language: _3!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityTextUrl(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi46.MessageEntity.messageEntityTextUrl(offset: _1!, length: _2!, url: _3!) - } - else { - return nil - } - } - - } - - public enum DecryptedMessageMedia { - case decryptedMessageMediaEmpty - case decryptedMessageMediaGeoPoint(lat: Double, long: Double) - case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) - case decryptedMessageMediaAudio(duration: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer) - case decryptedMessageMediaExternalDocument(id: Int64, accessHash: Int64, date: Int32, mimeType: String, size: Int32, thumb: SecretApi46.PhotoSize, dcId: Int32, attributes: [SecretApi46.DocumentAttribute]) - case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) - case decryptedMessageMediaDocument(thumb: Buffer, thumbW: Int32, thumbH: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer, attributes: [SecretApi46.DocumentAttribute], caption: String) - case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) - case decryptedMessageMediaVenue(lat: Double, long: Double, title: String, address: String, provider: String, venueId: String) - case decryptedMessageMediaWebPage(url: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageMediaEmpty: - if boxed { - buffer.appendInt32(144661578) - } - - break - case .decryptedMessageMediaGeoPoint(let lat, let long): - if boxed { - buffer.appendInt32(893913689) - } - serializeDouble(lat, buffer: buffer, boxed: false) - serializeDouble(long, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): - if boxed { - buffer.appendInt32(1485441687) - } - serializeString(phoneNumber, buffer: buffer, boxed: false) - serializeString(firstName, buffer: buffer, boxed: false) - serializeString(lastName, buffer: buffer, boxed: false) - serializeInt32(userId, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): - if boxed { - buffer.appendInt32(1474341323) - } - serializeInt32(duration, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaExternalDocument(let id, let accessHash, let date, let mimeType, let size, let thumb, let dcId, let attributes): - if boxed { - buffer.appendInt32(-90853155) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - thumb.serialize(buffer, true) - serializeInt32(dcId, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(attributes.count)) - for item in attributes { - item.serialize(buffer, true) - } - break - case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv, let caption): - if boxed { - buffer.appendInt32(-235238024) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - serializeString(caption, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let mimeType, let size, let key, let iv, let attributes, let caption): - if boxed { - buffer.appendInt32(2063502050) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(attributes.count)) - for item in attributes { - item.serialize(buffer, true) - } - serializeString(caption, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv, let caption): - if boxed { - buffer.appendInt32(-1760785394) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeInt32(duration, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - serializeString(caption, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaVenue(let lat, let long, let title, let address, let provider, let venueId): - if boxed { - buffer.appendInt32(-1978796689) - } - serializeDouble(lat, buffer: buffer, boxed: false) - serializeDouble(long, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - serializeString(address, buffer: buffer, boxed: false) - serializeString(provider, buffer: buffer, boxed: false) - serializeString(venueId, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaWebPage(let url): - if boxed { - buffer.appendInt32(-452652584) - } - serializeString(url, buffer: buffer, boxed: false) - break - } - } - - fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { - return SecretApi46.DecryptedMessageMedia.decryptedMessageMediaEmpty - } - fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Double? - _1 = reader.readDouble() - var _2: Double? - _2 = reader.readDouble() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi46.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + return SecretApi46.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) } else { return nil @@ -1286,29 +758,46 @@ public struct SecretApi46 { return nil } } - fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) + fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() var _3: Int32? _3 = reader.readInt32() - var _4: Buffer? - _4 = parseBytes(reader) - var _5: Buffer? - _5 = parseBytes(reader) + var _4: String? + _4 = parseString(reader) + var _5: Int32? + _5 = reader.readInt32() + var _6: Buffer? + _6 = parseBytes(reader) + var _7: Buffer? + _7 = parseBytes(reader) + var _8: [SecretApi46.DocumentAttribute]? + if let _ = reader.readInt32() { + _8 = SecretApi46.parseVector(reader, elementSignature: 0, elementType: SecretApi46.DocumentAttribute.self) + } + var _9: String? + _9 = parseString(reader) let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi46.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi46.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, mimeType: _4!, size: _5!, key: _6!, iv: _7!, attributes: _8!, caption: _9!) } else { return nil } } + fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { + return SecretApi46.DecryptedMessageMedia.decryptedMessageMediaEmpty + } fileprivate static func parse_decryptedMessageMediaExternalDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: Int64? _1 = reader.readInt64() @@ -1345,6 +834,20 @@ public struct SecretApi46 { return nil } } + fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + } + else { + return nil + } + } fileprivate static func parse_decryptedMessageMediaPhoto(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: Buffer? _1 = parseBytes(reader) @@ -1380,38 +883,27 @@ public struct SecretApi46 { return nil } } - fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Buffer? - _1 = parseBytes(reader) - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() + fileprivate static func parse_decryptedMessageMediaVenue(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + var _3: String? + _3 = parseString(reader) var _4: String? _4 = parseString(reader) - var _5: Int32? - _5 = reader.readInt32() - var _6: Buffer? - _6 = parseBytes(reader) - var _7: Buffer? - _7 = parseBytes(reader) - var _8: [SecretApi46.DocumentAttribute]? - if let _ = reader.readInt32() { - _8 = SecretApi46.parseVector(reader, elementSignature: 0, elementType: SecretApi46.DocumentAttribute.self) - } - var _9: String? - _9 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: String? + _6 = parseString(reader) let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return SecretApi46.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, mimeType: _4!, size: _5!, key: _6!, iv: _7!, attributes: _8!, caption: _9!) + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return SecretApi46.DecryptedMessageMedia.decryptedMessageMediaVenue(lat: _1!, long: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!) } else { return nil @@ -1458,32 +950,6 @@ public struct SecretApi46 { return nil } } - fileprivate static func parse_decryptedMessageMediaVenue(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Double? - _1 = reader.readDouble() - var _2: Double? - _2 = reader.readDouble() - var _3: String? - _3 = parseString(reader) - var _4: String? - _4 = parseString(reader) - var _5: String? - _5 = parseString(reader) - var _6: String? - _6 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return SecretApi46.DecryptedMessageMedia.decryptedMessageMediaVenue(lat: _1!, long: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!) - } - else { - return nil - } - } fileprivate static func parse_decryptedMessageMediaWebPage(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: String? _1 = parseString(reader) @@ -1495,11 +961,718 @@ public struct SecretApi46 { return nil } } - } - public struct functions { - + public enum DocumentAttribute { + case documentAttributeAnimated + case documentAttributeAudio(flags: Int32, duration: Int32, title: String?, performer: String?, waveform: Buffer?) + case documentAttributeFilename(fileName: String) + case documentAttributeImageSize(w: Int32, h: Int32) + case documentAttributeSticker(alt: String, stickerset: SecretApi46.InputStickerSet) + case documentAttributeVideo(duration: Int32, w: Int32, h: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .documentAttributeAnimated: + if boxed { + buffer.appendInt32(297109817) + } + break + case .documentAttributeAudio(let flags, let duration, let title, let performer, let waveform): + if boxed { + buffer.appendInt32(-1739392570) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 { + serializeString(title!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 1) != 0 { + serializeString(performer!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 2) != 0 { + serializeBytes(waveform!, buffer: buffer, boxed: false) + } + break + case .documentAttributeFilename(let fileName): + if boxed { + buffer.appendInt32(358154344) + } + serializeString(fileName, buffer: buffer, boxed: false) + break + case .documentAttributeImageSize(let w, let h): + if boxed { + buffer.appendInt32(1815593308) + } + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + case .documentAttributeSticker(let alt, let stickerset): + if boxed { + buffer.appendInt32(978674434) + } + serializeString(alt, buffer: buffer, boxed: false) + stickerset.serialize(buffer, true) + break + case .documentAttributeVideo(let duration, let w, let h): + if boxed { + buffer.appendInt32(1494273227) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_documentAttributeAnimated(_ reader: BufferReader) -> DocumentAttribute? { + return SecretApi46.DocumentAttribute.documentAttributeAnimated + } + fileprivate static func parse_documentAttributeAudio(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _3 = parseString(reader) + } + var _4: String? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _4 = parseString(reader) + } + var _5: Buffer? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _5 = parseBytes(reader) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi46.DocumentAttribute.documentAttributeAudio(flags: _1!, duration: _2!, title: _3, performer: _4, waveform: _5) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeFilename(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi46.DocumentAttribute.documentAttributeFilename(fileName: _1!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeImageSize(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.DocumentAttribute.documentAttributeImageSize(w: _1!, h: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeSticker(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi46.InputStickerSet? + if let signature = reader.readInt32() { + _2 = SecretApi46.parse(reader, signature: signature) as? SecretApi46.InputStickerSet + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.DocumentAttribute.documentAttributeSticker(alt: _1!, stickerset: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeVideo(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi46.DocumentAttribute.documentAttributeVideo(duration: _1!, w: _2!, h: _3!) + } + else { + return nil + } + } + } + + public enum FileLocation { + case fileLocation(dcId: Int32, volumeId: Int64, localId: Int32, secret: Int64) + case fileLocationUnavailable(volumeId: Int64, localId: Int32, secret: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .fileLocation(let dcId, let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(1406570614) + } + serializeInt32(dcId, buffer: buffer, boxed: false) + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + case .fileLocationUnavailable(let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(2086234950) + } + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_fileLocation(_ reader: BufferReader) -> FileLocation? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int64? + _4 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi46.FileLocation.fileLocation(dcId: _1!, volumeId: _2!, localId: _3!, secret: _4!) + } + else { + return nil + } + } + fileprivate static func parse_fileLocationUnavailable(_ reader: BufferReader) -> FileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi46.FileLocation.fileLocationUnavailable(volumeId: _1!, localId: _2!, secret: _3!) + } + else { + return nil + } + } + } + + public enum InputStickerSet { + case inputStickerSetEmpty + case inputStickerSetShortName(shortName: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputStickerSetEmpty: + if boxed { + buffer.appendInt32(-4838507) + } + break + case .inputStickerSetShortName(let shortName): + if boxed { + buffer.appendInt32(-2044933984) + } + serializeString(shortName, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_inputStickerSetEmpty(_ reader: BufferReader) -> InputStickerSet? { + return SecretApi46.InputStickerSet.inputStickerSetEmpty + } + fileprivate static func parse_inputStickerSetShortName(_ reader: BufferReader) -> InputStickerSet? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi46.InputStickerSet.inputStickerSetShortName(shortName: _1!) + } + else { + return nil + } + } + } + + public enum MessageEntity { + case messageEntityBold(offset: Int32, length: Int32) + case messageEntityBotCommand(offset: Int32, length: Int32) + case messageEntityCode(offset: Int32, length: Int32) + case messageEntityEmail(offset: Int32, length: Int32) + case messageEntityHashtag(offset: Int32, length: Int32) + case messageEntityItalic(offset: Int32, length: Int32) + case messageEntityMention(offset: Int32, length: Int32) + case messageEntityPre(offset: Int32, length: Int32, language: String) + case messageEntityTextUrl(offset: Int32, length: Int32, url: String) + case messageEntityUnknown(offset: Int32, length: Int32) + case messageEntityUrl(offset: Int32, length: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageEntityBold(let offset, let length): + if boxed { + buffer.appendInt32(-1117713463) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityBotCommand(let offset, let length): + if boxed { + buffer.appendInt32(1827637959) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityCode(let offset, let length): + if boxed { + buffer.appendInt32(681706865) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityEmail(let offset, let length): + if boxed { + buffer.appendInt32(1692693954) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityHashtag(let offset, let length): + if boxed { + buffer.appendInt32(1868782349) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityItalic(let offset, let length): + if boxed { + buffer.appendInt32(-2106619040) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityMention(let offset, let length): + if boxed { + buffer.appendInt32(-100378723) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityPre(let offset, let length, let language): + if boxed { + buffer.appendInt32(1938967520) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(language, buffer: buffer, boxed: false) + break + case .messageEntityTextUrl(let offset, let length, let url): + if boxed { + buffer.appendInt32(1990644519) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + break + case .messageEntityUnknown(let offset, let length): + if boxed { + buffer.appendInt32(-1148011883) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityUrl(let offset, let length): + if boxed { + buffer.appendInt32(1859134776) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_messageEntityBold(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.MessageEntity.messageEntityBold(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityBotCommand(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.MessageEntity.messageEntityBotCommand(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityCode(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.MessageEntity.messageEntityCode(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.MessageEntity.messageEntityEmail(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityHashtag(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.MessageEntity.messageEntityHashtag(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityItalic(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.MessageEntity.messageEntityItalic(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityMention(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.MessageEntity.messageEntityMention(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityPre(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi46.MessageEntity.messageEntityPre(offset: _1!, length: _2!, language: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityTextUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi46.MessageEntity.messageEntityTextUrl(offset: _1!, length: _2!, url: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUnknown(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.MessageEntity.messageEntityUnknown(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi46.MessageEntity.messageEntityUrl(offset: _1!, length: _2!) + } + else { + return nil + } + } + } + + public enum PhotoSize { + case photoCachedSize(type: String, location: SecretApi46.FileLocation, w: Int32, h: Int32, bytes: Buffer) + case photoSize(type: String, location: SecretApi46.FileLocation, w: Int32, h: Int32, size: Int32) + case photoSizeEmpty(type: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .photoCachedSize(let type, let location, let w, let h, let bytes): + if boxed { + buffer.appendInt32(-374917894) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeBytes(bytes, buffer: buffer, boxed: false) + break + case .photoSize(let type, let location, let w, let h, let size): + if boxed { + buffer.appendInt32(2009052699) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + break + case .photoSizeEmpty(let type): + if boxed { + buffer.appendInt32(236446268) + } + serializeString(type, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_photoCachedSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi46.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi46.parse(reader, signature: signature) as? SecretApi46.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi46.PhotoSize.photoCachedSize(type: _1!, location: _2!, w: _3!, h: _4!, bytes: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi46.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi46.parse(reader, signature: signature) as? SecretApi46.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi46.PhotoSize.photoSize(type: _1!, location: _2!, w: _3!, h: _4!, size: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSizeEmpty(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi46.PhotoSize.photoSizeEmpty(type: _1!) + } + else { + return nil + } + } + } + + public enum SendMessageAction { + case sendMessageCancelAction + case sendMessageChooseContactAction + case sendMessageGeoLocationAction + case sendMessageRecordAudioAction + case sendMessageRecordVideoAction + case sendMessageTypingAction + case sendMessageUploadAudioAction + case sendMessageUploadDocumentAction + case sendMessageUploadPhotoAction + case sendMessageUploadVideoAction + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sendMessageCancelAction: + if boxed { + buffer.appendInt32(-44119819) + } + break + case .sendMessageChooseContactAction: + if boxed { + buffer.appendInt32(1653390447) + } + break + case .sendMessageGeoLocationAction: + if boxed { + buffer.appendInt32(393186209) + } + break + case .sendMessageRecordAudioAction: + if boxed { + buffer.appendInt32(-718310409) + } + break + case .sendMessageRecordVideoAction: + if boxed { + buffer.appendInt32(-1584933265) + } + break + case .sendMessageTypingAction: + if boxed { + buffer.appendInt32(381645902) + } + break + case .sendMessageUploadAudioAction: + if boxed { + buffer.appendInt32(-424899985) + } + break + case .sendMessageUploadDocumentAction: + if boxed { + buffer.appendInt32(-1884362354) + } + break + case .sendMessageUploadPhotoAction: + if boxed { + buffer.appendInt32(-1727382502) + } + break + case .sendMessageUploadVideoAction: + if boxed { + buffer.appendInt32(-1845219337) + } + break + } + } + + fileprivate static func parse_sendMessageCancelAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi46.SendMessageAction.sendMessageCancelAction + } + fileprivate static func parse_sendMessageChooseContactAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi46.SendMessageAction.sendMessageChooseContactAction + } + fileprivate static func parse_sendMessageGeoLocationAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi46.SendMessageAction.sendMessageGeoLocationAction + } + fileprivate static func parse_sendMessageRecordAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi46.SendMessageAction.sendMessageRecordAudioAction + } + fileprivate static func parse_sendMessageRecordVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi46.SendMessageAction.sendMessageRecordVideoAction + } + fileprivate static func parse_sendMessageTypingAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi46.SendMessageAction.sendMessageTypingAction + } + fileprivate static func parse_sendMessageUploadAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi46.SendMessageAction.sendMessageUploadAudioAction + } + fileprivate static func parse_sendMessageUploadDocumentAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi46.SendMessageAction.sendMessageUploadDocumentAction + } + fileprivate static func parse_sendMessageUploadPhotoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi46.SendMessageAction.sendMessageUploadPhotoAction + } + fileprivate static func parse_sendMessageUploadVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi46.SendMessageAction.sendMessageUploadVideoAction + } } } diff --git a/submodules/TelegramApi/Sources/SecretApiLayer66.swift b/submodules/TelegramApi/Sources/SecretApiLayer66.swift new file mode 100644 index 0000000000..5a9957d87a --- /dev/null +++ b/submodules/TelegramApi/Sources/SecretApiLayer66.swift @@ -0,0 +1,1702 @@ + +fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { + var dict: [Int32 : (BufferReader) -> Any?] = [:] + dict[-1471112230] = { return $0.readInt32() } + dict[570911930] = { return $0.readInt64() } + dict[571523412] = { return $0.readDouble() } + dict[-1255641564] = { return parseString($0) } + dict[-1132882121] = { return SecretApi66.Bool.parse_boolFalse($0) } + dict[-1720552011] = { return SecretApi66.Bool.parse_boolTrue($0) } + dict[917541342] = { return SecretApi66.DecryptedMessage.parse_decryptedMessage($0) } + dict[1930838368] = { return SecretApi66.DecryptedMessage.parse_decryptedMessageService($0) } + dict[-586814357] = { return SecretApi66.DecryptedMessageAction.parse_decryptedMessageActionAbortKey($0) } + dict[1877046107] = { return SecretApi66.DecryptedMessageAction.parse_decryptedMessageActionAcceptKey($0) } + dict[-332526693] = { return SecretApi66.DecryptedMessageAction.parse_decryptedMessageActionCommitKey($0) } + dict[1700872964] = { return SecretApi66.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } + dict[1729750108] = { return SecretApi66.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } + dict[-1473258141] = { return SecretApi66.DecryptedMessageAction.parse_decryptedMessageActionNoop($0) } + dict[-217806717] = { return SecretApi66.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } + dict[206520510] = { return SecretApi66.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } + dict[-204906213] = { return SecretApi66.DecryptedMessageAction.parse_decryptedMessageActionRequestKey($0) } + dict[1360072880] = { return SecretApi66.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } + dict[-1967000459] = { return SecretApi66.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } + dict[-1586283796] = { return SecretApi66.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } + dict[-860719551] = { return SecretApi66.DecryptedMessageAction.parse_decryptedMessageActionTyping($0) } + dict[467867529] = { return SecretApi66.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } + dict[1474341323] = { return SecretApi66.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } + dict[1485441687] = { return SecretApi66.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } + dict[2063502050] = { return SecretApi66.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } + dict[144661578] = { return SecretApi66.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } + dict[-90853155] = { return SecretApi66.DecryptedMessageMedia.parse_decryptedMessageMediaExternalDocument($0) } + dict[893913689] = { return SecretApi66.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } + dict[-235238024] = { return SecretApi66.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } + dict[-1978796689] = { return SecretApi66.DecryptedMessageMedia.parse_decryptedMessageMediaVenue($0) } + dict[-1760785394] = { return SecretApi66.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } + dict[-452652584] = { return SecretApi66.DecryptedMessageMedia.parse_decryptedMessageMediaWebPage($0) } + dict[297109817] = { return SecretApi66.DocumentAttribute.parse_documentAttributeAnimated($0) } + dict[-1739392570] = { return SecretApi66.DocumentAttribute.parse_documentAttributeAudio($0) } + dict[358154344] = { return SecretApi66.DocumentAttribute.parse_documentAttributeFilename($0) } + dict[1815593308] = { return SecretApi66.DocumentAttribute.parse_documentAttributeImageSize($0) } + dict[978674434] = { return SecretApi66.DocumentAttribute.parse_documentAttributeSticker($0) } + dict[250621158] = { return SecretApi66.DocumentAttribute.parse_documentAttributeVideo($0) } + dict[1406570614] = { return SecretApi66.FileLocation.parse_fileLocation($0) } + dict[2086234950] = { return SecretApi66.FileLocation.parse_fileLocationUnavailable($0) } + dict[-4838507] = { return SecretApi66.InputStickerSet.parse_inputStickerSetEmpty($0) } + dict[-2044933984] = { return SecretApi66.InputStickerSet.parse_inputStickerSetShortName($0) } + dict[-1117713463] = { return SecretApi66.MessageEntity.parse_messageEntityBold($0) } + dict[1827637959] = { return SecretApi66.MessageEntity.parse_messageEntityBotCommand($0) } + dict[681706865] = { return SecretApi66.MessageEntity.parse_messageEntityCode($0) } + dict[1692693954] = { return SecretApi66.MessageEntity.parse_messageEntityEmail($0) } + dict[1868782349] = { return SecretApi66.MessageEntity.parse_messageEntityHashtag($0) } + dict[-2106619040] = { return SecretApi66.MessageEntity.parse_messageEntityItalic($0) } + dict[-100378723] = { return SecretApi66.MessageEntity.parse_messageEntityMention($0) } + dict[1938967520] = { return SecretApi66.MessageEntity.parse_messageEntityPre($0) } + dict[1990644519] = { return SecretApi66.MessageEntity.parse_messageEntityTextUrl($0) } + dict[-1148011883] = { return SecretApi66.MessageEntity.parse_messageEntityUnknown($0) } + dict[1859134776] = { return SecretApi66.MessageEntity.parse_messageEntityUrl($0) } + dict[-374917894] = { return SecretApi66.PhotoSize.parse_photoCachedSize($0) } + dict[2009052699] = { return SecretApi66.PhotoSize.parse_photoSize($0) } + dict[236446268] = { return SecretApi66.PhotoSize.parse_photoSizeEmpty($0) } + dict[-44119819] = { return SecretApi66.SendMessageAction.parse_sendMessageCancelAction($0) } + dict[1653390447] = { return SecretApi66.SendMessageAction.parse_sendMessageChooseContactAction($0) } + dict[393186209] = { return SecretApi66.SendMessageAction.parse_sendMessageGeoLocationAction($0) } + dict[-718310409] = { return SecretApi66.SendMessageAction.parse_sendMessageRecordAudioAction($0) } + dict[-1997373508] = { return SecretApi66.SendMessageAction.parse_sendMessageRecordRoundAction($0) } + dict[-1584933265] = { return SecretApi66.SendMessageAction.parse_sendMessageRecordVideoAction($0) } + dict[381645902] = { return SecretApi66.SendMessageAction.parse_sendMessageTypingAction($0) } + dict[-424899985] = { return SecretApi66.SendMessageAction.parse_sendMessageUploadAudioAction($0) } + dict[-1884362354] = { return SecretApi66.SendMessageAction.parse_sendMessageUploadDocumentAction($0) } + dict[-1727382502] = { return SecretApi66.SendMessageAction.parse_sendMessageUploadPhotoAction($0) } + dict[-1150187996] = { return SecretApi66.SendMessageAction.parse_sendMessageUploadRoundAction($0) } + dict[-1845219337] = { return SecretApi66.SendMessageAction.parse_sendMessageUploadVideoAction($0) } + return dict +}() + +public struct SecretApi66 { + public static func parse(_ buffer: Buffer) -> Any? { + let reader = BufferReader(buffer) + if let signature = reader.readInt32() { + return parse(reader, signature: signature) + } + return nil + } + + fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { + if let parser = parsers[signature] { + return parser(reader) + } + else { + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + return nil + } + } + + fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { + if let count = reader.readInt32() { + var array = [T]() + var i: Int32 = 0 + while i < count { + var signature = elementSignature + if elementSignature == 0 { + if let unboxedSignature = reader.readInt32() { + signature = unboxedSignature + } + else { + return nil + } + } + if let item = SecretApi66.parse(reader, signature: signature) as? T { + array.append(item) + } + else { + return nil + } + i += 1 + } + return array + } + return nil + } + + public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) { + switch object { + case let _1 as SecretApi66.Bool: + _1.serialize(buffer, boxed) + case let _1 as SecretApi66.DecryptedMessage: + _1.serialize(buffer, boxed) + case let _1 as SecretApi66.DecryptedMessageAction: + _1.serialize(buffer, boxed) + case let _1 as SecretApi66.DecryptedMessageLayer: + _1.serialize(buffer, boxed) + case let _1 as SecretApi66.DecryptedMessageMedia: + _1.serialize(buffer, boxed) + case let _1 as SecretApi66.DocumentAttribute: + _1.serialize(buffer, boxed) + case let _1 as SecretApi66.FileLocation: + _1.serialize(buffer, boxed) + case let _1 as SecretApi66.InputStickerSet: + _1.serialize(buffer, boxed) + case let _1 as SecretApi66.MessageEntity: + _1.serialize(buffer, boxed) + case let _1 as SecretApi66.PhotoSize: + _1.serialize(buffer, boxed) + case let _1 as SecretApi66.SendMessageAction: + _1.serialize(buffer, boxed) + default: + break + } + } + + public enum Bool { + case boolFalse + case boolTrue + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boolFalse: + if boxed { + buffer.appendInt32(-1132882121) + } + break + case .boolTrue: + if boxed { + buffer.appendInt32(-1720552011) + } + break + } + } + + fileprivate static func parse_boolFalse(_ reader: BufferReader) -> Bool? { + return SecretApi66.Bool.boolFalse + } + fileprivate static func parse_boolTrue(_ reader: BufferReader) -> Bool? { + return SecretApi66.Bool.boolTrue + } + } + + public enum DecryptedMessage { + case decryptedMessage(flags: Int32, randomId: Int64, ttl: Int32, message: String, media: SecretApi66.DecryptedMessageMedia?, entities: [SecretApi66.MessageEntity]?, viaBotName: String?, replyToRandomId: Int64?) + case decryptedMessageService(randomId: Int64, action: SecretApi66.DecryptedMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessage(let flags, let randomId, let ttl, let message, let media, let entities, let viaBotName, let replyToRandomId): + if boxed { + buffer.appendInt32(917541342) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(randomId, buffer: buffer, boxed: false) + serializeInt32(ttl, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 9) != 0 { + media!.serialize(buffer, true) + } + if Int(flags) & Int(1 << 7) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + } + } + if Int(flags) & Int(1 << 11) != 0 { + serializeString(viaBotName!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 3) != 0 { + serializeInt64(replyToRandomId!, buffer: buffer, boxed: false) + } + break + case .decryptedMessageService(let randomId, let action): + if boxed { + buffer.appendInt32(1930838368) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: SecretApi66.DecryptedMessageMedia? + if Int(_1 ?? 0) & Int(1 << 9) != 0 { + if let signature = reader.readInt32() { + _5 = SecretApi66.parse(reader, signature: signature) as? SecretApi66.DecryptedMessageMedia + } + } + var _6: [SecretApi66.MessageEntity]? + if Int(_1 ?? 0) & Int(1 << 7) != 0 { + if let _ = reader.readInt32() { + _6 = SecretApi66.parseVector(reader, elementSignature: 0, elementType: SecretApi66.MessageEntity.self) + } + } + var _7: String? + if Int(_1 ?? 0) & Int(1 << 11) != 0 { + _7 = parseString(reader) + } + var _8: Int64? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _8 = reader.readInt64() + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi66.DecryptedMessage.decryptedMessage(flags: _1!, randomId: _2!, ttl: _3!, message: _4!, media: _5, entities: _6, viaBotName: _7, replyToRandomId: _8) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: SecretApi66.DecryptedMessageAction? + if let signature = reader.readInt32() { + _2 = SecretApi66.parse(reader, signature: signature) as? SecretApi66.DecryptedMessageAction + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageAction { + case decryptedMessageActionAbortKey(exchangeId: Int64) + case decryptedMessageActionAcceptKey(exchangeId: Int64, gB: Buffer, keyFingerprint: Int64) + case decryptedMessageActionCommitKey(exchangeId: Int64, keyFingerprint: Int64) + case decryptedMessageActionDeleteMessages(randomIds: [Int64]) + case decryptedMessageActionFlushHistory + case decryptedMessageActionNoop + case decryptedMessageActionNotifyLayer(layer: Int32) + case decryptedMessageActionReadMessages(randomIds: [Int64]) + case decryptedMessageActionRequestKey(exchangeId: Int64, gA: Buffer) + case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) + case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) + case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) + case decryptedMessageActionTyping(action: SecretApi66.SendMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageActionAbortKey(let exchangeId): + if boxed { + buffer.appendInt32(-586814357) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + break + case .decryptedMessageActionAcceptKey(let exchangeId, let gB, let keyFingerprint): + if boxed { + buffer.appendInt32(1877046107) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gB, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionCommitKey(let exchangeId, let keyFingerprint): + if boxed { + buffer.appendInt32(-332526693) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionDeleteMessages(let randomIds): + if boxed { + buffer.appendInt32(1700872964) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionFlushHistory: + if boxed { + buffer.appendInt32(1729750108) + } + break + case .decryptedMessageActionNoop: + if boxed { + buffer.appendInt32(-1473258141) + } + break + case .decryptedMessageActionNotifyLayer(let layer): + if boxed { + buffer.appendInt32(-217806717) + } + serializeInt32(layer, buffer: buffer, boxed: false) + break + case .decryptedMessageActionReadMessages(let randomIds): + if boxed { + buffer.appendInt32(206520510) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionRequestKey(let exchangeId, let gA): + if boxed { + buffer.appendInt32(-204906213) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gA, buffer: buffer, boxed: false) + break + case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): + if boxed { + buffer.appendInt32(1360072880) + } + serializeInt32(startSeqNo, buffer: buffer, boxed: false) + serializeInt32(endSeqNo, buffer: buffer, boxed: false) + break + case .decryptedMessageActionScreenshotMessages(let randomIds): + if boxed { + buffer.appendInt32(-1967000459) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionSetMessageTTL(let ttlSeconds): + if boxed { + buffer.appendInt32(-1586283796) + } + serializeInt32(ttlSeconds, buffer: buffer, boxed: false) + break + case .decryptedMessageActionTyping(let action): + if boxed { + buffer.appendInt32(-860719551) + } + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessageActionAbortKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return SecretApi66.DecryptedMessageAction.decryptedMessageActionAbortKey(exchangeId: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionAcceptKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi66.DecryptedMessageAction.decryptedMessageActionAcceptKey(exchangeId: _1!, gB: _2!, keyFingerprint: _3!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionCommitKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.DecryptedMessageAction.decryptedMessageActionCommitKey(exchangeId: _1!, keyFingerprint: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi66.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi66.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi66.DecryptedMessageAction.decryptedMessageActionFlushHistory + } + fileprivate static func parse_decryptedMessageActionNoop(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi66.DecryptedMessageAction.decryptedMessageActionNoop + } + fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi66.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi66.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi66.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionRequestKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.DecryptedMessageAction.decryptedMessageActionRequestKey(exchangeId: _1!, gA: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi66.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi66.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi66.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionTyping(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: SecretApi66.SendMessageAction? + if let signature = reader.readInt32() { + _1 = SecretApi66.parse(reader, signature: signature) as? SecretApi66.SendMessageAction + } + let _c1 = _1 != nil + if _c1 { + return SecretApi66.DecryptedMessageAction.decryptedMessageActionTyping(action: _1!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageLayer { + case decryptedMessageLayer(randomBytes: Buffer, layer: Int32, inSeqNo: Int32, outSeqNo: Int32, message: SecretApi66.DecryptedMessage) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): + if boxed { + buffer.appendInt32(467867529) + } + serializeBytes(randomBytes, buffer: buffer, boxed: false) + serializeInt32(layer, buffer: buffer, boxed: false) + serializeInt32(inSeqNo, buffer: buffer, boxed: false) + serializeInt32(outSeqNo, buffer: buffer, boxed: false) + message.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessageLayer(_ reader: BufferReader) -> DecryptedMessageLayer? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: SecretApi66.DecryptedMessage? + if let signature = reader.readInt32() { + _5 = SecretApi66.parse(reader, signature: signature) as? SecretApi66.DecryptedMessage + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi66.DecryptedMessageLayer.decryptedMessageLayer(randomBytes: _1!, layer: _2!, inSeqNo: _3!, outSeqNo: _4!, message: _5!) + } + else { + return nil + } + } + } + + public enum DecryptedMessageMedia { + case decryptedMessageMediaAudio(duration: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) + case decryptedMessageMediaDocument(thumb: Buffer, thumbW: Int32, thumbH: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer, attributes: [SecretApi66.DocumentAttribute], caption: String) + case decryptedMessageMediaEmpty + case decryptedMessageMediaExternalDocument(id: Int64, accessHash: Int64, date: Int32, mimeType: String, size: Int32, thumb: SecretApi66.PhotoSize, dcId: Int32, attributes: [SecretApi66.DocumentAttribute]) + case decryptedMessageMediaGeoPoint(lat: Double, long: Double) + case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaVenue(lat: Double, long: Double, title: String, address: String, provider: String, venueId: String) + case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaWebPage(url: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(1474341323) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): + if boxed { + buffer.appendInt32(1485441687) + } + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + serializeInt32(userId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let mimeType, let size, let key, let iv, let attributes, let caption): + if boxed { + buffer.appendInt32(2063502050) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaEmpty: + if boxed { + buffer.appendInt32(144661578) + } + break + case .decryptedMessageMediaExternalDocument(let id, let accessHash, let date, let mimeType, let size, let thumb, let dcId, let attributes): + if boxed { + buffer.appendInt32(-90853155) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + thumb.serialize(buffer, true) + serializeInt32(dcId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + break + case .decryptedMessageMediaGeoPoint(let lat, let long): + if boxed { + buffer.appendInt32(893913689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-235238024) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVenue(let lat, let long, let title, let address, let provider, let venueId): + if boxed { + buffer.appendInt32(-1978796689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(address, buffer: buffer, boxed: false) + serializeString(provider, buffer: buffer, boxed: false) + serializeString(venueId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-1760785394) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaWebPage(let url): + if boxed { + buffer.appendInt32(-452652584) + } + serializeString(url, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + var _4: Buffer? + _4 = parseBytes(reader) + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi66.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaContact(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi66.DecryptedMessageMedia.decryptedMessageMediaContact(phoneNumber: _1!, firstName: _2!, lastName: _3!, userId: _4!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: Int32? + _5 = reader.readInt32() + var _6: Buffer? + _6 = parseBytes(reader) + var _7: Buffer? + _7 = parseBytes(reader) + var _8: [SecretApi66.DocumentAttribute]? + if let _ = reader.readInt32() { + _8 = SecretApi66.parseVector(reader, elementSignature: 0, elementType: SecretApi66.DocumentAttribute.self) + } + var _9: String? + _9 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi66.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, mimeType: _4!, size: _5!, key: _6!, iv: _7!, attributes: _8!, caption: _9!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { + return SecretApi66.DecryptedMessageMedia.decryptedMessageMediaEmpty + } + fileprivate static func parse_decryptedMessageMediaExternalDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: Int32? + _5 = reader.readInt32() + var _6: SecretApi66.PhotoSize? + if let signature = reader.readInt32() { + _6 = SecretApi66.parse(reader, signature: signature) as? SecretApi66.PhotoSize + } + var _7: Int32? + _7 = reader.readInt32() + var _8: [SecretApi66.DocumentAttribute]? + if let _ = reader.readInt32() { + _8 = SecretApi66.parseVector(reader, elementSignature: 0, elementType: SecretApi66.DocumentAttribute.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi66.DecryptedMessageMedia.decryptedMessageMediaExternalDocument(id: _1!, accessHash: _2!, date: _3!, mimeType: _4!, size: _5!, thumb: _6!, dcId: _7!, attributes: _8!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaPhoto(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + var _6: Int32? + _6 = reader.readInt32() + var _7: Buffer? + _7 = parseBytes(reader) + var _8: Buffer? + _8 = parseBytes(reader) + var _9: String? + _9 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi66.DecryptedMessageMedia.decryptedMessageMediaPhoto(thumb: _1!, thumbW: _2!, thumbH: _3!, w: _4!, h: _5!, size: _6!, key: _7!, iv: _8!, caption: _9!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaVenue(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + var _3: String? + _3 = parseString(reader) + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: String? + _6 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return SecretApi66.DecryptedMessageMedia.decryptedMessageMediaVenue(lat: _1!, long: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaVideo(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: String? + _5 = parseString(reader) + var _6: Int32? + _6 = reader.readInt32() + var _7: Int32? + _7 = reader.readInt32() + var _8: Int32? + _8 = reader.readInt32() + var _9: Buffer? + _9 = parseBytes(reader) + var _10: Buffer? + _10 = parseBytes(reader) + var _11: String? + _11 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + let _c10 = _10 != nil + let _c11 = _11 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { + return SecretApi66.DecryptedMessageMedia.decryptedMessageMediaVideo(thumb: _1!, thumbW: _2!, thumbH: _3!, duration: _4!, mimeType: _5!, w: _6!, h: _7!, size: _8!, key: _9!, iv: _10!, caption: _11!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaWebPage(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi66.DecryptedMessageMedia.decryptedMessageMediaWebPage(url: _1!) + } + else { + return nil + } + } + } + + public enum DocumentAttribute { + case documentAttributeAnimated + case documentAttributeAudio(flags: Int32, duration: Int32, title: String?, performer: String?, waveform: Buffer?) + case documentAttributeFilename(fileName: String) + case documentAttributeImageSize(w: Int32, h: Int32) + case documentAttributeSticker(alt: String, stickerset: SecretApi66.InputStickerSet) + case documentAttributeVideo(flags: Int32, duration: Int32, w: Int32, h: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .documentAttributeAnimated: + if boxed { + buffer.appendInt32(297109817) + } + break + case .documentAttributeAudio(let flags, let duration, let title, let performer, let waveform): + if boxed { + buffer.appendInt32(-1739392570) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 { + serializeString(title!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 1) != 0 { + serializeString(performer!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 2) != 0 { + serializeBytes(waveform!, buffer: buffer, boxed: false) + } + break + case .documentAttributeFilename(let fileName): + if boxed { + buffer.appendInt32(358154344) + } + serializeString(fileName, buffer: buffer, boxed: false) + break + case .documentAttributeImageSize(let w, let h): + if boxed { + buffer.appendInt32(1815593308) + } + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + case .documentAttributeSticker(let alt, let stickerset): + if boxed { + buffer.appendInt32(978674434) + } + serializeString(alt, buffer: buffer, boxed: false) + stickerset.serialize(buffer, true) + break + case .documentAttributeVideo(let flags, let duration, let w, let h): + if boxed { + buffer.appendInt32(250621158) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_documentAttributeAnimated(_ reader: BufferReader) -> DocumentAttribute? { + return SecretApi66.DocumentAttribute.documentAttributeAnimated + } + fileprivate static func parse_documentAttributeAudio(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _3 = parseString(reader) + } + var _4: String? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _4 = parseString(reader) + } + var _5: Buffer? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _5 = parseBytes(reader) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi66.DocumentAttribute.documentAttributeAudio(flags: _1!, duration: _2!, title: _3, performer: _4, waveform: _5) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeFilename(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi66.DocumentAttribute.documentAttributeFilename(fileName: _1!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeImageSize(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.DocumentAttribute.documentAttributeImageSize(w: _1!, h: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeSticker(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi66.InputStickerSet? + if let signature = reader.readInt32() { + _2 = SecretApi66.parse(reader, signature: signature) as? SecretApi66.InputStickerSet + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.DocumentAttribute.documentAttributeSticker(alt: _1!, stickerset: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeVideo(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi66.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!) + } + else { + return nil + } + } + } + + public enum FileLocation { + case fileLocation(dcId: Int32, volumeId: Int64, localId: Int32, secret: Int64) + case fileLocationUnavailable(volumeId: Int64, localId: Int32, secret: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .fileLocation(let dcId, let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(1406570614) + } + serializeInt32(dcId, buffer: buffer, boxed: false) + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + case .fileLocationUnavailable(let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(2086234950) + } + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_fileLocation(_ reader: BufferReader) -> FileLocation? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int64? + _4 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi66.FileLocation.fileLocation(dcId: _1!, volumeId: _2!, localId: _3!, secret: _4!) + } + else { + return nil + } + } + fileprivate static func parse_fileLocationUnavailable(_ reader: BufferReader) -> FileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi66.FileLocation.fileLocationUnavailable(volumeId: _1!, localId: _2!, secret: _3!) + } + else { + return nil + } + } + } + + public enum InputStickerSet { + case inputStickerSetEmpty + case inputStickerSetShortName(shortName: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputStickerSetEmpty: + if boxed { + buffer.appendInt32(-4838507) + } + break + case .inputStickerSetShortName(let shortName): + if boxed { + buffer.appendInt32(-2044933984) + } + serializeString(shortName, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_inputStickerSetEmpty(_ reader: BufferReader) -> InputStickerSet? { + return SecretApi66.InputStickerSet.inputStickerSetEmpty + } + fileprivate static func parse_inputStickerSetShortName(_ reader: BufferReader) -> InputStickerSet? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi66.InputStickerSet.inputStickerSetShortName(shortName: _1!) + } + else { + return nil + } + } + } + + public enum MessageEntity { + case messageEntityBold(offset: Int32, length: Int32) + case messageEntityBotCommand(offset: Int32, length: Int32) + case messageEntityCode(offset: Int32, length: Int32) + case messageEntityEmail(offset: Int32, length: Int32) + case messageEntityHashtag(offset: Int32, length: Int32) + case messageEntityItalic(offset: Int32, length: Int32) + case messageEntityMention(offset: Int32, length: Int32) + case messageEntityPre(offset: Int32, length: Int32, language: String) + case messageEntityTextUrl(offset: Int32, length: Int32, url: String) + case messageEntityUnknown(offset: Int32, length: Int32) + case messageEntityUrl(offset: Int32, length: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageEntityBold(let offset, let length): + if boxed { + buffer.appendInt32(-1117713463) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityBotCommand(let offset, let length): + if boxed { + buffer.appendInt32(1827637959) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityCode(let offset, let length): + if boxed { + buffer.appendInt32(681706865) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityEmail(let offset, let length): + if boxed { + buffer.appendInt32(1692693954) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityHashtag(let offset, let length): + if boxed { + buffer.appendInt32(1868782349) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityItalic(let offset, let length): + if boxed { + buffer.appendInt32(-2106619040) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityMention(let offset, let length): + if boxed { + buffer.appendInt32(-100378723) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityPre(let offset, let length, let language): + if boxed { + buffer.appendInt32(1938967520) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(language, buffer: buffer, boxed: false) + break + case .messageEntityTextUrl(let offset, let length, let url): + if boxed { + buffer.appendInt32(1990644519) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + break + case .messageEntityUnknown(let offset, let length): + if boxed { + buffer.appendInt32(-1148011883) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityUrl(let offset, let length): + if boxed { + buffer.appendInt32(1859134776) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_messageEntityBold(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.MessageEntity.messageEntityBold(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityBotCommand(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.MessageEntity.messageEntityBotCommand(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityCode(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.MessageEntity.messageEntityCode(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.MessageEntity.messageEntityEmail(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityHashtag(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.MessageEntity.messageEntityHashtag(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityItalic(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.MessageEntity.messageEntityItalic(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityMention(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.MessageEntity.messageEntityMention(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityPre(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi66.MessageEntity.messageEntityPre(offset: _1!, length: _2!, language: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityTextUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi66.MessageEntity.messageEntityTextUrl(offset: _1!, length: _2!, url: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUnknown(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.MessageEntity.messageEntityUnknown(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi66.MessageEntity.messageEntityUrl(offset: _1!, length: _2!) + } + else { + return nil + } + } + } + + public enum PhotoSize { + case photoCachedSize(type: String, location: SecretApi66.FileLocation, w: Int32, h: Int32, bytes: Buffer) + case photoSize(type: String, location: SecretApi66.FileLocation, w: Int32, h: Int32, size: Int32) + case photoSizeEmpty(type: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .photoCachedSize(let type, let location, let w, let h, let bytes): + if boxed { + buffer.appendInt32(-374917894) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeBytes(bytes, buffer: buffer, boxed: false) + break + case .photoSize(let type, let location, let w, let h, let size): + if boxed { + buffer.appendInt32(2009052699) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + break + case .photoSizeEmpty(let type): + if boxed { + buffer.appendInt32(236446268) + } + serializeString(type, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_photoCachedSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi66.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi66.parse(reader, signature: signature) as? SecretApi66.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi66.PhotoSize.photoCachedSize(type: _1!, location: _2!, w: _3!, h: _4!, bytes: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi66.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi66.parse(reader, signature: signature) as? SecretApi66.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi66.PhotoSize.photoSize(type: _1!, location: _2!, w: _3!, h: _4!, size: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSizeEmpty(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi66.PhotoSize.photoSizeEmpty(type: _1!) + } + else { + return nil + } + } + } + + public enum SendMessageAction { + case sendMessageCancelAction + case sendMessageChooseContactAction + case sendMessageGeoLocationAction + case sendMessageRecordAudioAction + case sendMessageRecordRoundAction + case sendMessageRecordVideoAction + case sendMessageTypingAction + case sendMessageUploadAudioAction + case sendMessageUploadDocumentAction + case sendMessageUploadPhotoAction + case sendMessageUploadRoundAction + case sendMessageUploadVideoAction + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sendMessageCancelAction: + if boxed { + buffer.appendInt32(-44119819) + } + break + case .sendMessageChooseContactAction: + if boxed { + buffer.appendInt32(1653390447) + } + break + case .sendMessageGeoLocationAction: + if boxed { + buffer.appendInt32(393186209) + } + break + case .sendMessageRecordAudioAction: + if boxed { + buffer.appendInt32(-718310409) + } + break + case .sendMessageRecordRoundAction: + if boxed { + buffer.appendInt32(-1997373508) + } + break + case .sendMessageRecordVideoAction: + if boxed { + buffer.appendInt32(-1584933265) + } + break + case .sendMessageTypingAction: + if boxed { + buffer.appendInt32(381645902) + } + break + case .sendMessageUploadAudioAction: + if boxed { + buffer.appendInt32(-424899985) + } + break + case .sendMessageUploadDocumentAction: + if boxed { + buffer.appendInt32(-1884362354) + } + break + case .sendMessageUploadPhotoAction: + if boxed { + buffer.appendInt32(-1727382502) + } + break + case .sendMessageUploadRoundAction: + if boxed { + buffer.appendInt32(-1150187996) + } + break + case .sendMessageUploadVideoAction: + if boxed { + buffer.appendInt32(-1845219337) + } + break + } + } + + fileprivate static func parse_sendMessageCancelAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi66.SendMessageAction.sendMessageCancelAction + } + fileprivate static func parse_sendMessageChooseContactAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi66.SendMessageAction.sendMessageChooseContactAction + } + fileprivate static func parse_sendMessageGeoLocationAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi66.SendMessageAction.sendMessageGeoLocationAction + } + fileprivate static func parse_sendMessageRecordAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi66.SendMessageAction.sendMessageRecordAudioAction + } + fileprivate static func parse_sendMessageRecordRoundAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi66.SendMessageAction.sendMessageRecordRoundAction + } + fileprivate static func parse_sendMessageRecordVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi66.SendMessageAction.sendMessageRecordVideoAction + } + fileprivate static func parse_sendMessageTypingAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi66.SendMessageAction.sendMessageTypingAction + } + fileprivate static func parse_sendMessageUploadAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi66.SendMessageAction.sendMessageUploadAudioAction + } + fileprivate static func parse_sendMessageUploadDocumentAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi66.SendMessageAction.sendMessageUploadDocumentAction + } + fileprivate static func parse_sendMessageUploadPhotoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi66.SendMessageAction.sendMessageUploadPhotoAction + } + fileprivate static func parse_sendMessageUploadRoundAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi66.SendMessageAction.sendMessageUploadRoundAction + } + fileprivate static func parse_sendMessageUploadVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi66.SendMessageAction.sendMessageUploadVideoAction + } + } + +} diff --git a/submodules/TelegramApi/Sources/SecretApiLayer73.swift b/submodules/TelegramApi/Sources/SecretApiLayer73.swift index 9aec5f6b73..5253a5c3c3 100644 --- a/submodules/TelegramApi/Sources/SecretApiLayer73.swift +++ b/submodules/TelegramApi/Sources/SecretApiLayer73.swift @@ -5,55 +5,70 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[570911930] = { return $0.readInt64() } dict[571523412] = { return $0.readDouble() } dict[-1255641564] = { return parseString($0) } - dict[-1586283796] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } - dict[206520510] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } - dict[1700872964] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } - dict[-1967000459] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } - dict[1729750108] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } - dict[-217806717] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } - dict[1360072880] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } - dict[-204906213] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionRequestKey($0) } - dict[1877046107] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionAcceptKey($0) } - dict[-586814357] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionAbortKey($0) } - dict[-332526693] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionCommitKey($0) } - dict[-1473258141] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionNoop($0) } - dict[236446268] = { return SecretApi73.PhotoSize.parse_photoSizeEmpty($0) } - dict[2009052699] = { return SecretApi73.PhotoSize.parse_photoSize($0) } - dict[-374917894] = { return SecretApi73.PhotoSize.parse_photoCachedSize($0) } - dict[2086234950] = { return SecretApi73.FileLocation.parse_fileLocationUnavailable($0) } - dict[1406570614] = { return SecretApi73.FileLocation.parse_fileLocation($0) } - dict[467867529] = { return SecretApi73.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } - dict[1930838368] = { return SecretApi73.DecryptedMessage.parse_decryptedMessageService($0) } + dict[-1132882121] = { return SecretApi73.Bool.parse_boolFalse($0) } + dict[-1720552011] = { return SecretApi73.Bool.parse_boolTrue($0) } dict[-1848883596] = { return SecretApi73.DecryptedMessage.parse_decryptedMessage($0) } - dict[1815593308] = { return SecretApi73.DocumentAttribute.parse_documentAttributeImageSize($0) } + dict[1930838368] = { return SecretApi73.DecryptedMessage.parse_decryptedMessageService($0) } + dict[-586814357] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionAbortKey($0) } + dict[1877046107] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionAcceptKey($0) } + dict[-332526693] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionCommitKey($0) } + dict[1700872964] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } + dict[1729750108] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } + dict[-1473258141] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionNoop($0) } + dict[-217806717] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } + dict[206520510] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } + dict[-204906213] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionRequestKey($0) } + dict[1360072880] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionResend($0) } + dict[-1967000459] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } + dict[-1586283796] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } + dict[-860719551] = { return SecretApi73.DecryptedMessageAction.parse_decryptedMessageActionTyping($0) } + dict[467867529] = { return SecretApi73.DecryptedMessageLayer.parse_decryptedMessageLayer($0) } + dict[1474341323] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } + dict[1485441687] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } + dict[2063502050] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } + dict[144661578] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } + dict[-90853155] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaExternalDocument($0) } + dict[893913689] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } + dict[-235238024] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } + dict[-1978796689] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaVenue($0) } + dict[-1760785394] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } + dict[-452652584] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaWebPage($0) } dict[297109817] = { return SecretApi73.DocumentAttribute.parse_documentAttributeAnimated($0) } - dict[358154344] = { return SecretApi73.DocumentAttribute.parse_documentAttributeFilename($0) } - dict[978674434] = { return SecretApi73.DocumentAttribute.parse_documentAttributeSticker($0) } dict[-1739392570] = { return SecretApi73.DocumentAttribute.parse_documentAttributeAudio($0) } + dict[358154344] = { return SecretApi73.DocumentAttribute.parse_documentAttributeFilename($0) } + dict[1815593308] = { return SecretApi73.DocumentAttribute.parse_documentAttributeImageSize($0) } + dict[978674434] = { return SecretApi73.DocumentAttribute.parse_documentAttributeSticker($0) } dict[250621158] = { return SecretApi73.DocumentAttribute.parse_documentAttributeVideo($0) } - dict[-2044933984] = { return SecretApi73.InputStickerSet.parse_inputStickerSetShortName($0) } + dict[1406570614] = { return SecretApi73.FileLocation.parse_fileLocation($0) } + dict[2086234950] = { return SecretApi73.FileLocation.parse_fileLocationUnavailable($0) } dict[-4838507] = { return SecretApi73.InputStickerSet.parse_inputStickerSetEmpty($0) } - dict[-1148011883] = { return SecretApi73.MessageEntity.parse_messageEntityUnknown($0) } - dict[-100378723] = { return SecretApi73.MessageEntity.parse_messageEntityMention($0) } - dict[1868782349] = { return SecretApi73.MessageEntity.parse_messageEntityHashtag($0) } - dict[1827637959] = { return SecretApi73.MessageEntity.parse_messageEntityBotCommand($0) } - dict[1859134776] = { return SecretApi73.MessageEntity.parse_messageEntityUrl($0) } - dict[1692693954] = { return SecretApi73.MessageEntity.parse_messageEntityEmail($0) } + dict[-2044933984] = { return SecretApi73.InputStickerSet.parse_inputStickerSetShortName($0) } dict[-1117713463] = { return SecretApi73.MessageEntity.parse_messageEntityBold($0) } - dict[-2106619040] = { return SecretApi73.MessageEntity.parse_messageEntityItalic($0) } + dict[1827637959] = { return SecretApi73.MessageEntity.parse_messageEntityBotCommand($0) } dict[681706865] = { return SecretApi73.MessageEntity.parse_messageEntityCode($0) } + dict[1692693954] = { return SecretApi73.MessageEntity.parse_messageEntityEmail($0) } + dict[1868782349] = { return SecretApi73.MessageEntity.parse_messageEntityHashtag($0) } + dict[-2106619040] = { return SecretApi73.MessageEntity.parse_messageEntityItalic($0) } + dict[-100378723] = { return SecretApi73.MessageEntity.parse_messageEntityMention($0) } dict[1938967520] = { return SecretApi73.MessageEntity.parse_messageEntityPre($0) } dict[1990644519] = { return SecretApi73.MessageEntity.parse_messageEntityTextUrl($0) } - dict[144661578] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } - dict[893913689] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } - dict[1485441687] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } - dict[1474341323] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } - dict[-90853155] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaExternalDocument($0) } - dict[-235238024] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } - dict[2063502050] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } - dict[-1760785394] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } - dict[-1978796689] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaVenue($0) } - dict[-452652584] = { return SecretApi73.DecryptedMessageMedia.parse_decryptedMessageMediaWebPage($0) } + dict[-1148011883] = { return SecretApi73.MessageEntity.parse_messageEntityUnknown($0) } + dict[1859134776] = { return SecretApi73.MessageEntity.parse_messageEntityUrl($0) } + dict[-374917894] = { return SecretApi73.PhotoSize.parse_photoCachedSize($0) } + dict[2009052699] = { return SecretApi73.PhotoSize.parse_photoSize($0) } + dict[236446268] = { return SecretApi73.PhotoSize.parse_photoSizeEmpty($0) } + dict[-44119819] = { return SecretApi73.SendMessageAction.parse_sendMessageCancelAction($0) } + dict[1653390447] = { return SecretApi73.SendMessageAction.parse_sendMessageChooseContactAction($0) } + dict[393186209] = { return SecretApi73.SendMessageAction.parse_sendMessageGeoLocationAction($0) } + dict[-718310409] = { return SecretApi73.SendMessageAction.parse_sendMessageRecordAudioAction($0) } + dict[-1997373508] = { return SecretApi73.SendMessageAction.parse_sendMessageRecordRoundAction($0) } + dict[-1584933265] = { return SecretApi73.SendMessageAction.parse_sendMessageRecordVideoAction($0) } + dict[381645902] = { return SecretApi73.SendMessageAction.parse_sendMessageTypingAction($0) } + dict[-424899985] = { return SecretApi73.SendMessageAction.parse_sendMessageUploadAudioAction($0) } + dict[-1884362354] = { return SecretApi73.SendMessageAction.parse_sendMessageUploadDocumentAction($0) } + dict[-1727382502] = { return SecretApi73.SendMessageAction.parse_sendMessageUploadPhotoAction($0) } + dict[-1150187996] = { return SecretApi73.SendMessageAction.parse_sendMessageUploadRoundAction($0) } + dict[-1845219337] = { return SecretApi73.SendMessageAction.parse_sendMessageUploadVideoAction($0) } return dict }() @@ -65,18 +80,18 @@ public struct SecretApi73 { } return nil } - - fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { - if let parser = parsers[signature] { - return parser(reader) - } - else { - telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") - return nil - } + + fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { + if let parser = parsers[signature] { + return parser(reader) } - - fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { + else { + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + return nil + } + } + + fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { if let count = reader.readInt32() { var array = [T]() var i: Int32 = 0 @@ -102,226 +117,293 @@ public struct SecretApi73 { } return nil } - + public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) { switch object { - case let _1 as SecretApi73.DecryptedMessageAction: - _1.serialize(buffer, boxed) - case let _1 as SecretApi73.PhotoSize: - _1.serialize(buffer, boxed) - case let _1 as SecretApi73.FileLocation: - _1.serialize(buffer, boxed) - case let _1 as SecretApi73.DecryptedMessageLayer: - _1.serialize(buffer, boxed) - case let _1 as SecretApi73.DecryptedMessage: - _1.serialize(buffer, boxed) - case let _1 as SecretApi73.DocumentAttribute: - _1.serialize(buffer, boxed) - case let _1 as SecretApi73.InputStickerSet: - _1.serialize(buffer, boxed) - case let _1 as SecretApi73.MessageEntity: - _1.serialize(buffer, boxed) - case let _1 as SecretApi73.DecryptedMessageMedia: - _1.serialize(buffer, boxed) - default: + case let _1 as SecretApi73.Bool: + _1.serialize(buffer, boxed) + case let _1 as SecretApi73.DecryptedMessage: + _1.serialize(buffer, boxed) + case let _1 as SecretApi73.DecryptedMessageAction: + _1.serialize(buffer, boxed) + case let _1 as SecretApi73.DecryptedMessageLayer: + _1.serialize(buffer, boxed) + case let _1 as SecretApi73.DecryptedMessageMedia: + _1.serialize(buffer, boxed) + case let _1 as SecretApi73.DocumentAttribute: + _1.serialize(buffer, boxed) + case let _1 as SecretApi73.FileLocation: + _1.serialize(buffer, boxed) + case let _1 as SecretApi73.InputStickerSet: + _1.serialize(buffer, boxed) + case let _1 as SecretApi73.MessageEntity: + _1.serialize(buffer, boxed) + case let _1 as SecretApi73.PhotoSize: + _1.serialize(buffer, boxed) + case let _1 as SecretApi73.SendMessageAction: + _1.serialize(buffer, boxed) + default: + break + } + } + + public enum Bool { + case boolFalse + case boolTrue + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boolFalse: + if boxed { + buffer.appendInt32(-1132882121) + } break + case .boolTrue: + if boxed { + buffer.appendInt32(-1720552011) + } + break + } + } + + fileprivate static func parse_boolFalse(_ reader: BufferReader) -> Bool? { + return SecretApi73.Bool.boolFalse + } + fileprivate static func parse_boolTrue(_ reader: BufferReader) -> Bool? { + return SecretApi73.Bool.boolTrue + } + } + + public enum DecryptedMessage { + case decryptedMessage(flags: Int32, randomId: Int64, ttl: Int32, message: String, media: SecretApi73.DecryptedMessageMedia?, entities: [SecretApi73.MessageEntity]?, viaBotName: String?, replyToRandomId: Int64?, groupedId: Int64?) + case decryptedMessageService(randomId: Int64, action: SecretApi73.DecryptedMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessage(let flags, let randomId, let ttl, let message, let media, let entities, let viaBotName, let replyToRandomId, let groupedId): + if boxed { + buffer.appendInt32(-1848883596) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(randomId, buffer: buffer, boxed: false) + serializeInt32(ttl, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 9) != 0 { + media!.serialize(buffer, true) + } + if Int(flags) & Int(1 << 7) != 0 { + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + } + } + if Int(flags) & Int(1 << 11) != 0 { + serializeString(viaBotName!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 3) != 0 { + serializeInt64(replyToRandomId!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 17) != 0 { + serializeInt64(groupedId!, buffer: buffer, boxed: false) + } + break + case .decryptedMessageService(let randomId, let action): + if boxed { + buffer.appendInt32(1930838368) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + action.serialize(buffer, true) + break + } + } + + fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: SecretApi73.DecryptedMessageMedia? + if Int(_1 ?? 0) & Int(1 << 9) != 0 { + if let signature = reader.readInt32() { + _5 = SecretApi73.parse(reader, signature: signature) as? SecretApi73.DecryptedMessageMedia + } + } + var _6: [SecretApi73.MessageEntity]? + if Int(_1 ?? 0) & Int(1 << 7) != 0 { + if let _ = reader.readInt32() { + _6 = SecretApi73.parseVector(reader, elementSignature: 0, elementType: SecretApi73.MessageEntity.self) + } + } + var _7: String? + if Int(_1 ?? 0) & Int(1 << 11) != 0 { + _7 = parseString(reader) + } + var _8: Int64? + if Int(_1 ?? 0) & Int(1 << 3) != 0 { + _8 = reader.readInt64() + } + var _9: Int64? + if Int(_1 ?? 0) & Int(1 << 17) != 0 { + _9 = reader.readInt64() + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 9) == 0) || _5 != nil + let _c6 = (Int(_1 ?? 0) & Int(1 << 7) == 0) || _6 != nil + let _c7 = (Int(_1 ?? 0) & Int(1 << 11) == 0) || _7 != nil + let _c8 = (Int(_1 ?? 0) & Int(1 << 3) == 0) || _8 != nil + let _c9 = (Int(_1 ?? 0) & Int(1 << 17) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi73.DecryptedMessage.decryptedMessage(flags: _1!, randomId: _2!, ttl: _3!, message: _4!, media: _5, entities: _6, viaBotName: _7, replyToRandomId: _8, groupedId: _9) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { + var _1: Int64? + _1 = reader.readInt64() + var _2: SecretApi73.DecryptedMessageAction? + if let signature = reader.readInt32() { + _2 = SecretApi73.parse(reader, signature: signature) as? SecretApi73.DecryptedMessageAction + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) + } + else { + return nil + } } } public enum DecryptedMessageAction { - case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) - case decryptedMessageActionReadMessages(randomIds: [Int64]) - case decryptedMessageActionDeleteMessages(randomIds: [Int64]) - case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) - case decryptedMessageActionFlushHistory - case decryptedMessageActionNotifyLayer(layer: Int32) - case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) - case decryptedMessageActionRequestKey(exchangeId: Int64, gA: Buffer) - case decryptedMessageActionAcceptKey(exchangeId: Int64, gB: Buffer, keyFingerprint: Int64) case decryptedMessageActionAbortKey(exchangeId: Int64) + case decryptedMessageActionAcceptKey(exchangeId: Int64, gB: Buffer, keyFingerprint: Int64) case decryptedMessageActionCommitKey(exchangeId: Int64, keyFingerprint: Int64) + case decryptedMessageActionDeleteMessages(randomIds: [Int64]) + case decryptedMessageActionFlushHistory case decryptedMessageActionNoop - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageActionSetMessageTTL(let ttlSeconds): - if boxed { - buffer.appendInt32(-1586283796) - } - serializeInt32(ttlSeconds, buffer: buffer, boxed: false) - break - case .decryptedMessageActionReadMessages(let randomIds): - if boxed { - buffer.appendInt32(206520510) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionDeleteMessages(let randomIds): - if boxed { - buffer.appendInt32(1700872964) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionScreenshotMessages(let randomIds): - if boxed { - buffer.appendInt32(-1967000459) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionFlushHistory: - if boxed { - buffer.appendInt32(1729750108) - } - - break - case .decryptedMessageActionNotifyLayer(let layer): - if boxed { - buffer.appendInt32(-217806717) - } - serializeInt32(layer, buffer: buffer, boxed: false) - break - case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): - if boxed { - buffer.appendInt32(1360072880) - } - serializeInt32(startSeqNo, buffer: buffer, boxed: false) - serializeInt32(endSeqNo, buffer: buffer, boxed: false) - break - case .decryptedMessageActionRequestKey(let exchangeId, let gA): - if boxed { - buffer.appendInt32(-204906213) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - serializeBytes(gA, buffer: buffer, boxed: false) - break - case .decryptedMessageActionAcceptKey(let exchangeId, let gB, let keyFingerprint): - if boxed { - buffer.appendInt32(1877046107) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - serializeBytes(gB, buffer: buffer, boxed: false) - serializeInt64(keyFingerprint, buffer: buffer, boxed: false) - break - case .decryptedMessageActionAbortKey(let exchangeId): - if boxed { - buffer.appendInt32(-586814357) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - break - case .decryptedMessageActionCommitKey(let exchangeId, let keyFingerprint): - if boxed { - buffer.appendInt32(-332526693) - } - serializeInt64(exchangeId, buffer: buffer, boxed: false) - serializeInt64(keyFingerprint, buffer: buffer, boxed: false) - break - case .decryptedMessageActionNoop: - if boxed { - buffer.appendInt32(-1473258141) - } - - break - } - } - fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return SecretApi73.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) - } - else { - return nil + case decryptedMessageActionNotifyLayer(layer: Int32) + case decryptedMessageActionReadMessages(randomIds: [Int64]) + case decryptedMessageActionRequestKey(exchangeId: Int64, gA: Buffer) + case decryptedMessageActionResend(startSeqNo: Int32, endSeqNo: Int32) + case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) + case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) + case decryptedMessageActionTyping(action: SecretApi73.SendMessageAction) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageActionAbortKey(let exchangeId): + if boxed { + buffer.appendInt32(-586814357) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + break + case .decryptedMessageActionAcceptKey(let exchangeId, let gB, let keyFingerprint): + if boxed { + buffer.appendInt32(1877046107) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gB, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionCommitKey(let exchangeId, let keyFingerprint): + if boxed { + buffer.appendInt32(-332526693) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeInt64(keyFingerprint, buffer: buffer, boxed: false) + break + case .decryptedMessageActionDeleteMessages(let randomIds): + if boxed { + buffer.appendInt32(1700872964) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionFlushHistory: + if boxed { + buffer.appendInt32(1729750108) + } + break + case .decryptedMessageActionNoop: + if boxed { + buffer.appendInt32(-1473258141) + } + break + case .decryptedMessageActionNotifyLayer(let layer): + if boxed { + buffer.appendInt32(-217806717) + } + serializeInt32(layer, buffer: buffer, boxed: false) + break + case .decryptedMessageActionReadMessages(let randomIds): + if boxed { + buffer.appendInt32(206520510) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionRequestKey(let exchangeId, let gA): + if boxed { + buffer.appendInt32(-204906213) + } + serializeInt64(exchangeId, buffer: buffer, boxed: false) + serializeBytes(gA, buffer: buffer, boxed: false) + break + case .decryptedMessageActionResend(let startSeqNo, let endSeqNo): + if boxed { + buffer.appendInt32(1360072880) + } + serializeInt32(startSeqNo, buffer: buffer, boxed: false) + serializeInt32(endSeqNo, buffer: buffer, boxed: false) + break + case .decryptedMessageActionScreenshotMessages(let randomIds): + if boxed { + buffer.appendInt32(-1967000459) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionSetMessageTTL(let ttlSeconds): + if boxed { + buffer.appendInt32(-1586283796) + } + serializeInt32(ttlSeconds, buffer: buffer, boxed: false) + break + case .decryptedMessageActionTyping(let action): + if boxed { + buffer.appendInt32(-860719551) + } + action.serialize(buffer, true) + break } } - fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi73.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi73.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi73.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi73.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi73.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi73.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { - return SecretApi73.DecryptedMessageAction.decryptedMessageActionFlushHistory - } - fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return SecretApi73.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionRequestKey(_ reader: BufferReader) -> DecryptedMessageAction? { + + fileprivate static func parse_decryptedMessageActionAbortKey(_ reader: BufferReader) -> DecryptedMessageAction? { var _1: Int64? _1 = reader.readInt64() - var _2: Buffer? - _2 = parseBytes(reader) let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.DecryptedMessageAction.decryptedMessageActionRequestKey(exchangeId: _1!, gA: _2!) + if _c1 { + return SecretApi73.DecryptedMessageAction.decryptedMessageActionAbortKey(exchangeId: _1!) } else { return nil @@ -344,17 +426,6 @@ public struct SecretApi73 { return nil } } - fileprivate static func parse_decryptedMessageActionAbortKey(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return SecretApi73.DecryptedMessageAction.decryptedMessageActionAbortKey(exchangeId: _1!) - } - else { - return nil - } - } fileprivate static func parse_decryptedMessageActionCommitKey(_ reader: BufferReader) -> DecryptedMessageAction? { var _1: Int64? _1 = reader.readInt64() @@ -369,196 +440,134 @@ public struct SecretApi73 { return nil } } + fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi73.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi73.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi73.DecryptedMessageAction.decryptedMessageActionFlushHistory + } fileprivate static func parse_decryptedMessageActionNoop(_ reader: BufferReader) -> DecryptedMessageAction? { return SecretApi73.DecryptedMessageAction.decryptedMessageActionNoop } - - - } - - public enum PhotoSize { - case photoSizeEmpty(type: String) - case photoSize(type: String, location: SecretApi73.FileLocation, w: Int32, h: Int32, size: Int32) - case photoCachedSize(type: String, location: SecretApi73.FileLocation, w: Int32, h: Int32, bytes: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .photoSizeEmpty(let type): - if boxed { - buffer.appendInt32(236446268) - } - serializeString(type, buffer: buffer, boxed: false) - break - case .photoSize(let type, let location, let w, let h, let size): - if boxed { - buffer.appendInt32(2009052699) - } - serializeString(type, buffer: buffer, boxed: false) - location.serialize(buffer, true) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - break - case .photoCachedSize(let type, let location, let w, let h, let bytes): - if boxed { - buffer.appendInt32(-374917894) - } - serializeString(type, buffer: buffer, boxed: false) - location.serialize(buffer, true) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeBytes(bytes, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_photoSizeEmpty(_ reader: BufferReader) -> PhotoSize? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return SecretApi73.PhotoSize.photoSizeEmpty(type: _1!) - } - else { - return nil - } - } - fileprivate static func parse_photoSize(_ reader: BufferReader) -> PhotoSize? { - var _1: String? - _1 = parseString(reader) - var _2: SecretApi73.FileLocation? - if let signature = reader.readInt32() { - _2 = SecretApi73.parse(reader, signature: signature) as? SecretApi73.FileLocation - } - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi73.PhotoSize.photoSize(type: _1!, location: _2!, w: _3!, h: _4!, size: _5!) - } - else { - return nil - } - } - fileprivate static func parse_photoCachedSize(_ reader: BufferReader) -> PhotoSize? { - var _1: String? - _1 = parseString(reader) - var _2: SecretApi73.FileLocation? - if let signature = reader.readInt32() { - _2 = SecretApi73.parse(reader, signature: signature) as? SecretApi73.FileLocation - } - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Buffer? - _5 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi73.PhotoSize.photoCachedSize(type: _1!, location: _2!, w: _3!, h: _4!, bytes: _5!) - } - else { - return nil - } - } - - - } - - public enum FileLocation { - case fileLocationUnavailable(volumeId: Int64, localId: Int32, secret: Int64) - case fileLocation(dcId: Int32, volumeId: Int64, localId: Int32, secret: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .fileLocationUnavailable(let volumeId, let localId, let secret): - if boxed { - buffer.appendInt32(2086234950) - } - serializeInt64(volumeId, buffer: buffer, boxed: false) - serializeInt32(localId, buffer: buffer, boxed: false) - serializeInt64(secret, buffer: buffer, boxed: false) - break - case .fileLocation(let dcId, let volumeId, let localId, let secret): - if boxed { - buffer.appendInt32(1406570614) - } - serializeInt32(dcId, buffer: buffer, boxed: false) - serializeInt64(volumeId, buffer: buffer, boxed: false) - serializeInt32(localId, buffer: buffer, boxed: false) - serializeInt64(secret, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_fileLocationUnavailable(_ reader: BufferReader) -> FileLocation? { - var _1: Int64? - _1 = reader.readInt64() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int64? - _3 = reader.readInt64() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi73.FileLocation.fileLocationUnavailable(volumeId: _1!, localId: _2!, secret: _3!) - } - else { - return nil - } - } - fileprivate static func parse_fileLocation(_ reader: BufferReader) -> FileLocation? { + fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { var _1: Int32? _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int64? - _4 = reader.readInt64() let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return SecretApi73.FileLocation.fileLocation(dcId: _1!, volumeId: _2!, localId: _3!, secret: _4!) + if _c1 { + return SecretApi73.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi73.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi73.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionRequestKey(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Buffer? + _2 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.DecryptedMessageAction.decryptedMessageActionRequestKey(exchangeId: _1!, gA: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionResend(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.DecryptedMessageAction.decryptedMessageActionResend(startSeqNo: _1!, endSeqNo: _2!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi73.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi73.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi73.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionTyping(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: SecretApi73.SendMessageAction? + if let signature = reader.readInt32() { + _1 = SecretApi73.parse(reader, signature: signature) as? SecretApi73.SendMessageAction + } + let _c1 = _1 != nil + if _c1 { + return SecretApi73.DecryptedMessageAction.decryptedMessageActionTyping(action: _1!) } else { return nil } } - - } public enum DecryptedMessageLayer { case decryptedMessageLayer(randomBytes: Buffer, layer: Int32, inSeqNo: Int32, outSeqNo: Int32, message: SecretApi73.DecryptedMessage) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): - if boxed { - buffer.appendInt32(467867529) - } - serializeBytes(randomBytes, buffer: buffer, boxed: false) - serializeInt32(layer, buffer: buffer, boxed: false) - serializeInt32(inSeqNo, buffer: buffer, boxed: false) - serializeInt32(outSeqNo, buffer: buffer, boxed: false) - message.serialize(buffer, true) - break - } - } + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageLayer(let randomBytes, let layer, let inSeqNo, let outSeqNo, let message): + if boxed { + buffer.appendInt32(467867529) + } + serializeBytes(randomBytes, buffer: buffer, boxed: false) + serializeInt32(layer, buffer: buffer, boxed: false) + serializeInt32(inSeqNo, buffer: buffer, boxed: false) + serializeInt32(outSeqNo, buffer: buffer, boxed: false) + message.serialize(buffer, true) + break + } + } + fileprivate static func parse_decryptedMessageLayer(_ reader: BufferReader) -> DecryptedMessageLayer? { var _1: Buffer? _1 = parseBytes(reader) @@ -584,690 +593,156 @@ public struct SecretApi73 { return nil } } - - - } - - public enum DecryptedMessage { - case decryptedMessageService(randomId: Int64, action: SecretApi73.DecryptedMessageAction) - case decryptedMessage(flags: Int32, randomId: Int64, ttl: Int32, message: String, media: SecretApi73.DecryptedMessageMedia?, entities: [SecretApi73.MessageEntity]?, viaBotName: String?, replyToRandomId: Int64?, groupedId: Int64?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageService(let randomId, let action): - if boxed { - buffer.appendInt32(1930838368) - } - serializeInt64(randomId, buffer: buffer, boxed: false) - action.serialize(buffer, true) - break - case .decryptedMessage(let flags, let randomId, let ttl, let message, let media, let entities, let viaBotName, let replyToRandomId, let groupedId): - if boxed { - buffer.appendInt32(-1848883596) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(randomId, buffer: buffer, boxed: false) - serializeInt32(ttl, buffer: buffer, boxed: false) - serializeString(message, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 9) != 0 {media!.serialize(buffer, true)} - if Int(flags) & Int(1 << 7) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(entities!.count)) - for item in entities! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 11) != 0 {serializeString(viaBotName!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 3) != 0 {serializeInt64(replyToRandomId!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 17) != 0 {serializeInt64(groupedId!, buffer: buffer, boxed: false)} - break - } - } - fileprivate static func parse_decryptedMessageService(_ reader: BufferReader) -> DecryptedMessage? { - var _1: Int64? - _1 = reader.readInt64() - var _2: SecretApi73.DecryptedMessageAction? - if let signature = reader.readInt32() { - _2 = SecretApi73.parse(reader, signature: signature) as? SecretApi73.DecryptedMessageAction - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.DecryptedMessage.decryptedMessageService(randomId: _1!, action: _2!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int32? - _3 = reader.readInt32() - var _4: String? - _4 = parseString(reader) - var _5: SecretApi73.DecryptedMessageMedia? - if Int(_1!) & Int(1 << 9) != 0 {if let signature = reader.readInt32() { - _5 = SecretApi73.parse(reader, signature: signature) as? SecretApi73.DecryptedMessageMedia - } } - var _6: [SecretApi73.MessageEntity]? - if Int(_1!) & Int(1 << 7) != 0 {if let _ = reader.readInt32() { - _6 = SecretApi73.parseVector(reader, elementSignature: 0, elementType: SecretApi73.MessageEntity.self) - } } - var _7: String? - if Int(_1!) & Int(1 << 11) != 0 {_7 = parseString(reader) } - var _8: Int64? - if Int(_1!) & Int(1 << 3) != 0 {_8 = reader.readInt64() } - var _9: Int64? - if Int(_1!) & Int(1 << 17) != 0 {_9 = reader.readInt64() } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 9) == 0) || _5 != nil - let _c6 = (Int(_1!) & Int(1 << 7) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 11) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 3) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 17) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return SecretApi73.DecryptedMessage.decryptedMessage(flags: _1!, randomId: _2!, ttl: _3!, message: _4!, media: _5, entities: _6, viaBotName: _7, replyToRandomId: _8, groupedId: _9) - } - else { - return nil - } - } - - - } - - public enum DocumentAttribute { - case documentAttributeImageSize(w: Int32, h: Int32) - case documentAttributeAnimated - case documentAttributeFilename(fileName: String) - case documentAttributeSticker(alt: String, stickerset: SecretApi73.InputStickerSet) - case documentAttributeAudio(flags: Int32, duration: Int32, title: String?, performer: String?, waveform: Buffer?) - case documentAttributeVideo(flags: Int32, duration: Int32, w: Int32, h: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .documentAttributeImageSize(let w, let h): - if boxed { - buffer.appendInt32(1815593308) - } - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - break - case .documentAttributeAnimated: - if boxed { - buffer.appendInt32(297109817) - } - - break - case .documentAttributeFilename(let fileName): - if boxed { - buffer.appendInt32(358154344) - } - serializeString(fileName, buffer: buffer, boxed: false) - break - case .documentAttributeSticker(let alt, let stickerset): - if boxed { - buffer.appendInt32(978674434) - } - serializeString(alt, buffer: buffer, boxed: false) - stickerset.serialize(buffer, true) - break - case .documentAttributeAudio(let flags, let duration, let title, let performer, let waveform): - if boxed { - buffer.appendInt32(-1739392570) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(duration, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(title!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(performer!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {serializeBytes(waveform!, buffer: buffer, boxed: false)} - break - case .documentAttributeVideo(let flags, let duration, let w, let h): - if boxed { - buffer.appendInt32(250621158) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(duration, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_documentAttributeImageSize(_ reader: BufferReader) -> DocumentAttribute? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.DocumentAttribute.documentAttributeImageSize(w: _1!, h: _2!) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeAnimated(_ reader: BufferReader) -> DocumentAttribute? { - return SecretApi73.DocumentAttribute.documentAttributeAnimated - } - fileprivate static func parse_documentAttributeFilename(_ reader: BufferReader) -> DocumentAttribute? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return SecretApi73.DocumentAttribute.documentAttributeFilename(fileName: _1!) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeSticker(_ reader: BufferReader) -> DocumentAttribute? { - var _1: String? - _1 = parseString(reader) - var _2: SecretApi73.InputStickerSet? - if let signature = reader.readInt32() { - _2 = SecretApi73.parse(reader, signature: signature) as? SecretApi73.InputStickerSet - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.DocumentAttribute.documentAttributeSticker(alt: _1!, stickerset: _2!) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeAudio(_ reader: BufferReader) -> DocumentAttribute? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } - var _4: String? - if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } - var _5: Buffer? - if Int(_1!) & Int(1 << 2) != 0 {_5 = parseBytes(reader) } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi73.DocumentAttribute.documentAttributeAudio(flags: _1!, duration: _2!, title: _3, performer: _4, waveform: _5) - } - else { - return nil - } - } - fileprivate static func parse_documentAttributeVideo(_ reader: BufferReader) -> DocumentAttribute? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return SecretApi73.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!) - } - else { - return nil - } - } - - - } - - public enum InputStickerSet { - case inputStickerSetShortName(shortName: String) - case inputStickerSetEmpty - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputStickerSetShortName(let shortName): - if boxed { - buffer.appendInt32(-2044933984) - } - serializeString(shortName, buffer: buffer, boxed: false) - break - case .inputStickerSetEmpty: - if boxed { - buffer.appendInt32(-4838507) - } - - break - } - } - fileprivate static func parse_inputStickerSetShortName(_ reader: BufferReader) -> InputStickerSet? { - var _1: String? - _1 = parseString(reader) - let _c1 = _1 != nil - if _c1 { - return SecretApi73.InputStickerSet.inputStickerSetShortName(shortName: _1!) - } - else { - return nil - } - } - fileprivate static func parse_inputStickerSetEmpty(_ reader: BufferReader) -> InputStickerSet? { - return SecretApi73.InputStickerSet.inputStickerSetEmpty - } - - - } - - public enum MessageEntity { - case messageEntityUnknown(offset: Int32, length: Int32) - case messageEntityMention(offset: Int32, length: Int32) - case messageEntityHashtag(offset: Int32, length: Int32) - case messageEntityBotCommand(offset: Int32, length: Int32) - case messageEntityUrl(offset: Int32, length: Int32) - case messageEntityEmail(offset: Int32, length: Int32) - case messageEntityBold(offset: Int32, length: Int32) - case messageEntityItalic(offset: Int32, length: Int32) - case messageEntityCode(offset: Int32, length: Int32) - case messageEntityPre(offset: Int32, length: Int32, language: String) - case messageEntityTextUrl(offset: Int32, length: Int32, url: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .messageEntityUnknown(let offset, let length): - if boxed { - buffer.appendInt32(-1148011883) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityMention(let offset, let length): - if boxed { - buffer.appendInt32(-100378723) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityHashtag(let offset, let length): - if boxed { - buffer.appendInt32(1868782349) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityBotCommand(let offset, let length): - if boxed { - buffer.appendInt32(1827637959) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityUrl(let offset, let length): - if boxed { - buffer.appendInt32(1859134776) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityEmail(let offset, let length): - if boxed { - buffer.appendInt32(1692693954) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityBold(let offset, let length): - if boxed { - buffer.appendInt32(-1117713463) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityItalic(let offset, let length): - if boxed { - buffer.appendInt32(-2106619040) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityCode(let offset, let length): - if boxed { - buffer.appendInt32(681706865) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - break - case .messageEntityPre(let offset, let length, let language): - if boxed { - buffer.appendInt32(1938967520) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - serializeString(language, buffer: buffer, boxed: false) - break - case .messageEntityTextUrl(let offset, let length, let url): - if boxed { - buffer.appendInt32(1990644519) - } - serializeInt32(offset, buffer: buffer, boxed: false) - serializeInt32(length, buffer: buffer, boxed: false) - serializeString(url, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_messageEntityUnknown(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.MessageEntity.messageEntityUnknown(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityMention(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.MessageEntity.messageEntityMention(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityHashtag(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.MessageEntity.messageEntityHashtag(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityBotCommand(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.MessageEntity.messageEntityBotCommand(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityUrl(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.MessageEntity.messageEntityUrl(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.MessageEntity.messageEntityEmail(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityBold(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.MessageEntity.messageEntityBold(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityItalic(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.MessageEntity.messageEntityItalic(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityCode(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.MessageEntity.messageEntityCode(offset: _1!, length: _2!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityPre(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi73.MessageEntity.messageEntityPre(offset: _1!, length: _2!, language: _3!) - } - else { - return nil - } - } - fileprivate static func parse_messageEntityTextUrl(_ reader: BufferReader) -> MessageEntity? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: String? - _3 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return SecretApi73.MessageEntity.messageEntityTextUrl(offset: _1!, length: _2!, url: _3!) - } - else { - return nil - } - } - - } public enum DecryptedMessageMedia { - case decryptedMessageMediaEmpty - case decryptedMessageMediaGeoPoint(lat: Double, long: Double) - case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) case decryptedMessageMediaAudio(duration: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer) - case decryptedMessageMediaExternalDocument(id: Int64, accessHash: Int64, date: Int32, mimeType: String, size: Int32, thumb: SecretApi73.PhotoSize, dcId: Int32, attributes: [SecretApi73.DocumentAttribute]) - case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) case decryptedMessageMediaDocument(thumb: Buffer, thumbW: Int32, thumbH: Int32, mimeType: String, size: Int32, key: Buffer, iv: Buffer, attributes: [SecretApi73.DocumentAttribute], caption: String) - case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) + case decryptedMessageMediaEmpty + case decryptedMessageMediaExternalDocument(id: Int64, accessHash: Int64, date: Int32, mimeType: String, size: Int32, thumb: SecretApi73.PhotoSize, dcId: Int32, attributes: [SecretApi73.DocumentAttribute]) + case decryptedMessageMediaGeoPoint(lat: Double, long: Double) + case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) case decryptedMessageMediaVenue(lat: Double, long: Double, title: String, address: String, provider: String, venueId: String) + case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, mimeType: String, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer, caption: String) case decryptedMessageMediaWebPage(url: String) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageMediaEmpty: - if boxed { - buffer.appendInt32(144661578) - } - - break - case .decryptedMessageMediaGeoPoint(let lat, let long): - if boxed { - buffer.appendInt32(893913689) - } - serializeDouble(lat, buffer: buffer, boxed: false) - serializeDouble(long, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): - if boxed { - buffer.appendInt32(1485441687) - } - serializeString(phoneNumber, buffer: buffer, boxed: false) - serializeString(firstName, buffer: buffer, boxed: false) - serializeString(lastName, buffer: buffer, boxed: false) - serializeInt32(userId, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): - if boxed { - buffer.appendInt32(1474341323) - } - serializeInt32(duration, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaExternalDocument(let id, let accessHash, let date, let mimeType, let size, let thumb, let dcId, let attributes): - if boxed { - buffer.appendInt32(-90853155) - } - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - thumb.serialize(buffer, true) - serializeInt32(dcId, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(attributes.count)) - for item in attributes { - item.serialize(buffer, true) - } - break - case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv, let caption): - if boxed { - buffer.appendInt32(-235238024) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - serializeString(caption, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let mimeType, let size, let key, let iv, let attributes, let caption): - if boxed { - buffer.appendInt32(2063502050) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(attributes.count)) - for item in attributes { - item.serialize(buffer, true) - } - serializeString(caption, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv, let caption): - if boxed { - buffer.appendInt32(-1760785394) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeInt32(duration, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - serializeString(caption, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaVenue(let lat, let long, let title, let address, let provider, let venueId): - if boxed { - buffer.appendInt32(-1978796689) - } - serializeDouble(lat, buffer: buffer, boxed: false) - serializeDouble(long, buffer: buffer, boxed: false) - serializeString(title, buffer: buffer, boxed: false) - serializeString(address, buffer: buffer, boxed: false) - serializeString(provider, buffer: buffer, boxed: false) - serializeString(venueId, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaWebPage(let url): - if boxed { - buffer.appendInt32(-452652584) - } - serializeString(url, buffer: buffer, boxed: false) - break - } - } - fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { - return SecretApi73.DecryptedMessageMedia.decryptedMessageMediaEmpty + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageMediaAudio(let duration, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(1474341323) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): + if boxed { + buffer.appendInt32(1485441687) + } + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + serializeInt32(userId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let mimeType, let size, let key, let iv, let attributes, let caption): + if boxed { + buffer.appendInt32(2063502050) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaEmpty: + if boxed { + buffer.appendInt32(144661578) + } + break + case .decryptedMessageMediaExternalDocument(let id, let accessHash, let date, let mimeType, let size, let thumb, let dcId, let attributes): + if boxed { + buffer.appendInt32(-90853155) + } + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + thumb.serialize(buffer, true) + serializeInt32(dcId, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + break + case .decryptedMessageMediaGeoPoint(let lat, let long): + if boxed { + buffer.appendInt32(893913689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-235238024) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVenue(let lat, let long, let title, let address, let provider, let venueId): + if boxed { + buffer.appendInt32(-1978796689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + serializeString(address, buffer: buffer, boxed: false) + serializeString(provider, buffer: buffer, boxed: false) + serializeString(venueId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let mimeType, let w, let h, let size, let key, let iv, let caption): + if boxed { + buffer.appendInt32(-1760785394) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + serializeString(caption, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaWebPage(let url): + if boxed { + buffer.appendInt32(-452652584) + } + serializeString(url, buffer: buffer, boxed: false) + break + } } - fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Double? - _1 = reader.readDouble() - var _2: Double? - _2 = reader.readDouble() + + fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + var _4: Buffer? + _4 = parseBytes(reader) + var _5: Buffer? + _5 = parseBytes(reader) let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi73.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi73.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) } else { return nil @@ -1293,29 +768,46 @@ public struct SecretApi73 { return nil } } - fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) + fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() var _3: Int32? _3 = reader.readInt32() - var _4: Buffer? - _4 = parseBytes(reader) - var _5: Buffer? - _5 = parseBytes(reader) + var _4: String? + _4 = parseString(reader) + var _5: Int32? + _5 = reader.readInt32() + var _6: Buffer? + _6 = parseBytes(reader) + var _7: Buffer? + _7 = parseBytes(reader) + var _8: [SecretApi73.DocumentAttribute]? + if let _ = reader.readInt32() { + _8 = SecretApi73.parseVector(reader, elementSignature: 0, elementType: SecretApi73.DocumentAttribute.self) + } + var _9: String? + _9 = parseString(reader) let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return SecretApi73.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, mimeType: _2!, size: _3!, key: _4!, iv: _5!) + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + let _c9 = _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return SecretApi73.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, mimeType: _4!, size: _5!, key: _6!, iv: _7!, attributes: _8!, caption: _9!) } else { return nil } } + fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { + return SecretApi73.DecryptedMessageMedia.decryptedMessageMediaEmpty + } fileprivate static func parse_decryptedMessageMediaExternalDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: Int64? _1 = reader.readInt64() @@ -1352,6 +844,20 @@ public struct SecretApi73 { return nil } } + fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + } + else { + return nil + } + } fileprivate static func parse_decryptedMessageMediaPhoto(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: Buffer? _1 = parseBytes(reader) @@ -1387,38 +893,27 @@ public struct SecretApi73 { return nil } } - fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Buffer? - _1 = parseBytes(reader) - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() + fileprivate static func parse_decryptedMessageMediaVenue(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + var _3: String? + _3 = parseString(reader) var _4: String? _4 = parseString(reader) - var _5: Int32? - _5 = reader.readInt32() - var _6: Buffer? - _6 = parseBytes(reader) - var _7: Buffer? - _7 = parseBytes(reader) - var _8: [SecretApi73.DocumentAttribute]? - if let _ = reader.readInt32() { - _8 = SecretApi73.parseVector(reader, elementSignature: 0, elementType: SecretApi73.DocumentAttribute.self) - } - var _9: String? - _9 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: String? + _6 = parseString(reader) let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - let _c9 = _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return SecretApi73.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, mimeType: _4!, size: _5!, key: _6!, iv: _7!, attributes: _8!, caption: _9!) + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return SecretApi73.DecryptedMessageMedia.decryptedMessageMediaVenue(lat: _1!, long: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!) } else { return nil @@ -1465,32 +960,6 @@ public struct SecretApi73 { return nil } } - fileprivate static func parse_decryptedMessageMediaVenue(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Double? - _1 = reader.readDouble() - var _2: Double? - _2 = reader.readDouble() - var _3: String? - _3 = parseString(reader) - var _4: String? - _4 = parseString(reader) - var _5: String? - _5 = parseString(reader) - var _6: String? - _6 = parseString(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return SecretApi73.DecryptedMessageMedia.decryptedMessageMediaVenue(lat: _1!, long: _2!, title: _3!, address: _4!, provider: _5!, venueId: _6!) - } - else { - return nil - } - } fileprivate static func parse_decryptedMessageMediaWebPage(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: String? _1 = parseString(reader) @@ -1502,12 +971,740 @@ public struct SecretApi73 { return nil } } - - } - public struct functions { - + public enum DocumentAttribute { + case documentAttributeAnimated + case documentAttributeAudio(flags: Int32, duration: Int32, title: String?, performer: String?, waveform: Buffer?) + case documentAttributeFilename(fileName: String) + case documentAttributeImageSize(w: Int32, h: Int32) + case documentAttributeSticker(alt: String, stickerset: SecretApi73.InputStickerSet) + case documentAttributeVideo(flags: Int32, duration: Int32, w: Int32, h: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .documentAttributeAnimated: + if boxed { + buffer.appendInt32(297109817) + } + break + case .documentAttributeAudio(let flags, let duration, let title, let performer, let waveform): + if boxed { + buffer.appendInt32(-1739392570) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 { + serializeString(title!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 1) != 0 { + serializeString(performer!, buffer: buffer, boxed: false) + } + if Int(flags) & Int(1 << 2) != 0 { + serializeBytes(waveform!, buffer: buffer, boxed: false) + } + break + case .documentAttributeFilename(let fileName): + if boxed { + buffer.appendInt32(358154344) + } + serializeString(fileName, buffer: buffer, boxed: false) + break + case .documentAttributeImageSize(let w, let h): + if boxed { + buffer.appendInt32(1815593308) + } + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + case .documentAttributeSticker(let alt, let stickerset): + if boxed { + buffer.appendInt32(978674434) + } + serializeString(alt, buffer: buffer, boxed: false) + stickerset.serialize(buffer, true) + break + case .documentAttributeVideo(let flags, let duration, let w, let h): + if boxed { + buffer.appendInt32(250621158) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_documentAttributeAnimated(_ reader: BufferReader) -> DocumentAttribute? { + return SecretApi73.DocumentAttribute.documentAttributeAnimated + } + fileprivate static func parse_documentAttributeAudio(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + if Int(_1 ?? 0) & Int(1 << 0) != 0 { + _3 = parseString(reader) + } + var _4: String? + if Int(_1 ?? 0) & Int(1 << 1) != 0 { + _4 = parseString(reader) + } + var _5: Buffer? + if Int(_1 ?? 0) & Int(1 << 2) != 0 { + _5 = parseBytes(reader) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1 ?? 0) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1 ?? 0) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1 ?? 0) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi73.DocumentAttribute.documentAttributeAudio(flags: _1!, duration: _2!, title: _3, performer: _4, waveform: _5) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeFilename(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi73.DocumentAttribute.documentAttributeFilename(fileName: _1!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeImageSize(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.DocumentAttribute.documentAttributeImageSize(w: _1!, h: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeSticker(_ reader: BufferReader) -> DocumentAttribute? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi73.InputStickerSet? + if let signature = reader.readInt32() { + _2 = SecretApi73.parse(reader, signature: signature) as? SecretApi73.InputStickerSet + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.DocumentAttribute.documentAttributeSticker(alt: _1!, stickerset: _2!) + } + else { + return nil + } + } + fileprivate static func parse_documentAttributeVideo(_ reader: BufferReader) -> DocumentAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi73.DocumentAttribute.documentAttributeVideo(flags: _1!, duration: _2!, w: _3!, h: _4!) + } + else { + return nil + } + } + } + + public enum FileLocation { + case fileLocation(dcId: Int32, volumeId: Int64, localId: Int32, secret: Int64) + case fileLocationUnavailable(volumeId: Int64, localId: Int32, secret: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .fileLocation(let dcId, let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(1406570614) + } + serializeInt32(dcId, buffer: buffer, boxed: false) + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + case .fileLocationUnavailable(let volumeId, let localId, let secret): + if boxed { + buffer.appendInt32(2086234950) + } + serializeInt64(volumeId, buffer: buffer, boxed: false) + serializeInt32(localId, buffer: buffer, boxed: false) + serializeInt64(secret, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_fileLocation(_ reader: BufferReader) -> FileLocation? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int64? + _4 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi73.FileLocation.fileLocation(dcId: _1!, volumeId: _2!, localId: _3!, secret: _4!) + } + else { + return nil + } + } + fileprivate static func parse_fileLocationUnavailable(_ reader: BufferReader) -> FileLocation? { + var _1: Int64? + _1 = reader.readInt64() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int64? + _3 = reader.readInt64() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi73.FileLocation.fileLocationUnavailable(volumeId: _1!, localId: _2!, secret: _3!) + } + else { + return nil + } + } + } + + public enum InputStickerSet { + case inputStickerSetEmpty + case inputStickerSetShortName(shortName: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputStickerSetEmpty: + if boxed { + buffer.appendInt32(-4838507) + } + break + case .inputStickerSetShortName(let shortName): + if boxed { + buffer.appendInt32(-2044933984) + } + serializeString(shortName, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_inputStickerSetEmpty(_ reader: BufferReader) -> InputStickerSet? { + return SecretApi73.InputStickerSet.inputStickerSetEmpty + } + fileprivate static func parse_inputStickerSetShortName(_ reader: BufferReader) -> InputStickerSet? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi73.InputStickerSet.inputStickerSetShortName(shortName: _1!) + } + else { + return nil + } + } + } + + public enum MessageEntity { + case messageEntityBold(offset: Int32, length: Int32) + case messageEntityBotCommand(offset: Int32, length: Int32) + case messageEntityCode(offset: Int32, length: Int32) + case messageEntityEmail(offset: Int32, length: Int32) + case messageEntityHashtag(offset: Int32, length: Int32) + case messageEntityItalic(offset: Int32, length: Int32) + case messageEntityMention(offset: Int32, length: Int32) + case messageEntityPre(offset: Int32, length: Int32, language: String) + case messageEntityTextUrl(offset: Int32, length: Int32, url: String) + case messageEntityUnknown(offset: Int32, length: Int32) + case messageEntityUrl(offset: Int32, length: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .messageEntityBold(let offset, let length): + if boxed { + buffer.appendInt32(-1117713463) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityBotCommand(let offset, let length): + if boxed { + buffer.appendInt32(1827637959) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityCode(let offset, let length): + if boxed { + buffer.appendInt32(681706865) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityEmail(let offset, let length): + if boxed { + buffer.appendInt32(1692693954) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityHashtag(let offset, let length): + if boxed { + buffer.appendInt32(1868782349) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityItalic(let offset, let length): + if boxed { + buffer.appendInt32(-2106619040) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityMention(let offset, let length): + if boxed { + buffer.appendInt32(-100378723) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityPre(let offset, let length, let language): + if boxed { + buffer.appendInt32(1938967520) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(language, buffer: buffer, boxed: false) + break + case .messageEntityTextUrl(let offset, let length, let url): + if boxed { + buffer.appendInt32(1990644519) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + serializeString(url, buffer: buffer, boxed: false) + break + case .messageEntityUnknown(let offset, let length): + if boxed { + buffer.appendInt32(-1148011883) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + case .messageEntityUrl(let offset, let length): + if boxed { + buffer.appendInt32(1859134776) + } + serializeInt32(offset, buffer: buffer, boxed: false) + serializeInt32(length, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_messageEntityBold(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.MessageEntity.messageEntityBold(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityBotCommand(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.MessageEntity.messageEntityBotCommand(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityCode(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.MessageEntity.messageEntityCode(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityEmail(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.MessageEntity.messageEntityEmail(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityHashtag(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.MessageEntity.messageEntityHashtag(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityItalic(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.MessageEntity.messageEntityItalic(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityMention(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.MessageEntity.messageEntityMention(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityPre(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi73.MessageEntity.messageEntityPre(offset: _1!, length: _2!, language: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityTextUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: String? + _3 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return SecretApi73.MessageEntity.messageEntityTextUrl(offset: _1!, length: _2!, url: _3!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUnknown(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.MessageEntity.messageEntityUnknown(offset: _1!, length: _2!) + } + else { + return nil + } + } + fileprivate static func parse_messageEntityUrl(_ reader: BufferReader) -> MessageEntity? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi73.MessageEntity.messageEntityUrl(offset: _1!, length: _2!) + } + else { + return nil + } + } + } + + public enum PhotoSize { + case photoCachedSize(type: String, location: SecretApi73.FileLocation, w: Int32, h: Int32, bytes: Buffer) + case photoSize(type: String, location: SecretApi73.FileLocation, w: Int32, h: Int32, size: Int32) + case photoSizeEmpty(type: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .photoCachedSize(let type, let location, let w, let h, let bytes): + if boxed { + buffer.appendInt32(-374917894) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeBytes(bytes, buffer: buffer, boxed: false) + break + case .photoSize(let type, let location, let w, let h, let size): + if boxed { + buffer.appendInt32(2009052699) + } + serializeString(type, buffer: buffer, boxed: false) + location.serialize(buffer, true) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + break + case .photoSizeEmpty(let type): + if boxed { + buffer.appendInt32(236446268) + } + serializeString(type, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_photoCachedSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi73.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi73.parse(reader, signature: signature) as? SecretApi73.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Buffer? + _5 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi73.PhotoSize.photoCachedSize(type: _1!, location: _2!, w: _3!, h: _4!, bytes: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSize(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + var _2: SecretApi73.FileLocation? + if let signature = reader.readInt32() { + _2 = SecretApi73.parse(reader, signature: signature) as? SecretApi73.FileLocation + } + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return SecretApi73.PhotoSize.photoSize(type: _1!, location: _2!, w: _3!, h: _4!, size: _5!) + } + else { + return nil + } + } + fileprivate static func parse_photoSizeEmpty(_ reader: BufferReader) -> PhotoSize? { + var _1: String? + _1 = parseString(reader) + let _c1 = _1 != nil + if _c1 { + return SecretApi73.PhotoSize.photoSizeEmpty(type: _1!) + } + else { + return nil + } + } + } + + public enum SendMessageAction { + case sendMessageCancelAction + case sendMessageChooseContactAction + case sendMessageGeoLocationAction + case sendMessageRecordAudioAction + case sendMessageRecordRoundAction + case sendMessageRecordVideoAction + case sendMessageTypingAction + case sendMessageUploadAudioAction + case sendMessageUploadDocumentAction + case sendMessageUploadPhotoAction + case sendMessageUploadRoundAction + case sendMessageUploadVideoAction + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .sendMessageCancelAction: + if boxed { + buffer.appendInt32(-44119819) + } + break + case .sendMessageChooseContactAction: + if boxed { + buffer.appendInt32(1653390447) + } + break + case .sendMessageGeoLocationAction: + if boxed { + buffer.appendInt32(393186209) + } + break + case .sendMessageRecordAudioAction: + if boxed { + buffer.appendInt32(-718310409) + } + break + case .sendMessageRecordRoundAction: + if boxed { + buffer.appendInt32(-1997373508) + } + break + case .sendMessageRecordVideoAction: + if boxed { + buffer.appendInt32(-1584933265) + } + break + case .sendMessageTypingAction: + if boxed { + buffer.appendInt32(381645902) + } + break + case .sendMessageUploadAudioAction: + if boxed { + buffer.appendInt32(-424899985) + } + break + case .sendMessageUploadDocumentAction: + if boxed { + buffer.appendInt32(-1884362354) + } + break + case .sendMessageUploadPhotoAction: + if boxed { + buffer.appendInt32(-1727382502) + } + break + case .sendMessageUploadRoundAction: + if boxed { + buffer.appendInt32(-1150187996) + } + break + case .sendMessageUploadVideoAction: + if boxed { + buffer.appendInt32(-1845219337) + } + break + } + } + + fileprivate static func parse_sendMessageCancelAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi73.SendMessageAction.sendMessageCancelAction + } + fileprivate static func parse_sendMessageChooseContactAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi73.SendMessageAction.sendMessageChooseContactAction + } + fileprivate static func parse_sendMessageGeoLocationAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi73.SendMessageAction.sendMessageGeoLocationAction + } + fileprivate static func parse_sendMessageRecordAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi73.SendMessageAction.sendMessageRecordAudioAction + } + fileprivate static func parse_sendMessageRecordRoundAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi73.SendMessageAction.sendMessageRecordRoundAction + } + fileprivate static func parse_sendMessageRecordVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi73.SendMessageAction.sendMessageRecordVideoAction + } + fileprivate static func parse_sendMessageTypingAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi73.SendMessageAction.sendMessageTypingAction + } + fileprivate static func parse_sendMessageUploadAudioAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi73.SendMessageAction.sendMessageUploadAudioAction + } + fileprivate static func parse_sendMessageUploadDocumentAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi73.SendMessageAction.sendMessageUploadDocumentAction + } + fileprivate static func parse_sendMessageUploadPhotoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi73.SendMessageAction.sendMessageUploadPhotoAction + } + fileprivate static func parse_sendMessageUploadRoundAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi73.SendMessageAction.sendMessageUploadRoundAction + } + fileprivate static func parse_sendMessageUploadVideoAction(_ reader: BufferReader) -> SendMessageAction? { + return SecretApi73.SendMessageAction.sendMessageUploadVideoAction + } } } diff --git a/submodules/TelegramApi/Sources/SecretApiLayer8.swift b/submodules/TelegramApi/Sources/SecretApiLayer8.swift index 9d070f35df..1b3acd81fa 100644 --- a/submodules/TelegramApi/Sources/SecretApiLayer8.swift +++ b/submodules/TelegramApi/Sources/SecretApiLayer8.swift @@ -5,21 +5,23 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[570911930] = { return $0.readInt64() } dict[571523412] = { return $0.readDouble() } dict[-1255641564] = { return parseString($0) } + dict[-1132882121] = { return SecretApi8.Bool.parse_boolFalse($0) } + dict[-1720552011] = { return SecretApi8.Bool.parse_boolTrue($0) } dict[528568095] = { return SecretApi8.DecryptedMessage.parse_decryptedMessage($0) } dict[-1438109059] = { return SecretApi8.DecryptedMessage.parse_decryptedMessageService($0) } - dict[144661578] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } - dict[846826124] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } - dict[1290694387] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } - dict[893913689] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } - dict[1485441687] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } - dict[-1332395189] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } - dict[1619031439] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } - dict[-1586283796] = { return SecretApi8.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } - dict[206520510] = { return SecretApi8.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } dict[1700872964] = { return SecretApi8.DecryptedMessageAction.parse_decryptedMessageActionDeleteMessages($0) } - dict[-1967000459] = { return SecretApi8.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } dict[1729750108] = { return SecretApi8.DecryptedMessageAction.parse_decryptedMessageActionFlushHistory($0) } dict[-217806717] = { return SecretApi8.DecryptedMessageAction.parse_decryptedMessageActionNotifyLayer($0) } + dict[206520510] = { return SecretApi8.DecryptedMessageAction.parse_decryptedMessageActionReadMessages($0) } + dict[-1967000459] = { return SecretApi8.DecryptedMessageAction.parse_decryptedMessageActionScreenshotMessages($0) } + dict[-1586283796] = { return SecretApi8.DecryptedMessageAction.parse_decryptedMessageActionSetMessageTTL($0) } + dict[1619031439] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaAudio($0) } + dict[1485441687] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaContact($0) } + dict[-1332395189] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaDocument($0) } + dict[144661578] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaEmpty($0) } + dict[893913689] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaGeoPoint($0) } + dict[846826124] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaPhoto($0) } + dict[1290694387] = { return SecretApi8.DecryptedMessageMedia.parse_decryptedMessageMediaVideo($0) } return dict }() @@ -31,18 +33,18 @@ public struct SecretApi8 { } return nil } - - fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { - if let parser = parsers[signature] { - return parser(reader) - } - else { - telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") - return nil - } + + fileprivate static func parse(_ reader: BufferReader, signature: Int32) -> Any? { + if let parser = parsers[signature] { + return parser(reader) } - - fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { + else { + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + return nil + } + } + + fileprivate static func parseVector(_ reader: BufferReader, elementSignature: Int32, elementType: T.Type) -> [T]? { if let count = reader.readInt32() { var array = [T]() var i: Int32 = 0 @@ -68,46 +70,75 @@ public struct SecretApi8 { } return nil } - + public static func serializeObject(_ object: Any, buffer: Buffer, boxed: Swift.Bool) { switch object { - case let _1 as SecretApi8.DecryptedMessage: - _1.serialize(buffer, boxed) - case let _1 as SecretApi8.DecryptedMessageMedia: - _1.serialize(buffer, boxed) - case let _1 as SecretApi8.DecryptedMessageAction: - _1.serialize(buffer, boxed) - default: + case let _1 as SecretApi8.Bool: + _1.serialize(buffer, boxed) + case let _1 as SecretApi8.DecryptedMessage: + _1.serialize(buffer, boxed) + case let _1 as SecretApi8.DecryptedMessageAction: + _1.serialize(buffer, boxed) + case let _1 as SecretApi8.DecryptedMessageMedia: + _1.serialize(buffer, boxed) + default: + break + } + } + + public enum Bool { + case boolFalse + case boolTrue + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .boolFalse: + if boxed { + buffer.appendInt32(-1132882121) + } break + case .boolTrue: + if boxed { + buffer.appendInt32(-1720552011) + } + break + } + } + + fileprivate static func parse_boolFalse(_ reader: BufferReader) -> Bool? { + return SecretApi8.Bool.boolFalse + } + fileprivate static func parse_boolTrue(_ reader: BufferReader) -> Bool? { + return SecretApi8.Bool.boolTrue } } public enum DecryptedMessage { case decryptedMessage(randomId: Int64, randomBytes: Buffer, message: String, media: SecretApi8.DecryptedMessageMedia) case decryptedMessageService(randomId: Int64, randomBytes: Buffer, action: SecretApi8.DecryptedMessageAction) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessage(let randomId, let randomBytes, let message, let media): - if boxed { - buffer.appendInt32(528568095) - } - serializeInt64(randomId, buffer: buffer, boxed: false) - serializeBytes(randomBytes, buffer: buffer, boxed: false) - serializeString(message, buffer: buffer, boxed: false) - media.serialize(buffer, true) - break - case .decryptedMessageService(let randomId, let randomBytes, let action): - if boxed { - buffer.appendInt32(-1438109059) - } - serializeInt64(randomId, buffer: buffer, boxed: false) - serializeBytes(randomBytes, buffer: buffer, boxed: false) - action.serialize(buffer, true) - break - } - } - + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessage(let randomId, let randomBytes, let message, let media): + if boxed { + buffer.appendInt32(528568095) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + serializeBytes(randomBytes, buffer: buffer, boxed: false) + serializeString(message, buffer: buffer, boxed: false) + media.serialize(buffer, true) + break + case .decryptedMessageService(let randomId, let randomBytes, let action): + if boxed { + buffer.appendInt32(-1438109059) + } + serializeInt64(randomId, buffer: buffer, boxed: false) + serializeBytes(randomBytes, buffer: buffer, boxed: false) + action.serialize(buffer, true) + break + } + } + fileprivate static func parse_decryptedMessage(_ reader: BufferReader) -> DecryptedMessage? { var _1: Int64? _1 = reader.readInt64() @@ -149,97 +180,307 @@ public struct SecretApi8 { return nil } } - + } + + public enum DecryptedMessageAction { + case decryptedMessageActionDeleteMessages(randomIds: [Int64]) + case decryptedMessageActionFlushHistory + case decryptedMessageActionNotifyLayer(layer: Int32) + case decryptedMessageActionReadMessages(randomIds: [Int64]) + case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) + case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageActionDeleteMessages(let randomIds): + if boxed { + buffer.appendInt32(1700872964) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionFlushHistory: + if boxed { + buffer.appendInt32(1729750108) + } + break + case .decryptedMessageActionNotifyLayer(let layer): + if boxed { + buffer.appendInt32(-217806717) + } + serializeInt32(layer, buffer: buffer, boxed: false) + break + case .decryptedMessageActionReadMessages(let randomIds): + if boxed { + buffer.appendInt32(206520510) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionScreenshotMessages(let randomIds): + if boxed { + buffer.appendInt32(-1967000459) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(randomIds.count)) + for item in randomIds { + serializeInt64(item, buffer: buffer, boxed: false) + } + break + case .decryptedMessageActionSetMessageTTL(let ttlSeconds): + if boxed { + buffer.appendInt32(-1586283796) + } + serializeInt32(ttlSeconds, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi8.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi8.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { + return SecretApi8.DecryptedMessageAction.decryptedMessageActionFlushHistory + } + fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi8.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi8.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi8.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: [Int64]? + if let _ = reader.readInt32() { + _1 = SecretApi8.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) + } + let _c1 = _1 != nil + if _c1 { + return SecretApi8.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return SecretApi8.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) + } + else { + return nil + } + } } public enum DecryptedMessageMedia { - case decryptedMessageMediaEmpty - case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer) - case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer) - case decryptedMessageMediaGeoPoint(lat: Double, long: Double) + case decryptedMessageMediaAudio(duration: Int32, size: Int32, key: Buffer, iv: Buffer) case decryptedMessageMediaContact(phoneNumber: String, firstName: String, lastName: String, userId: Int32) case decryptedMessageMediaDocument(thumb: Buffer, thumbW: Int32, thumbH: Int32, fileName: String, mimeType: String, size: Int32, key: Buffer, iv: Buffer) - case decryptedMessageMediaAudio(duration: Int32, size: Int32, key: Buffer, iv: Buffer) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageMediaEmpty: - if boxed { - buffer.appendInt32(144661578) - } - - break - case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv): - if boxed { - buffer.appendInt32(846826124) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let w, let h, let size, let key, let iv): - if boxed { - buffer.appendInt32(1290694387) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeInt32(duration, buffer: buffer, boxed: false) - serializeInt32(w, buffer: buffer, boxed: false) - serializeInt32(h, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaGeoPoint(let lat, let long): - if boxed { - buffer.appendInt32(893913689) - } - serializeDouble(lat, buffer: buffer, boxed: false) - serializeDouble(long, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): - if boxed { - buffer.appendInt32(1485441687) - } - serializeString(phoneNumber, buffer: buffer, boxed: false) - serializeString(firstName, buffer: buffer, boxed: false) - serializeString(lastName, buffer: buffer, boxed: false) - serializeInt32(userId, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let fileName, let mimeType, let size, let key, let iv): - if boxed { - buffer.appendInt32(-1332395189) - } - serializeBytes(thumb, buffer: buffer, boxed: false) - serializeInt32(thumbW, buffer: buffer, boxed: false) - serializeInt32(thumbH, buffer: buffer, boxed: false) - serializeString(fileName, buffer: buffer, boxed: false) - serializeString(mimeType, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - break - case .decryptedMessageMediaAudio(let duration, let size, let key, let iv): - if boxed { - buffer.appendInt32(1619031439) - } - serializeInt32(duration, buffer: buffer, boxed: false) - serializeInt32(size, buffer: buffer, boxed: false) - serializeBytes(key, buffer: buffer, boxed: false) - serializeBytes(iv, buffer: buffer, boxed: false) - break - } - } - + case decryptedMessageMediaEmpty + case decryptedMessageMediaGeoPoint(lat: Double, long: Double) + case decryptedMessageMediaPhoto(thumb: Buffer, thumbW: Int32, thumbH: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer) + case decryptedMessageMediaVideo(thumb: Buffer, thumbW: Int32, thumbH: Int32, duration: Int32, w: Int32, h: Int32, size: Int32, key: Buffer, iv: Buffer) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .decryptedMessageMediaAudio(let duration, let size, let key, let iv): + if boxed { + buffer.appendInt32(1619031439) + } + serializeInt32(duration, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaContact(let phoneNumber, let firstName, let lastName, let userId): + if boxed { + buffer.appendInt32(1485441687) + } + serializeString(phoneNumber, buffer: buffer, boxed: false) + serializeString(firstName, buffer: buffer, boxed: false) + serializeString(lastName, buffer: buffer, boxed: false) + serializeInt32(userId, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaDocument(let thumb, let thumbW, let thumbH, let fileName, let mimeType, let size, let key, let iv): + if boxed { + buffer.appendInt32(-1332395189) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeString(fileName, buffer: buffer, boxed: false) + serializeString(mimeType, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaEmpty: + if boxed { + buffer.appendInt32(144661578) + } + break + case .decryptedMessageMediaGeoPoint(let lat, let long): + if boxed { + buffer.appendInt32(893913689) + } + serializeDouble(lat, buffer: buffer, boxed: false) + serializeDouble(long, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaPhoto(let thumb, let thumbW, let thumbH, let w, let h, let size, let key, let iv): + if boxed { + buffer.appendInt32(846826124) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + case .decryptedMessageMediaVideo(let thumb, let thumbW, let thumbH, let duration, let w, let h, let size, let key, let iv): + if boxed { + buffer.appendInt32(1290694387) + } + serializeBytes(thumb, buffer: buffer, boxed: false) + serializeInt32(thumbW, buffer: buffer, boxed: false) + serializeInt32(thumbH, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeInt32(w, buffer: buffer, boxed: false) + serializeInt32(h, buffer: buffer, boxed: false) + serializeInt32(size, buffer: buffer, boxed: false) + serializeBytes(key, buffer: buffer, boxed: false) + serializeBytes(iv, buffer: buffer, boxed: false) + break + } + } + + fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Buffer? + _3 = parseBytes(reader) + var _4: Buffer? + _4 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi8.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, size: _2!, key: _3!, iv: _4!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaContact(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: String? + _3 = parseString(reader) + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return SecretApi8.DecryptedMessageMedia.decryptedMessageMediaContact(phoneNumber: _1!, firstName: _2!, lastName: _3!, userId: _4!) + } + else { + return nil + } + } + fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: String? + _4 = parseString(reader) + var _5: String? + _5 = parseString(reader) + var _6: Int32? + _6 = reader.readInt32() + var _7: Buffer? + _7 = parseBytes(reader) + var _8: Buffer? + _8 = parseBytes(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return SecretApi8.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, fileName: _4!, mimeType: _5!, size: _6!, key: _7!, iv: _8!) + } + else { + return nil + } + } fileprivate static func parse_decryptedMessageMediaEmpty(_ reader: BufferReader) -> DecryptedMessageMedia? { return SecretApi8.DecryptedMessageMedia.decryptedMessageMediaEmpty } + fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return SecretApi8.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) + } + else { + return nil + } + } fileprivate static func parse_decryptedMessageMediaPhoto(_ reader: BufferReader) -> DecryptedMessageMedia? { var _1: Buffer? _1 = parseBytes(reader) @@ -307,225 +548,6 @@ public struct SecretApi8 { return nil } } - fileprivate static func parse_decryptedMessageMediaGeoPoint(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Double? - _1 = reader.readDouble() - var _2: Double? - _2 = reader.readDouble() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return SecretApi8.DecryptedMessageMedia.decryptedMessageMediaGeoPoint(lat: _1!, long: _2!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageMediaContact(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - var _3: String? - _3 = parseString(reader) - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return SecretApi8.DecryptedMessageMedia.decryptedMessageMediaContact(phoneNumber: _1!, firstName: _2!, lastName: _3!, userId: _4!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageMediaDocument(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Buffer? - _1 = parseBytes(reader) - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: String? - _4 = parseString(reader) - var _5: String? - _5 = parseString(reader) - var _6: Int32? - _6 = reader.readInt32() - var _7: Buffer? - _7 = parseBytes(reader) - var _8: Buffer? - _8 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = _7 != nil - let _c8 = _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return SecretApi8.DecryptedMessageMedia.decryptedMessageMediaDocument(thumb: _1!, thumbW: _2!, thumbH: _3!, fileName: _4!, mimeType: _5!, size: _6!, key: _7!, iv: _8!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageMediaAudio(_ reader: BufferReader) -> DecryptedMessageMedia? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Buffer? - _3 = parseBytes(reader) - var _4: Buffer? - _4 = parseBytes(reader) - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return SecretApi8.DecryptedMessageMedia.decryptedMessageMediaAudio(duration: _1!, size: _2!, key: _3!, iv: _4!) - } - else { - return nil - } - } - - } - - public enum DecryptedMessageAction { - case decryptedMessageActionSetMessageTTL(ttlSeconds: Int32) - case decryptedMessageActionReadMessages(randomIds: [Int64]) - case decryptedMessageActionDeleteMessages(randomIds: [Int64]) - case decryptedMessageActionScreenshotMessages(randomIds: [Int64]) - case decryptedMessageActionFlushHistory - case decryptedMessageActionNotifyLayer(layer: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .decryptedMessageActionSetMessageTTL(let ttlSeconds): - if boxed { - buffer.appendInt32(-1586283796) - } - serializeInt32(ttlSeconds, buffer: buffer, boxed: false) - break - case .decryptedMessageActionReadMessages(let randomIds): - if boxed { - buffer.appendInt32(206520510) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionDeleteMessages(let randomIds): - if boxed { - buffer.appendInt32(1700872964) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionScreenshotMessages(let randomIds): - if boxed { - buffer.appendInt32(-1967000459) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(randomIds.count)) - for item in randomIds { - serializeInt64(item, buffer: buffer, boxed: false) - } - break - case .decryptedMessageActionFlushHistory: - if boxed { - buffer.appendInt32(1729750108) - } - - break - case .decryptedMessageActionNotifyLayer(let layer): - if boxed { - buffer.appendInt32(-217806717) - } - serializeInt32(layer, buffer: buffer, boxed: false) - break - } - } - - fileprivate static func parse_decryptedMessageActionSetMessageTTL(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return SecretApi8.DecryptedMessageAction.decryptedMessageActionSetMessageTTL(ttlSeconds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionReadMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi8.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi8.DecryptedMessageAction.decryptedMessageActionReadMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionDeleteMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi8.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi8.DecryptedMessageAction.decryptedMessageActionDeleteMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionScreenshotMessages(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: [Int64]? - if let _ = reader.readInt32() { - _1 = SecretApi8.parseVector(reader, elementSignature: 570911930, elementType: Int64.self) - } - let _c1 = _1 != nil - if _c1 { - return SecretApi8.DecryptedMessageAction.decryptedMessageActionScreenshotMessages(randomIds: _1!) - } - else { - return nil - } - } - fileprivate static func parse_decryptedMessageActionFlushHistory(_ reader: BufferReader) -> DecryptedMessageAction? { - return SecretApi8.DecryptedMessageAction.decryptedMessageActionFlushHistory - } - fileprivate static func parse_decryptedMessageActionNotifyLayer(_ reader: BufferReader) -> DecryptedMessageAction? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return SecretApi8.DecryptedMessageAction.decryptedMessageActionNotifyLayer(layer: _1!) - } - else { - return nil - } - } - - } - - public struct functions { - } } 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/TelegramBaseController/Sources/TelegramBaseController.swift b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift index 87cad945ee..2f8a557fda 100644 --- a/submodules/TelegramBaseController/Sources/TelegramBaseController.swift +++ b/submodules/TelegramBaseController/Sources/TelegramBaseController.swift @@ -196,7 +196,14 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { self.view.endEditing(true) self.context.joinGroupCall(peerId: peerId, invite: invite, requestJoinAsPeerId: { completion in - let currentAccountPeer = context.account.postbox.loadedPeerWithId(context.account.peerId) + let currentAccountPeer = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> map { peer in return [FoundPeer(peer: peer, subscribers: nil)] } @@ -233,10 +240,10 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { var items: [ActionSheetItem] = [] var isGroup = false for peer in peers { - if peer.peer is TelegramGroup { + if case .legacyGroup = peer.peer { isGroup = true break - } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + } else if case let .channel(channel) = peer.peer, case .group = channel.info { isGroup = true break } @@ -248,14 +255,14 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder { if peer.peer.id.namespace == Namespaces.Peer.CloudUser { subtitle = presentationData.strings.VoiceChat_PersonalAccount } else if let subscribers = peer.subscribers { - if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(channel) = peer.peer, case .broadcast = channel.info { subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers) } else { subtitle = strongSelf.presentationData.strings.Conversation_StatusMembers(subscribers) } } - items.append(VoiceChatPeerActionSheetItem(context: context, peer: EnginePeer(peer.peer), title: EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), subtitle: subtitle ?? "", action: { + items.append(VoiceChatPeerActionSheetItem(context: context, peer: peer.peer, title: peer.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), subtitle: subtitle ?? "", action: { dismissAction() completion(peer.peer.id) })) diff --git a/submodules/TelegramCallsUI/Sources/CallController.swift b/submodules/TelegramCallsUI/Sources/CallController.swift index c7dd71d3fc..098c84bf08 100644 --- a/submodules/TelegramCallsUI/Sources/CallController.swift +++ b/submodules/TelegramCallsUI/Sources/CallController.swift @@ -521,7 +521,6 @@ public final class CallController: ViewController { confirmation: { peer in switch peer { case let .peer(peer, _, _): - let peer = EnginePeer(peer) guard case let .user(user) = peer else { return .single(false) } @@ -539,7 +538,6 @@ public final class CallController: ViewController { isPeerEnabled: { peer in switch peer { case let .peer(peer, _, _): - let peer = EnginePeer(peer) guard case let .user(user) = peer else { return false } diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift index 5c0c2d233a..7833a53670 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData diff --git a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift index f0ea64fe0d..86db813a19 100644 --- a/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift +++ b/submodules/TelegramCallsUI/Sources/CallControllerNodeV2.swift @@ -626,7 +626,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP self.peerAvatarDisposable?.dispose() let size = CGSize(width: 128.0, height: 128.0) - if let representation = peer.largeProfileImage, let signal = peerAvatarImage(account: self.call.context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: self.callScreenState?.avatarImage == nil) { + if let representation = peer.largeProfileImage, let signal = peerAvatarImage(account: self.call.context.account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: self.callScreenState?.avatarImage == nil) { self.peerAvatarDisposable = (signal |> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in guard let self else { diff --git a/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift b/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift index 9e8a045458..fbd9141072 100644 --- a/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift +++ b/submodules/TelegramCallsUI/Sources/CallFeedbackController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import ItemListUI diff --git a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift index 04529dbea6..97dbfb86b2 100644 --- a/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift +++ b/submodules/TelegramCallsUI/Sources/CallStatusBarNode.swift @@ -319,15 +319,23 @@ public class CallStatusBarNodeImpl: CallStatusBarNode { switch content { case let .call(sharedContext, account, call): self.presentationData = sharedContext.currentPresentationData.with { $0 } + let callPeer = TelegramEngine(account: account).data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } self.stateDisposable.set( (combineLatest( - account.postbox.loadedPeerWithId(call.peerId), + callPeer, call.state, call.isMuted ) |> deliverOnMainQueue).start(next: { [weak self] peer, state, isMuted in if let strongSelf = self { - strongSelf.currentPeer = peer + strongSelf.currentPeer = peer._asPeer() strongSelf.currentCallState = state strongSelf.currentIsMuted = isMuted diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 24bb59e0de..df2a9311c6 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 @@ -1185,12 +1185,20 @@ public final class MediaStreamComponentController: ViewControllerComponentContai return } - let _ = (combineLatest(queue: .mainQueue(), self.context.account.postbox.loadedPeerWithId(peerId), self.callImpl.state |> take(1)) + let sharedPeerSignal = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + let _ = (combineLatest(queue: .mainQueue(), sharedPeerSignal, self.callImpl.state |> take(1)) |> deliverOnMainQueue).start(next: { [weak self] peer, callState in if let strongSelf = self { var inviteLinks = inviteLinks - - if let peer = peer as? TelegramChannel, case .group = peer.info, !peer.flags.contains(.isGigagroup), !(peer.addressName ?? "").isEmpty, let defaultParticipantMuteState = callState.defaultParticipantMuteState { + + if case let .channel(channel) = peer, case .group = channel.info, !channel.flags.contains(.isGigagroup), !(channel.addressName ?? "").isEmpty, let defaultParticipantMuteState = callState.defaultParticipantMuteState { let isMuted = defaultParticipantMuteState == .muted if !isMuted { @@ -1202,8 +1210,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 +1226,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 +1243,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/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index 2a4275f114..6d7cd2b900 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -320,7 +320,10 @@ public final class PresentationCallManagerImpl: PresentationCallManager { if let firstState = ringingStates.first { if self.currentCall == nil && self.currentGroupCall == nil { self.currentCallDisposable.set((combineLatest( - firstState.0.account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, PreferencesKeys.appConfiguration]) |> take(1), + firstState.0.engine.data.subscribe( + TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.voipConfiguration), + TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration) + ) |> take(1), accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings]) |> take(1) ) |> deliverOnMainQueue).start(next: { [weak self] preferences, sharedData in @@ -330,12 +333,12 @@ public final class PresentationCallManagerImpl: PresentationCallManager { if strongSelf.currentUpgradedToConferenceCallId == firstState.2.id { return } - - let configuration = preferences.values[PreferencesKeys.voipConfiguration]?.get(VoipConfiguration.self) ?? .defaultValue + + let configuration = preferences.0?.get(VoipConfiguration.self) ?? .defaultValue let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) ?? .defaultSettings let experimentalSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? .defaultSettings - let appConfiguration = preferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue - + let appConfiguration = preferences.1?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + let call = PresentationCallImpl( context: firstState.0, audioSession: strongSelf.audioSession, @@ -366,7 +369,10 @@ public final class PresentationCallManagerImpl: PresentationCallManager { let _ = currentCall.hangUp().startStandalone() self.currentCallDisposable.set((combineLatest( - firstState.0.account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, PreferencesKeys.appConfiguration]) |> take(1), + firstState.0.engine.data.subscribe( + TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.voipConfiguration), + TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration) + ) |> take(1), accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings]) |> take(1) ) |> deliverOnMainQueue).start(next: { [weak self] preferences, sharedData in @@ -376,11 +382,11 @@ public final class PresentationCallManagerImpl: PresentationCallManager { if strongSelf.currentUpgradedToConferenceCallId == firstState.2.id { return } - - let configuration = preferences.values[PreferencesKeys.voipConfiguration]?.get(VoipConfiguration.self) ?? .defaultValue + + let configuration = preferences.0?.get(VoipConfiguration.self) ?? .defaultValue let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) ?? .defaultSettings let experimentalSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) ?? .defaultSettings - let appConfiguration = preferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + let appConfiguration = preferences.1?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue let call = PresentationCallImpl( context: firstState.0, @@ -628,7 +634,10 @@ public final class PresentationCallManagerImpl: PresentationCallManager { return peerView.peerIsContact } |> take(1), - context.account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, PreferencesKeys.appConfiguration]) |> take(1), + context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.voipConfiguration), + TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration) + ) |> take(1), accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.experimentalUISettings]) |> take(1), areVideoCallsAvailable ) @@ -638,10 +647,10 @@ public final class PresentationCallManagerImpl: PresentationCallManager { if let currentCall = strongSelf.currentCall { currentCall.rejectBusy() } - - let configuration = preferences.values[PreferencesKeys.voipConfiguration]?.get(VoipConfiguration.self) ?? .defaultValue + + let configuration = preferences.0?.get(VoipConfiguration.self) ?? .defaultValue let autodownloadSettings = sharedData.entries[SharedDataKeys.autodownloadSettings]?.get(AutodownloadSettings.self) ?? .defaultSettings - let appConfiguration = preferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + let appConfiguration = preferences.1?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue let isVideoPossible: Bool = areVideoCallsAvailable diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 4cb912c60e..25916cfe2f 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -1182,25 +1182,32 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { }) if let peerId { - let _ = (self.account.postbox.loadedPeerWithId(peerId) + let _ = (self.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { [weak self] peer in guard let self else { return } var canManageCall = false - if let peer = peer as? TelegramGroup { - if case .creator = peer.role { + if case let .legacyGroup(group) = peer { + if case .creator = group.role { canManageCall = true - } else if case let .admin(rights, _) = peer.role, rights.rights.contains(.canManageCalls) { + } else if case let .admin(rights, _) = group.role, rights.rights.contains(.canManageCalls) { canManageCall = true } - } else if let peer = peer as? TelegramChannel { - if peer.flags.contains(.isCreator) { + } else if case let .channel(channel) = peer { + if channel.flags.contains(.isCreator) { canManageCall = true - } else if (peer.adminRights?.rights.contains(.canManageCalls) == true) { + } else if (channel.adminRights?.rights.contains(.canManageCalls) == true) { canManageCall = true } - self.peerUpdatesSubscription = self.accountContext.account.viewTracker.polledChannel(peerId: peer.id).start() + self.peerUpdatesSubscription = self.accountContext.account.viewTracker.polledChannel(peerId: channel.id).start() } var updatedValue = self.stateValue updatedValue.canManageCall = canManageCall diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index 4c6ef24fcc..128651bb7e 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -350,7 +350,7 @@ final class VideoChatParticipantVideoComponent: Component { if self.blurredAvatarDisposable == nil { //TODO:release synchronous - if let participantPeer = component.participant.peer, let imageCache = component.call.accountContext.imageCache as? DirectMediaImageCache, let peerReference = PeerReference(participantPeer._asPeer()) { + if let participantPeer = component.participant.peer, let imageCache = component.call.accountContext.imageCache as? DirectMediaImageCache, let peerReference = PeerReference(participantPeer) { if let result = imageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: participantPeer.profileImageRepresentations.first?.immediateThumbnailData, size: 64, synchronous: false) { if let image = result.image { blurredAvatarView.image = blurredAvatarImage(image) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index 5aa0a630c3..80b80058f5 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import ComponentFlow -import Postbox import TelegramCore import AccountContext import PlainButtonComponent diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index def60adbb4..7cc99459e5 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 @@ -677,7 +677,14 @@ final class VideoChatScreenComponent: Component { return } - let _ = (groupCall.accountContext.account.postbox.loadedPeerWithId(peerId) + let _ = (groupCall.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { [weak self] chatPeer in guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return @@ -685,7 +692,7 @@ final class VideoChatScreenComponent: Component { guard let callState = self.callState, let peer = self.peer else { return } - + let initialTitle = callState.title let title: String @@ -698,7 +705,7 @@ final class VideoChatScreenComponent: Component { text = environment.strings.VoiceChat_EditTitleText } - let controller = voiceChatTitleEditController(context: groupCall.accountContext, forceTheme: environment.theme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak self] title in + let controller = voiceChatTitleEditController(context: groupCall.accountContext, forceTheme: environment.theme, title: title, text: text, placeholder: chatPeer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak self] title in guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return } @@ -796,7 +803,14 @@ final class VideoChatScreenComponent: Component { } if let peerId = groupCall.peerId { - let _ = (groupCall.accountContext.account.postbox.loadedPeerWithId(peerId) + let _ = (groupCall.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { [weak self] peer in guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return @@ -825,8 +839,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 +858,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 +880,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 +888,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 +907,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 +929,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)) } } @@ -1811,7 +1821,14 @@ final class VideoChatScreenComponent: Component { } }) - let currentAccountPeer = groupCall.accountContext.account.postbox.loadedPeerWithId(groupCall.accountContext.account.peerId) + let currentAccountPeer = groupCall.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: groupCall.accountContext.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> map { peer in return [FoundPeer(peer: peer, subscribers: nil)] } @@ -1894,11 +1911,10 @@ final class VideoChatScreenComponent: Component { } } } else { - //TODO:localized if event.joined { - self.lastTitleEvent = "\(event.peer.compactDisplayTitle) joined" + self.lastTitleEvent = environment.strings.VideoChat_StatusPeerJoined(event.peer.compactDisplayTitle).string } else { - self.lastTitleEvent = "\(event.peer.compactDisplayTitle) left" + self.lastTitleEvent = environment.strings.VideoChat_StatusPeerLeft(event.peer.compactDisplayTitle).string } if !self.isUpdating { self.state?.updated(transition: .spring(duration: 0.4)) @@ -3453,7 +3469,6 @@ final class VideoChatScreenComponent: Component { mode: .standard(.default), chatLocation: .peer(id: call.accountContext.account.peerId), subject: nil, - peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift index dd47fdab48..34b08a5114 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenInviteMembers.swift @@ -136,7 +136,7 @@ extension VideoChatScreenComponent.View { } else { text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string } - self.presentToast(icon: .peer(EnginePeer(participant.peer)), text: text, duration: 3) + self.presentToast(icon: .peer(participant.peer), text: text, duration: 3) } } else { if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) { @@ -292,12 +292,19 @@ extension VideoChatScreenComponent.View { switch error { case .privacy: - let _ = (groupCall.accountContext.account.postbox.loadedPeerWithId(peer.id) + let _ = (groupCall.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { [weak self] peer in guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else { return } - environment.controller()?.present(textAlertController(context: groupCall.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) + environment.controller()?.present(textAlertController(context: groupCall.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(peer.compactDisplayTitle, peer.compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) }) case .notMutualContact: environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root)) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift index 8514be1ed1..41b9b2ff45 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenMoreMenu.swift @@ -168,7 +168,7 @@ extension VideoChatScreenComponent.View { for peer in displayAsPeers { if peer.peer.id == callState.myPeerId { let avatarSize = CGSize(width: 28.0, height: 28.0) - items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: currentCall.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { [weak self] c, _ in + items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: environment.strings, displayOrder: currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: currentCall.accountContext.account, peer: peer.peer, size: avatarSize)), action: { [weak self] c, _ in guard let self else { return } @@ -625,10 +625,10 @@ extension VideoChatScreenComponent.View { var isGroup = false if let displayAsPeers = self.displayAsPeers { for peer in displayAsPeers { - if peer.peer is TelegramGroup { + if case .legacyGroup = peer.peer { isGroup = true break - } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + } else if case let .channel(channel) = peer.peer, case .group = channel.info { isGroup = true break } @@ -645,7 +645,7 @@ extension VideoChatScreenComponent.View { if peer.peer.id.namespace == Namespaces.Peer.CloudUser { subtitle = environment.strings.VoiceChat_PersonalAccount } else if let subscribers = peer.subscribers { - if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(channel) = peer.peer, case .broadcast = channel.info { subtitle = environment.strings.Conversation_StatusSubscribers(subscribers) } else { subtitle = environment.strings.Conversation_StatusMembers(subscribers) @@ -655,7 +655,7 @@ extension VideoChatScreenComponent.View { let isSelected = peer.peer.id == myPeerId let extendedAvatarSize = CGSize(width: 35.0, height: 35.0) let theme = environment.theme - let avatarSignal = peerAvatarCompleteImage(account: groupCall.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize) + let avatarSignal = peerAvatarCompleteImage(account: groupCall.accountContext.account, peer: peer.peer, size: avatarSize) |> map { image -> UIImage? in if isSelected, let image = image { return generateImage(extendedAvatarSize, rotatedContext: { size, context in @@ -676,7 +676,7 @@ extension VideoChatScreenComponent.View { } } - items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { [weak self] _, f in + items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { [weak self] _, f in f(.default) guard let self, case let .group(groupCall) = self.currentCall else { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift index c4ba7b067f..0bfe632b7e 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreenParticipantContextMenu.swift @@ -497,9 +497,8 @@ extension VideoChatScreenComponent.View { } let _ = self.currentAvatarMixin.swap(nil) - let postbox = currentCall.accountContext.account.postbox self.updateAvatarDisposable.set((currentCall.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + return mapResourceToAvatarSizes(engine: currentCall.accountContext.engine, resource: resource, representations: representations) }) |> deliverOnMainQueue).start()) } @@ -552,16 +551,15 @@ extension VideoChatScreenComponent.View { let peerId = callState.myPeerId let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - currentCall.accountContext.account.postbox.mediaBox.storeResourceData(resource.id, data: data) + currentCall.accountContext.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) self.currentUpdatingAvatar = (representation, 0.0) - let postbox = currentCall.accountContext.account.postbox - let signal = peerId.namespace == Namespaces.Peer.CloudUser ? currentCall.accountContext.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) - }) : currentCall.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: currentCall.accountContext.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + let signal = peerId.namespace == Namespaces.Peer.CloudUser ? currentCall.accountContext.engine.accountData.updateAccountPhoto(resource: EngineMediaResource(resource), videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: currentCall.accountContext.engine, resource: resource, representations: representations) + }) : currentCall.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: currentCall.accountContext.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(resource)), mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: currentCall.accountContext.engine, resource: resource, representations: representations) }) self.updateAvatarDisposable.set((signal @@ -594,7 +592,7 @@ extension VideoChatScreenComponent.View { let peerId = callState.myPeerId let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - currentCall.accountContext.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) + currentCall.accountContext.engine.resources.storeResourceData(id: EngineMediaResource.Id(photoResource.id), data: data) let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) self.currentUpdatingAvatar = (representation, 0.0) @@ -693,12 +691,12 @@ extension VideoChatScreenComponent.View { self.updateAvatarDisposable.set((signal |> mapToSignal { videoResource -> Signal in if peerId.namespace == Namespaces.Peer.CloudUser { - return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + return context.engine.accountData.updateAccountPhoto(resource: EngineMediaResource(photoResource), videoResource: EngineMediaResource(videoResource), videoStartTimestamp: videoStartTimestamp, markup: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) } else { - return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(photoResource)), video: context.engine.peers.uploadedPeerVideo(resource: EngineMediaResource(videoResource)) |> map(Optional.init), videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatAccountHeaderActionSheetItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatAccountHeaderActionSheetItem.swift index 86fffedf79..2edf8c66e4 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatAccountHeaderActionSheetItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatAccountHeaderActionSheetItem.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import Display -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift index e6de2d4862..f950e540f0 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import AccountContext 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..9688ba3d0d 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 { @@ -139,7 +139,7 @@ public final class VoiceChatJoinScreen: ViewController { strongSelf.dismiss() strongSelf.join(activeCall) } else { - strongSelf.controllerNode.setPeer(call: activeCall, peer: peer._asPeer(), title: call.info.title, memberCount: call.info.participantCount, isStream: call.info.isStream) + strongSelf.controllerNode.setPeer(call: activeCall, peer: peer, title: call.info.title, memberCount: call.info.participantCount, isStream: call.info.isStream) } } else { let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -622,7 +622,7 @@ public final class VoiceChatJoinScreen: ViewController { })) } - func setPeer(call: CachedChannelData.ActiveCall, peer: Peer, title: String?, memberCount: Int, isStream: Bool) { + func setPeer(call: CachedChannelData.ActiveCall, peer: EnginePeer, title: String?, memberCount: Int, isStream: Bool) { self.call = call let transition = ContainedViewLayoutTransition.animated(duration: 0.22, curve: .easeInOut) @@ -647,7 +647,7 @@ final class VoiceChatPreviewContentNode: ASDisplayNode, ShareContentContainerNod private let titleNode: ImmediateTextNode private let countNode: ImmediateTextNode - init(context: AccountContext, peer: Peer, title: String?, memberCount: Int, isStream: Bool, theme: PresentationTheme, strings: PresentationStrings, displayOrder: PresentationPersonNameOrder) { + init(context: AccountContext, peer: EnginePeer, title: String?, memberCount: Int, isStream: Bool, theme: PresentationTheme, strings: PresentationStrings, displayOrder: PresentationPersonNameOrder) { self.avatarNode = AvatarNode(font: avatarFont) self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 4 @@ -659,10 +659,10 @@ final class VoiceChatPreviewContentNode: ASDisplayNode, ShareContentContainerNod super.init() self.addSubnode(self.avatarNode) - self.avatarNode.setPeer(context: context, theme: theme, peer: EnginePeer(peer), emptyColor: theme.list.mediaPlaceholderColor) + self.avatarNode.setPeer(context: context, theme: theme, peer: peer, emptyColor: theme.list.mediaPlaceholderColor) self.addSubnode(self.titleNode) - self.titleNode.attributedText = NSAttributedString(string: title ?? EnginePeer(peer).displayTitle(strings: strings, displayOrder: displayOrder), font: Font.semibold(16.0), textColor: theme.actionSheet.primaryTextColor) + self.titleNode.attributedText = NSAttributedString(string: title ?? peer.displayTitle(strings: strings, displayOrder: displayOrder), font: Font.semibold(16.0), textColor: theme.actionSheet.primaryTextColor) self.addSubnode(self.countNode) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift index 1e3a3d83db..02d15f743f 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift @@ -630,18 +630,25 @@ final class VoiceChatMainStageNode: ASDisplayNode { self.speakingAudioLevelView = nil } - self.speakingPeerDisposable.set((self.context.account.postbox.loadedPeerWithId(peerId) + self.speakingPeerDisposable.set((self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).start(next: { [weak self] peer in guard let strongSelf = self else { return } - + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.speakingAvatarNode.setPeer(context: strongSelf.context, theme: presentationData.theme, peer: EnginePeer(peer)) - + strongSelf.speakingAvatarNode.setPeer(context: strongSelf.context, theme: presentationData.theme, peer: peer) + let bodyAttributes = MarkdownAttributeSet(font: Font.regular(15.0), textColor: .white, additionalAttributes: [:]) let boldAttributes = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: .white, additionalAttributes: [:]) - let attributedText = addAttributesToStringWithRanges(presentationData.strings.VoiceChat_ParticipantIsSpeaking(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + let attributedText = addAttributesToStringWithRanges(presentationData.strings.VoiceChat_ParticipantIsSpeaking(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) strongSelf.speakingTitleNode.attributedText = attributedText strongSelf.speakingContainerNode.alpha = 0.0 diff --git a/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs b/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs index 5822db8b5f..091afbd2e0 100644 --- a/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs +++ b/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs @@ -34,7 +34,9 @@ union InstantPageBlock_Value { InstantPageBlock_Table, InstantPageBlock_Details, InstantPageBlock_RelatedArticles, - InstantPageBlock_Map + InstantPageBlock_Map, + InstantPageBlock_Heading, + InstantPageBlock_Formula } table InstantPageBlock { @@ -70,6 +72,7 @@ table InstantPageBlock_Paragraph { table InstantPageBlock_Preformatted { text:RichText (id: 0, required); + language:string (id: 1); } table InstantPageBlock_Footer { @@ -184,6 +187,15 @@ table InstantPageBlock_Map { caption:InstantPageCaption (id: 4, required); } +table InstantPageBlock_Heading { + text:RichText (id: 0, required); + level:int32 (id: 1); +} + +table InstantPageBlock_Formula { + latex:string (id: 0, required); +} + table InstantPageCaption { text:RichText (id: 0, required); credit:RichText (id: 1, required); diff --git a/submodules/TelegramCore/FlatSerialization/Models/RichText.fbs b/submodules/TelegramCore/FlatSerialization/Models/RichText.fbs index 7a1da32aff..564cd98ce2 100644 --- a/submodules/TelegramCore/FlatSerialization/Models/RichText.fbs +++ b/submodules/TelegramCore/FlatSerialization/Models/RichText.fbs @@ -19,7 +19,8 @@ union RichText_Value { RichText_Marked, RichText_Phone, RichText_Image, - RichText_Anchor + RichText_Anchor, + RichText_Formula } table RichText { @@ -92,4 +93,8 @@ table RichText_Image { table RichText_Anchor { text:RichText (id: 0, required); name:string (id: 1, required); -} \ No newline at end of file +} + +table RichText_Formula { + latex:string (id: 0, required); +} diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index ee663bb5aa..232b84e1e8 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -128,7 +128,7 @@ public class UnauthorizedAccount { if let nextType = nextType { parsedNextType = AuthorizationCodeNextType(apiType: nextType) } - if let state = transaction.getState() as? UnauthorizedAccountState, case let .payment(phoneNumber, _, _, _, _, syncContacts) = state.contents { + if let state = transaction.getState() as? UnauthorizedAccountState, case let .payment(phoneNumber, _, _, _, _, _, syncContacts) = state.contents { transaction.setState(UnauthorizedAccountState(isTestingEnvironment: testingEnvironment, masterDatacenterId: masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: nil, usePrevious: false))) } }).start() @@ -139,7 +139,7 @@ public class UnauthorizedAccount { let (futureAuthToken, apiUser) = (authorizationData.futureAuthToken, authorizationData.user) let _ = postbox.transaction({ [weak self] transaction in var syncContacts = true - if let state = transaction.getState() as? UnauthorizedAccountState, case let .payment(_, _, _, _, _, syncContactsValue) = state.contents { + if let state = transaction.getState() as? UnauthorizedAccountState, case let .payment(_, _, _, _, _, _, syncContactsValue) = state.contents { syncContacts = syncContactsValue } @@ -166,7 +166,7 @@ public class UnauthorizedAccount { let termsOfService = authorizationSignUpRequiredData.termsOfService let _ = postbox.transaction({ [weak self] transaction in if let self { - if let state = transaction.getState() as? UnauthorizedAccountState, case let .payment(number, codeHash, _, _, _, syncContacts) = state.contents { + if let state = transaction.getState() as? UnauthorizedAccountState, case let .payment(number, codeHash, _, _, _, _, syncContacts) = state.contents { let _ = beginSignUp( account: self, data: AuthorizationSignUpData( diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index 0024856b50..e9ea0821e5 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -242,6 +242,7 @@ private var declaredEncodables: Void = { declareEncodable(TelegramMediaLiveStream.self, f: { TelegramMediaLiveStream(decoder: $0) }) declareEncodable(ScheduledRepeatAttribute.self, f: { ScheduledRepeatAttribute(decoder: $0) }) declareEncodable(SummarizationMessageAttribute.self, f: { SummarizationMessageAttribute(decoder: $0) }) + declareEncodable(GuestChatMessageAttribute.self, f: { GuestChatMessageAttribute(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/AccountManager/AccountManagerResources.swift b/submodules/TelegramCore/Sources/AccountManager/AccountManagerResources.swift new file mode 100644 index 0000000000..9c53cd3732 --- /dev/null +++ b/submodules/TelegramCore/Sources/AccountManager/AccountManagerResources.swift @@ -0,0 +1,57 @@ +import Foundation +import SwiftSignalKit +import Postbox + +public final class AccountManagerResources { + private let mediaBox: MediaBox + + init(mediaBox: MediaBox) { + self.mediaBox = mediaBox + } + + public func data( + resource: EngineMediaResource, + pathExtension: String? = nil, + waitUntilFetchStatus: Bool = false, + attemptSynchronously: Bool = false + ) -> Signal { + return self.mediaBox.resourceData( + resource._asResource(), + pathExtension: pathExtension, + option: .complete(waitUntilFetchStatus: waitUntilFetchStatus), + attemptSynchronously: attemptSynchronously + ) + |> map { EngineMediaResource.ResourceData($0) } + } + + public func storeResourceData(id: EngineMediaResource.Id, data: Data, synchronous: Bool = false) { + self.mediaBox.storeResourceData(MediaResourceId(id.stringRepresentation), data: data, synchronous: synchronous) + } + + public func completedResourcePath(resource: EngineMediaResource, pathExtension: String? = nil) -> String? { + return self.mediaBox.completedResourcePath(resource._asResource(), pathExtension: pathExtension) + } + + public func completedResourcePath(id: EngineMediaResource.Id, pathExtension: String? = nil) -> String? { + return self.mediaBox.completedResourcePath(id: MediaResourceId(id.stringRepresentation), pathExtension: pathExtension) + } + + public func status(resource: EngineMediaResource, approximateSynchronousValue: Bool = false) -> Signal { + return self.mediaBox.resourceStatus(resource._asResource(), approximateSynchronousValue: approximateSynchronousValue) + |> map { EngineMediaResource.FetchStatus($0) } + } + + public func moveResourceData(from: EngineMediaResource.Id, to: EngineMediaResource.Id, synchronous: Bool = false) { + self.mediaBox.moveResourceData(from: MediaResourceId(from.stringRepresentation), to: MediaResourceId(to.stringRepresentation), synchronous: synchronous) + } + + public func fetch(reference: MediaResourceReference, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType) -> Signal { + return fetchedMediaResource(mediaBox: self.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: reference) + } +} + +public extension AccountManager { + var resources: AccountManagerResources { + return AccountManagerResources(mediaBox: self.mediaBox) + } +} diff --git a/submodules/TelegramCore/Sources/ApiUtils/ApiUtils.swift b/submodules/TelegramCore/Sources/ApiUtils/ApiUtils.swift index 43a8d691e6..7c585c3d32 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ApiUtils.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ApiUtils.swift @@ -14,6 +14,10 @@ public extension PeerReference { return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)) } } + + init?(_ peer: EnginePeer) { + self.init(peer._asPeer()) + } } extension PeerReference { @@ -79,6 +83,60 @@ func apiInputPeer(_ peer: Peer) -> Api.InputPeer? { } } +private func apiInputPeerFromSourceMessage(_ sourceMessageId: MessageId?, transaction: Transaction) -> (MessageId, Api.InputPeer)? { + guard let sourceMessageId else { + return nil + } + guard let sourcePeer = transaction.getPeer(sourceMessageId.peerId) else { + return nil + } + guard let inputPeer = apiInputPeer(sourcePeer) else { + return nil + } + return (sourceMessageId, inputPeer) +} + +func apiInputPeer(_ peer: Peer, sourceMessageId: MessageId?, transaction: Transaction) -> Api.InputPeer? { + switch peer { + case let user as TelegramUser: + if let accessHash = user.accessHash { + switch accessHash { + case let .personal(value): + return Api.InputPeer.inputPeerUser(.init(userId: user.id.id._internalGetInt64Value(), accessHash: value)) + case let .genericPublic(value): + if let (sourceMessageId, sourcePeer) = apiInputPeerFromSourceMessage(sourceMessageId, transaction: transaction) { + return Api.InputPeer.inputPeerUserFromMessage(.init(peer: sourcePeer, msgId: sourceMessageId.id, userId: user.id.id._internalGetInt64Value())) + } + return Api.InputPeer.inputPeerUser(.init(userId: user.id.id._internalGetInt64Value(), accessHash: value)) + } + } else if let (sourceMessageId, sourcePeer) = apiInputPeerFromSourceMessage(sourceMessageId, transaction: transaction) { + return Api.InputPeer.inputPeerUserFromMessage(.init(peer: sourcePeer, msgId: sourceMessageId.id, userId: user.id.id._internalGetInt64Value())) + } else { + return nil + } + case let group as TelegramGroup: + return Api.InputPeer.inputPeerChat(.init(chatId: group.id.id._internalGetInt64Value())) + case let channel as TelegramChannel: + if let accessHash = channel.accessHash { + switch accessHash { + case let .personal(value): + return Api.InputPeer.inputPeerChannel(.init(channelId: channel.id.id._internalGetInt64Value(), accessHash: value)) + case let .genericPublic(value): + if let (sourceMessageId, sourcePeer) = apiInputPeerFromSourceMessage(sourceMessageId, transaction: transaction) { + return Api.InputPeer.inputPeerChannelFromMessage(.init(peer: sourcePeer, msgId: sourceMessageId.id, channelId: channel.id.id._internalGetInt64Value())) + } + return Api.InputPeer.inputPeerChannel(.init(channelId: channel.id.id._internalGetInt64Value(), accessHash: value)) + } + } else if let (sourceMessageId, sourcePeer) = apiInputPeerFromSourceMessage(sourceMessageId, transaction: transaction) { + return Api.InputPeer.inputPeerChannelFromMessage(.init(peer: sourcePeer, msgId: sourceMessageId.id, channelId: channel.id.id._internalGetInt64Value())) + } else { + return nil + } + default: + return nil + } +} + func apiInputPeerOrSelf(_ peer: Peer, accountPeerId: PeerId) -> Api.InputPeer? { if peer.id == accountPeerId { return .inputPeerSelf @@ -94,6 +152,27 @@ func apiInputChannel(_ peer: Peer) -> Api.InputChannel? { } } +func apiInputChannel(_ peer: Peer, sourceMessageId: MessageId?, transaction: Transaction) -> Api.InputChannel? { + guard let channel = peer as? TelegramChannel else { + return nil + } + if let accessHash = channel.accessHash { + switch accessHash { + case let .personal(value): + return Api.InputChannel.inputChannel(.init(channelId: channel.id.id._internalGetInt64Value(), accessHash: value)) + case let .genericPublic(value): + if let (sourceMessageId, sourcePeer) = apiInputPeerFromSourceMessage(sourceMessageId, transaction: transaction) { + return Api.InputChannel.inputChannelFromMessage(.init(peer: sourcePeer, msgId: sourceMessageId.id, channelId: channel.id.id._internalGetInt64Value())) + } + return Api.InputChannel.inputChannel(.init(channelId: channel.id.id._internalGetInt64Value(), accessHash: value)) + } + } else if let (sourceMessageId, sourcePeer) = apiInputPeerFromSourceMessage(sourceMessageId, transaction: transaction) { + return Api.InputChannel.inputChannelFromMessage(.init(peer: sourcePeer, msgId: sourceMessageId.id, channelId: channel.id.id._internalGetInt64Value())) + } else { + return nil + } +} + func apiInputUser(_ peer: Peer) -> Api.InputUser? { if let user = peer as? TelegramUser, let accessHash = user.accessHash { return Api.InputUser.inputUser(.init(userId: user.id.id._internalGetInt64Value(), accessHash: accessHash.value)) @@ -102,6 +181,27 @@ func apiInputUser(_ peer: Peer) -> Api.InputUser? { } } +func apiInputUser(_ peer: Peer, sourceMessageId: MessageId?, transaction: Transaction) -> Api.InputUser? { + guard let user = peer as? TelegramUser else { + return nil + } + if let accessHash = user.accessHash { + switch accessHash { + case let .personal(value): + return Api.InputUser.inputUser(.init(userId: user.id.id._internalGetInt64Value(), accessHash: value)) + case let .genericPublic(value): + if let (sourceMessageId, sourcePeer) = apiInputPeerFromSourceMessage(sourceMessageId, transaction: transaction) { + return Api.InputUser.inputUserFromMessage(.init(peer: sourcePeer, msgId: sourceMessageId.id, userId: user.id.id._internalGetInt64Value())) + } + return Api.InputUser.inputUser(.init(userId: user.id.id._internalGetInt64Value(), accessHash: value)) + } + } else if let (sourceMessageId, sourcePeer) = apiInputPeerFromSourceMessage(sourceMessageId, transaction: transaction) { + return Api.InputUser.inputUserFromMessage(.init(peer: sourcePeer, msgId: sourceMessageId.id, userId: user.id.id._internalGetInt64Value())) + } else { + return nil + } +} + func apiInputSecretChat(_ peer: Peer) -> Api.InputEncryptedChat? { if let chat = peer as? TelegramSecretChat { return Api.InputEncryptedChat.inputEncryptedChat(.init(chatId: Int32(peer.id.id._internalGetInt64Value()), accessHash: chat.accessHash)) diff --git a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift index 262901d2a0..7030b2176a 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift @@ -121,7 +121,7 @@ extension InstantPageBlock { self = .paragraph(RichText(apiText: text)) case let .pageBlockPreformatted(pageBlockPreformattedData): let text = pageBlockPreformattedData.text - self = .preformatted(RichText(apiText: text)) + self = .preformatted(text: RichText(apiText: text), language: nil) case let .pageBlockFooter(pageBlockFooterData): let text = pageBlockFooterData.text self = .footer(RichText(apiText: text)) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index e36aff5d0c..30e939643c 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -463,7 +463,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI let (poll, results) = (messageMediaPollData.poll, messageMediaPollData.results) switch poll { case let .poll(pollData): - let (id, flags, question, answers, closePeriod, closeDate, pollHash) = (pollData.id, pollData.flags, pollData.question, pollData.answers, pollData.closePeriod, pollData.closeDate, pollData.hash) + let (id, flags, question, answers, closePeriod, closeDate, pollHash, countries) = (pollData.id, pollData.flags, pollData.question, pollData.answers, pollData.closePeriod, pollData.closeDate, pollData.hash, pollData.countriesIso2) let publicity: TelegramMediaPollPublicity if (flags & (1 << 1)) != 0 { publicity = .public @@ -482,7 +482,8 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI let shuffleAnswers = (flags & (1 << 8)) != 0 let hideResultsUntilClose = (flags & (1 << 9)) != 0 let isCreator = (flags & (1 << 10)) != 0 - + let restrictToSubscribers = (flags & (1 << 11)) != 0 + let questionText: String let questionEntities: [MessageTextEntity] switch question { @@ -496,7 +497,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI if let apiAttachedMedia = messageMediaPollData.attachedMedia { parsedAttachedMedia = textMediaAndExpirationTimerFromApiMedia(apiAttachedMedia, peerId).media } - return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod, deadlineDate: closeDate, pollHash: pollHash, openAnswers: openAnswers, revotingDisabled: revotingDisabled, shuffleAnswers: shuffleAnswers, hideResultsUntilClose: hideResultsUntilClose, isCreator: isCreator, attachedMedia: parsedAttachedMedia), nil, nil, nil, nil, nil) + return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod, deadlineDate: closeDate, pollHash: pollHash, openAnswers: openAnswers, revotingDisabled: revotingDisabled, shuffleAnswers: shuffleAnswers, hideResultsUntilClose: hideResultsUntilClose, isCreator: isCreator, attachedMedia: parsedAttachedMedia, restrictToSubscribers: restrictToSubscribers, countries: countries ?? []), nil, nil, nil, nil, nil) } case let .messageMediaToDo(messageMediaToDoData): let (todo, completions) = (messageMediaToDoData.todo, messageMediaToDoData.completions) @@ -847,7 +848,7 @@ extension StoreMessage { convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) { switch apiMessage { case let .message(messageData): - let (flags, flags2, id, fromId, boosts, rank, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId, messageEffectId, factCheck, reportDeliveryUntilDate, paidMessageStars, suggestedPost, scheduledRepeatPeriod, summaryFromLanguage) = (messageData.flags, messageData.flags2, messageData.id, messageData.fromId, messageData.fromBoostsApplied, messageData.fromRank, messageData.peerId, messageData.savedPeerId, messageData.fwdFrom, messageData.viaBotId, messageData.viaBusinessBotId, messageData.replyTo, messageData.date, messageData.message, messageData.media, messageData.replyMarkup, messageData.entities, messageData.views, messageData.forwards, messageData.replies, messageData.editDate, messageData.postAuthor, messageData.groupedId, messageData.reactions, messageData.restrictionReason, messageData.ttlPeriod, messageData.quickReplyShortcutId, messageData.effect, messageData.factcheck, messageData.reportDeliveryUntilDate, messageData.paidMessageStars, messageData.suggestedPost, messageData.scheduleRepeatPeriod, messageData.summaryFromLanguage) + let (flags, flags2, id, fromId, boosts, rank, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, guestChatViaFrom, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId, messageEffectId, factCheck, reportDeliveryUntilDate, paidMessageStars, suggestedPost, scheduledRepeatPeriod, summaryFromLanguage) = (messageData.flags, messageData.flags2, messageData.id, messageData.fromId, messageData.fromBoostsApplied, messageData.fromRank, messageData.peerId, messageData.savedPeerId, messageData.fwdFrom, messageData.viaBotId, messageData.viaBusinessBotId, messageData.guestchatViaFrom, messageData.replyTo, messageData.date, messageData.message, messageData.media, messageData.replyMarkup, messageData.entities, messageData.views, messageData.forwards, messageData.replies, messageData.editDate, messageData.postAuthor, messageData.groupedId, messageData.reactions, messageData.restrictionReason, messageData.ttlPeriod, messageData.quickReplyShortcutId, messageData.effect, messageData.factcheck, messageData.reportDeliveryUntilDate, messageData.paidMessageStars, messageData.suggestedPost, messageData.scheduleRepeatPeriod, messageData.summaryFromLanguage) var attributes: [MessageAttribute] = [] if (flags2 & (1 << 4)) != 0 { @@ -1086,6 +1087,10 @@ extension StoreMessage { if let viaBusinessBotId { attributes.append(InlineBusinessBotMessageAttribute(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(viaBusinessBotId)), title: nil)) } + + if let guestChatViaFrom { + attributes.append(GuestChatMessageAttribute(peerId: guestChatViaFrom.peerId)) + } if !Namespaces.Message.allNonRegular.contains(namespace) { if let views = views { @@ -1341,6 +1346,8 @@ extension StoreMessage { attributes.append(ReplyMessageAttribute(messageId: MessageId(peerId: replyPeerId, namespace: Namespaces.Message.Cloud, id: replyToMsgId), threadMessageId: threadMessageId, quote: quote, isQuote: isQuote, innerSubject: innerSubject)) } else if let replyHeader = replyHeader { attributes.append(QuotedReplyMessageAttribute(apiHeader: replyHeader, quote: quote, isQuote: isQuote)) + } else if let replyToTopId, peerIsForum { + threadId = Int64(replyToTopId) } case let .messageReplyStoryHeader(messageReplyStoryHeaderData): let (peer, storyId) = (messageReplyStoryHeaderData.peer, messageReplyStoryHeaderData.storyId) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift index 7e5d355ecb..4580acf837 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaPoll.swift @@ -71,10 +71,12 @@ extension TelegramMediaPollResults { if (flags & (1 << 0)) == 0 {//isMin hasUnseenVotes = (flags & (1 << 6)) != 0 } + + let canViewStats = (flags & (1 << 7)) != 0 self.init(voters: results.flatMap({ $0.map(TelegramMediaPollOptionVoters.init(apiVoters:)) }), totalVoters: totalVoters, recentVoters: recentVoters.flatMap { recentVoters in return recentVoters.map { $0.peerId } - } ?? [], solution: parsedSolution, hasUnseenVotes: hasUnseenVotes) + } ?? [], solution: parsedSolution, hasUnseenVotes: hasUnseenVotes, canViewStats: canViewStats) } } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift index 37327ac121..d1fa5fc352 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift @@ -44,6 +44,8 @@ func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPa return .giftAuction(TelegramMediaWebpageGiftAuctionAttribute(gift: gift, endDate: endDate)) case .webPageAttributeStory: return nil + case let .webPageAttributeAiComposeTone(webPageAttributeAiComposeTone): + return .aiTextStyle(TelegramMediaWebpageAITextStyleAttribute(emojiFileId: webPageAttributeAiComposeTone.emojiId)) } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift index e19c5f723f..08ee9e7caf 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift @@ -131,6 +131,9 @@ extension TelegramUser { if (flags2 & (1 << 18)) != 0 { botFlags.insert(.canManageBots) } + if (flags2 & (1 << 19)) != 0 { + botFlags.insert(.isGuestChat) + } botInfo = BotUserInfo(flags: botFlags, inlinePlaceholder: botInlinePlaceholder) } diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index 25caa0afc8..5bc16112de 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -527,8 +527,8 @@ private func internalResendAuthorizationCode(accountManager: AccountManager Signal in + var state = transaction.getPeerChatState(peerId) as? SecretChatState + + for (content, media, attributes) in allResults { + var text: String = "" + switch content.content { + case let .text(textValue): + text = textValue + case let .media(_, textValue): + text = textValue + default: + break + } + + if let currentState = state, let updatedState = enqueueSecretChatUploadedMessageContent( + transaction: transaction, + peerId: peerId, + state: currentState, + content: content, + text: text, + attributes: attributes, + media: media + ) { + state = updatedState + } else { + return .fail(StandaloneSendMessagesError(peerId: peerId, reason: .none)) + } + } + + return managedSecretChatOutgoingOperations( + auxiliaryMethods: auxiliaryMethods, + postbox: postbox, + network: network, + accountPeerId: accountPeerId, + mode: .standaloneComplete(peerId: peerId) + ) + |> castError(StandaloneSendMessagesError.self) + |> ignoreValues + } + |> castError(StandaloneSendMessagesError.self) + |> switchToLatest + |> map { _ -> StandaloneSendMessageStatus in + } + |> then(.single(.done)) + } + var sendSignals: [Signal] = [] for (content, media, attributes) in allResults { @@ -231,11 +278,10 @@ public func standaloneSendEnqueueMessages( } sendSignals.append(sendUploadedMessageContent( - auxiliaryMethods: auxiliaryMethods, postbox: postbox, network: network, stateManager: stateManager, - accountPeerId: stateManager.accountPeerId, + accountPeerId: accountPeerId, peerId: peerId, content: content, text: text, @@ -256,8 +302,52 @@ public func standaloneSendEnqueueMessages( } } +private func enqueueSecretChatUploadedMessageContent( + transaction: Transaction, + peerId: PeerId, + state: SecretChatState, + content: PendingMessageUploadedContentAndReuploadInfo, + text: String, + attributes: [MessageAttribute], + media: [Media] +) -> SecretChatState? { + var secretFile: SecretChatOutgoingFile? + switch content.content { + case let .secretMedia(file, size, key): + if let fileReference = SecretChatOutgoingFileReference(file) { + secretFile = SecretChatOutgoingFile(reference: fileReference, size: size, key: key) + } + default: + break + } + + let layer: SecretChatLayer + switch state.embeddedState { + case .terminated, .handshake: + return nil + case .basicLayer: + layer = .layer8 + case let .sequenceBasedLayer(sequenceState): + layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer + } + + let messageContents = StandaloneSecretMessageContents( + id: Int64.random(in: Int64.min ... Int64.max), + text: text, + attributes: attributes, + media: media.first, + file: secretFile + ) + + let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .sendStandaloneMessage(layer: layer, contents: messageContents), state: state) + if updatedState != state { + transaction.setPeerChatState(peerId, state: updatedState) + } + + return updatedState +} + private func sendUploadedMessageContent( - auxiliaryMethods: AccountAuxiliaryMethods, postbox: Postbox, network: Network, stateManager: AccountStateManager, @@ -271,55 +361,7 @@ private func sendUploadedMessageContent( ) -> Signal { return postbox.transaction { transaction -> Signal in if peerId.namespace == Namespaces.Peer.SecretChat { - var secretFile: SecretChatOutgoingFile? - switch content.content { - case let .secretMedia(file, size, key): - if let fileReference = SecretChatOutgoingFileReference(file) { - secretFile = SecretChatOutgoingFile(reference: fileReference, size: size, key: key) - } - default: - break - } - - var layer: SecretChatLayer? - let state = transaction.getPeerChatState(peerId) as? SecretChatState - if let state = state { - switch state.embeddedState { - case .terminated, .handshake: - break - case .basicLayer: - layer = .layer8 - case let .sequenceBasedLayer(sequenceState): - layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer - } - } - - if let state = state, let layer = layer { - let messageContents = StandaloneSecretMessageContents( - id: Int64.random(in: Int64.min ... Int64.max), - text: text, - attributes: attributes, - media: media.first, - file: secretFile - ) - - let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .sendStandaloneMessage(layer: layer, contents: messageContents), state: state) - if updatedState != state { - transaction.setPeerChatState(peerId, state: updatedState) - } - - return managedSecretChatOutgoingOperations( - auxiliaryMethods: auxiliaryMethods, - postbox: postbox, - network: network, - accountPeerId: accountPeerId, - mode: .standaloneComplete(peerId: peerId) - ) - |> castError(StandaloneSendMessagesError.self) - |> ignoreValues - } else { - return .fail(StandaloneSendMessagesError(peerId: peerId, reason: .none)) - } + return .fail(StandaloneSendMessagesError(peerId: peerId, reason: .none)) } else if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { var uniqueId: Int64 = 0 var forwardSourceInfoAttribute: ForwardSourceInfoAttribute? diff --git a/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift b/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift index e8495e3b02..b12e59e661 100644 --- a/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift +++ b/submodules/TelegramCore/Sources/Settings/PrivacySettings.swift @@ -11,6 +11,10 @@ public final class SelectivePrivacyPeer: Equatable { self.peer = peer self.participantCount = participantCount } + + public convenience init(peer: EnginePeer, participantCount: Int32?) { + self.init(peer: peer._asPeer(), participantCount: participantCount) + } public static func ==(lhs: SelectivePrivacyPeer, rhs: SelectivePrivacyPeer) -> Bool { if !lhs.peer.isEqual(rhs.peer) { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 555b9d62cb..7fde2c2909 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -2806,6 +2806,15 @@ func extractEmojiFileIds(message: StoreMessage, fileIds: inout Set) { } } } + for media in message.media { + if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content { + for attribute in content.attributes { + if case let .aiTextStyle(aiTextStyle) = attribute { + fileIds.insert(aiTextStyle.emojiFileId) + } + } + } + } } private func messagesFromOperations(state: AccountMutableState) -> [StoreMessage] { @@ -3309,7 +3318,7 @@ func resetChannels(accountPeerId: PeerId, postbox: Postbox, network: Network, pe resetForumTopics.insert(peerId) } - + for message in messages { var peerIsForum = false if let peerId = message.peerId { @@ -3943,6 +3952,7 @@ func replayFinalState( var updatedStarGiftAuctionState: [Int64: GiftAuctionContext.State.AuctionState] = [:] var updatedStarGiftAuctionMyState: [Int64: GiftAuctionContext.State.MyState] = [:] var updatedEmojiGameInfo: EmojiGameInfo? + var recentlyUsedGuestChatBots = Set() var holesFromPreviousStateMessageIds: [MessageId] = [] var clearHolesFromPreviousStateForChannelMessagesWithPts: [PeerIdAndMessageNamespace: Int32] = [:] @@ -4008,10 +4018,10 @@ func replayFinalState( } case update(Update) - case cancel + case cancel(updatedTimestamp: Int32) } - var liveTypingDraftUpdates: [PeerAndThreadId: LiveTypingDraftUpdate] = [:] + var liveTypingDraftUpdates: [PeerAndThreadId: [LiveTypingDraftUpdate]] = [:] for operation in finalState.state.operations { switch operation { @@ -4226,11 +4236,11 @@ func replayFinalState( let allKey = PeerAndThreadId(peerId: chatPeerId, threadId: nil) if liveTypingDraftUpdates[key] != nil { - liveTypingDraftUpdates[key] = .cancel - liveTypingDraftUpdates[allKey] = .cancel + liveTypingDraftUpdates[key] = [.cancel(updatedTimestamp: message.timestamp)] + liveTypingDraftUpdates[allKey] = [.cancel(updatedTimestamp: message.timestamp)] } else if let currentDraft = transaction.getCurrentTypingDraft(location: key) { - liveTypingDraftUpdates[key] = .cancel - liveTypingDraftUpdates[allKey] = .cancel + liveTypingDraftUpdates[key] = [.cancel(updatedTimestamp: message.timestamp)] + liveTypingDraftUpdates[allKey] = [.cancel(updatedTimestamp: message.timestamp)] messages[i] = messages[i].withUpdatedCustomStableId(currentDraft.stableId) } } @@ -4341,6 +4351,15 @@ func replayFinalState( } } } + + if message.flags.contains(.Incoming), let authorId = message.authorId { + for attribute in message.attributes { + if let attribute = attribute as? GuestChatMessageAttribute, attribute.peerId == accountPeerId { + recentlyUsedGuestChatBots.insert(authorId) + break + } + } + } } if !message.flags.contains(.Incoming) && !message.flags.contains(.Unsent) { if message.id.peerId.namespace == Namespaces.Peer.CloudChannel { @@ -4350,7 +4369,7 @@ func replayFinalState( if !message.flags.contains(.Incoming), message.forwardInfo == nil { if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), let peer = transaction.getPeer(message.id.peerId), peer.isCopyProtectionEnabled { - + } else if message.id.peerId.namespace == Namespaces.Peer.CloudUser, let cachedUserData = transaction.getPeerCachedData(peerId: message.id.peerId) as? CachedUserData, cachedUserData.flags.contains(.copyProtectionEnabled) || cachedUserData.flags.contains(.myCopyProtectionEnabled) { } else { @@ -4515,7 +4534,7 @@ func replayFinalState( if let apiPoll = apiPoll { switch apiPoll { case let .poll(pollData): - let (id, flags, question, answers, closePeriod, closeDate, pollHash) = (pollData.id, pollData.flags, pollData.question, pollData.answers, pollData.closePeriod, pollData.closeDate, pollData.hash) + let (id, flags, question, answers, closePeriod, closeDate, pollHash, countries) = (pollData.id, pollData.flags, pollData.question, pollData.answers, pollData.closePeriod, pollData.closeDate, pollData.hash, pollData.countriesIso2) let publicity: TelegramMediaPollPublicity if (flags & (1 << 1)) != 0 { publicity = .public @@ -4533,6 +4552,7 @@ func replayFinalState( let shuffleAnswers = (flags & (1 << 8)) != 0 let hideResultsUntilClose = (flags & (1 << 9)) != 0 let isCreator = (flags & (1 << 10)) != 0 + let restrictToSubscribers = (flags & (1 << 11)) != 0 let questionText: String let questionEntities: [MessageTextEntity] @@ -4543,7 +4563,7 @@ func replayFinalState( questionEntities = messageTextEntitiesFromApiEntities(entities) } - updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: poll.results, isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod, deadlineDate: closeDate, pollHash: pollHash, openAnswers: openAnswers, revotingDisabled: revotingDisabled, shuffleAnswers: shuffleAnswers, hideResultsUntilClose: hideResultsUntilClose, isCreator: isCreator, attachedMedia: poll.attachedMedia) + updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: poll.results, isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod, deadlineDate: closeDate, pollHash: pollHash, openAnswers: openAnswers, revotingDisabled: revotingDisabled, shuffleAnswers: shuffleAnswers, hideResultsUntilClose: hideResultsUntilClose, isCreator: isCreator, attachedMedia: poll.attachedMedia, restrictToSubscribers: restrictToSubscribers, countries: countries ?? []) } } updatedPoll = updatedPoll.withUpdatedResults(TelegramMediaPollResults(apiResults: results), min: resultsMin) @@ -4930,23 +4950,30 @@ func replayFinalState( updatedSecretChatTypingActivities.insert(chatPeerId.peerId) } case let .AddPeerLiveTypingDraftUpdate(peerAndThreadId, id, timestamp, authorId, text, entities): - liveTypingDraftUpdates[peerAndThreadId] = .update(LiveTypingDraftUpdate.Update( + if liveTypingDraftUpdates[peerAndThreadId] == nil { + liveTypingDraftUpdates[peerAndThreadId] = [] + } + liveTypingDraftUpdates[peerAndThreadId]?.append(.update(LiveTypingDraftUpdate.Update( id: id, threadId: peerAndThreadId.threadId, authorId: authorId, timestamp: timestamp, text: text, entities: entities - )) + ))) if peerAndThreadId.threadId != nil { - liveTypingDraftUpdates[PeerAndThreadId(peerId: peerAndThreadId.peerId, threadId: nil)] = .update(LiveTypingDraftUpdate.Update( + let allKey = PeerAndThreadId(peerId: peerAndThreadId.peerId, threadId: nil) + if liveTypingDraftUpdates[allKey] == nil { + liveTypingDraftUpdates[allKey] = [] + } + liveTypingDraftUpdates[allKey]?.append(.update(LiveTypingDraftUpdate.Update( id: id, threadId: peerAndThreadId.threadId, authorId: authorId, timestamp: timestamp, text: text, entities: entities - )) + ))) } case let .UpdatePinnedItemIds(groupId, pinnedOperation): switch pinnedOperation { @@ -5859,6 +5886,10 @@ func replayFinalState( } } + for peerId in recentlyUsedGuestChatBots { + _internal_addRecentlyUsedInlineBot(transaction: transaction, peerId: peerId) + } + if syncAttachMenuBots { // addSynchronizeAttachMenuBotsOperation(transaction: transaction) } @@ -5881,6 +5912,7 @@ func replayFinalState( var addedSecretMessageIds: [MessageId] = [] var addedSecretMessageAuthorIds: [PeerId: PeerId] = [:] + let keepArchivedUnmuted = fetchGlobalPrivacySettings(transaction: transaction).keepArchivedUnmuted for peerId in peerIdsWithAddedSecretMessages { inner: while true { @@ -5889,7 +5921,7 @@ func replayFinalState( let processResult = processSecretChatIncomingDecryptedOperations(encryptionProvider: encryptionProvider, mediaBox: mediaBox, transaction: transaction, peerId: peerId) if !processResult.addedMessages.isEmpty { let currentInclusion = transaction.getPeerChatListInclusion(peerId) - if let groupId = currentInclusion.groupId, groupId == Namespaces.PeerGroup.archive { + if let groupId = currentInclusion.groupId, groupId == Namespaces.PeerGroup.archive, !keepArchivedUnmuted { if let peer = transaction.getPeer(peerId) as? TelegramSecretChat { let isRemovedFromTotalUnreadCount = resolvedIsRemovedFromTotalUnreadCount(globalSettings: transaction.getGlobalNotificationSettings(), peer: peer, peerSettings: transaction.getPeerNotificationSettings(id: peer.regularPeerId)) @@ -6048,18 +6080,59 @@ func replayFinalState( } if !liveTypingDraftUpdates.isEmpty { + for (key, updates) in liveTypingDraftUpdates { + if key.threadId == nil { + var maxCancelledTimestamp: Int32? + for update in updates { + if case let .cancel(updatedTimestamp) = update { + if let current = maxCancelledTimestamp { + maxCancelledTimestamp = max(current, updatedTimestamp) + } else { + maxCancelledTimestamp = updatedTimestamp + } + } + } + if let maxCancelledTimestamp { + transaction.offsetPendingMessagesTimestamps(lowerBound: MessageId(peerId: key.peerId, namespace: Namespaces.Message.Local, id: 1), excludeIds: Set(), timestamp: maxCancelledTimestamp) + } + } + } transaction.combineTypingDrafts(locations: Set(liveTypingDraftUpdates.keys), update: { key, current in - guard let update = liveTypingDraftUpdates[key] else { + guard let update = liveTypingDraftUpdates[key]?.max(by: { lhs, rhs in + switch lhs { + case .cancel: + return false + case let .update(lhsUpdate): + switch rhs { + case .cancel: + return true + case let .update(rhsUpdate): + return lhsUpdate.timestamp < rhsUpdate.timestamp + } + } + }) else { return current } switch update { case let .update(update): + if let current, current.id > update.id { + return current + } + var timestamp = update.timestamp + if let current, current.id == update.id { + timestamp = current.timestamp + } + if current == nil { + if let index = transaction.getTopPeerMessageIndex(peerId: key.peerId) { + timestamp = max(timestamp, index.timestamp) + } + } return ( update.id, Namespaces.Message.Cloud, update.threadId, update.authorId, - update.timestamp, + timestamp, update.text, [ TypingDraftMessageAttribute(), diff --git a/submodules/TelegramCore/Sources/State/AccountStateManager.swift b/submodules/TelegramCore/Sources/State/AccountStateManager.swift index eea995bc40..b559213273 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManager.swift @@ -360,6 +360,11 @@ public final class AccountStateManager { return self.starRefBotConnectionEventsPipe.signal() } + fileprivate let installedStickerPacksArchivedEventsPipe = ValuePipe() + var installedStickerPacksArchivedEvents: Signal { + return self.installedStickerPacksArchivedEventsPipe.signal() + } + private var updatedWebpageContexts: [MediaId: UpdatedWebpageSubscriberContext] = [:] private var updatedPeersNearbyContext = UpdatedPeersNearbySubscriberContext() private var updatedStarsBalanceContext = UpdatedStarsBalanceSubscriberContext() @@ -2036,6 +2041,18 @@ public final class AccountStateManager { } } + public var installedStickerPacksArchivedEvents: Signal { + return self.impl.signalWith { impl, subscriber in + return impl.installedStickerPacksArchivedEvents.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) + } + } + + func installedStickerPacksArchived(count: Int) { + self.impl.with { impl in + impl.installedStickerPacksArchivedEventsPipe.putNext(count) + } + } + var botPreviewUpdates: Signal<[InternalBotPreviewUpdate], NoError> { return self.impl.signalWith { impl, subscriber in return impl.botPreviewUpdates.start(next: subscriber.putNext, error: subscriber.putError, completed: subscriber.putCompletion) diff --git a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift index 84ba2acf28..087214ebb7 100644 --- a/submodules/TelegramCore/Sources/State/AccountTaskManager.swift +++ b/submodules/TelegramCore/Sources/State/AccountTaskManager.swift @@ -127,6 +127,7 @@ final class AccountTaskManager { tasks.add(managedStarGiftsUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start()) tasks.add(managedSavedMusicIdsUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start()) tasks.add(managedEmojiGameUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) + tasks.add(managedSynchronizeCloudAITextStyles(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) self.managedTopReactionsDisposable.set(managedTopReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start()) 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() diff --git a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift index 1ebe2f011e..5e2cdf1b07 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSecretChatOutgoingOperations.swift @@ -1889,7 +1889,7 @@ private func sendStandaloneMessage(auxiliaryMethods: AccountAuxiliaryMethods, po var attributes = contents.attributes if !attributes.contains(where: { $0 is AutoremoveTimeoutMessageAttribute }), let messageAutoremoveTimeout = state.messageAutoremoveTimeout { - attributes.append(AutoclearTimeoutMessageAttribute(timeout: messageAutoremoveTimeout, countdownBeginTime: nil)) + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: messageAutoremoveTimeout, countdownBeginTime: nil)) } let message = Message( diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift index d7e1728e67..58c7a11fef 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift @@ -228,49 +228,60 @@ private func resolveStickerPacks(network: Network, remoteInfos: [ItemCollectionI } } -private func installRemoteStickerPacks(network: Network, infos: [StickerPackCollectionInfo]) -> Signal, NoError> { - var signals: [Signal, NoError>] = [] +private struct InstallRemoteStickerPacksResult { + var ids: Set + var archivedCount: Int + + init(ids: Set, archivedCount: Int) { + self.ids = ids + self.archivedCount = archivedCount + } +} + +private func installRemoteStickerPacks(network: Network, infos: [StickerPackCollectionInfo]) -> Signal { + var signals: [Signal] = [] for info in infos { let install = network.request(Api.functions.messages.installStickerSet(stickerset: .inputStickerSetID(.init(id: info.id.id, accessHash: info.accessHash)), archived: .boolFalse)) - |> map { result -> Set in - switch result { - case .stickerSetInstallResultSuccess: - return Set() - case let .stickerSetInstallResultArchive(stickerSetInstallResultArchiveData): - let archivedSets = stickerSetInstallResultArchiveData.sets - var archivedIds = Set() - for archivedSet in archivedSets { - switch archivedSet { - case let .stickerSetCovered(stickerSetCoveredData): - let set = stickerSetCoveredData.set - archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) - case let .stickerSetMultiCovered(stickerSetMultiCoveredData): - let set = stickerSetMultiCoveredData.set - archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) - case let .stickerSetFullCovered(stickerSetFullCoveredData): - let set = stickerSetFullCoveredData.set - archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) - case let .stickerSetNoCovered(stickerSetNoCoveredData): - let set = stickerSetNoCoveredData.set - archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) - } - } - return archivedIds + |> map { result -> InstallRemoteStickerPacksResult in + switch result { + case .stickerSetInstallResultSuccess: + return InstallRemoteStickerPacksResult(ids: Set(), archivedCount: 0) + case let .stickerSetInstallResultArchive(stickerSetInstallResultArchiveData): + let archivedSets = stickerSetInstallResultArchiveData.sets + var archivedIds = Set() + for archivedSet in archivedSets { + switch archivedSet { + case let .stickerSetCovered(stickerSetCoveredData): + let set = stickerSetCoveredData.set + archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) + case let .stickerSetMultiCovered(stickerSetMultiCoveredData): + let set = stickerSetMultiCoveredData.set + archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) + case let .stickerSetFullCovered(stickerSetFullCoveredData): + let set = stickerSetFullCoveredData.set + archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) + case let .stickerSetNoCovered(stickerSetNoCoveredData): + let set = stickerSetNoCoveredData.set + archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) + } } + return InstallRemoteStickerPacksResult(ids: archivedIds, archivedCount: archivedIds.count) } - |> `catch` { _ -> Signal, NoError> in - return .single(Set()) - } + } + |> `catch` { _ -> Signal in + return .single(InstallRemoteStickerPacksResult(ids: Set(), archivedCount: 0)) + } signals.append(install) } return combineLatest(signals) - |> map { idsSets -> Set in - var result = Set() - for ids in idsSets { - result.formUnion(ids) - } - return result + |> map { results -> InstallRemoteStickerPacksResult in + var result = InstallRemoteStickerPacksResult(ids: Set(), archivedCount: 0) + for resultValue in results { + result.ids.formUnion(resultValue.ids) + result.archivedCount += resultValue.archivedCount } + return result + } } private func removeRemoteStickerPacks(network: Network, infos: [StickerPackCollectionInfo]) -> Signal { @@ -341,12 +352,12 @@ private func reorderRemoteStickerPacks(network: Network, namespace: SynchronizeI private func synchronizeInstalledStickerPacks(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, namespace: SynchronizeInstalledStickerPacksOperationNamespace, operation: SynchronizeInstalledStickerPacksOperation) -> Signal { let collectionNamespace: ItemCollectionId.Namespace switch namespace { - case .stickers: - collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks - case .masks: - collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks - case .emoji: - collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks + case .stickers: + collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks + case .masks: + collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks + case .emoji: + collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks } let localCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo } @@ -466,12 +477,12 @@ func debugFetchAllStickers(account: Account) -> Signal { private func continueSynchronizeInstalledStickerPacks(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, namespace: SynchronizeInstalledStickerPacksOperationNamespace, operation: SynchronizeInstalledStickerPacksOperation) -> Signal { let collectionNamespace: ItemCollectionId.Namespace switch namespace { - case .stickers: - collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks - case .masks: - collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks - case .emoji: - collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks + case .stickers: + collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks + case .masks: + collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks + case .emoji: + collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks } let localCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo } @@ -479,12 +490,12 @@ private func continueSynchronizeInstalledStickerPacks(transaction: Transaction, let request: Signal switch namespace { - case .stickers: - request = network.request(Api.functions.messages.getAllStickers(hash: initialLocalHash)) - case .masks: - request = network.request(Api.functions.messages.getMaskStickers(hash: initialLocalHash)) - case .emoji: - request = network.request(Api.functions.messages.getEmojiStickers(hash: initialLocalHash)) + case .stickers: + request = network.request(Api.functions.messages.getAllStickers(hash: initialLocalHash)) + case .masks: + request = network.request(Api.functions.messages.getMaskStickers(hash: initialLocalHash)) + case .emoji: + request = network.request(Api.functions.messages.getEmojiStickers(hash: initialLocalHash)) } let sequence = request @@ -661,7 +672,12 @@ private func continueSynchronizeInstalledStickerPacks(transaction: Transaction, |> then(Signal.single(Void()))) |> mapToSignal { _ -> Signal, NoError> in return installRemoteStickerPacks(network: network, infos: addRemoteCollectionInfos) - |> mapToSignal { ids -> Signal, NoError> in + |> mapToSignal { result -> Signal, NoError> in + let ids = result.ids + if result.archivedCount != 0 { + stateManager.installedStickerPacksArchived(count: result.archivedCount) + } + return (reorderRemoteStickerPacks(network: network, namespace: namespace, ids: resultingCollectionInfos.map({ $0.0.id }).filter({ !ids.contains($0) })) |> then(Signal.single(Void()))) |> map { _ -> Set in diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index 0434b0e286..951b02d2c4 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -31,6 +31,8 @@ private enum PendingMessageState { case waitingForUploadToStart(groupId: Int64?, upload: Signal) case uploading(groupId: Int64?) case waitingToBeSent(groupId: Int64?, content: PendingMessageUploadedContentAndReuploadInfo) + case waitingForSendGate(groupId: Int64?, content: PendingMessageUploadedContentAndReuploadInfo) + case waitingForForwardSendGate case sending(groupId: Int64?) case waitingForNewTopic(message: Message) @@ -46,6 +48,10 @@ private enum PendingMessageState { return groupId case let .waitingToBeSent(groupId, _): return groupId + case let .waitingForSendGate(groupId, _): + return groupId + case .waitingForForwardSendGate: + return nil case let .sending(groupId): return groupId case let .waitingForNewTopic(message): @@ -117,7 +123,7 @@ public struct PeerPendingMessageDelivered { private final class PeerPendingMessagesSummaryContext { var messageDeliveredSubscribers = Bag<([PeerPendingMessageDelivered]) -> Void>() - var messageFailedSubscribers = Bag<(PendingMessageFailureReason) -> Void>() + var messageFailedSubscribers = Bag<(MessageId.Namespace, PendingMessageFailureReason) -> Void>() var newTopicEvents = Bag<(PendingMessageManager.NewTopicEvent) -> Void>() var isEmpty: Bool { @@ -230,6 +236,9 @@ public final class PendingMessageManager { private var messageContexts: [MessageId: PendingMessageContext] = [:] private var pendingMessageIds = Set() + private var liveTypingDraftKeys: Set = [] + private let allTypingDraftsDisposable = MetaDisposable() + private var forwardSendGateGroups: [PeerAndThreadId: [[(PendingMessageContext, Message, ForwardSourceInfoAttribute)]]] = [:] private let beginSendingMessagesDisposables = DisposableSet() private var newTopicDisposables: [PeerId: Disposable] = [:] @@ -250,6 +259,14 @@ public final class PendingMessageManager { self.localInputActivityManager = localInputActivityManager self.messageMediaPreuploadManager = messageMediaPreuploadManager self.revalidationContext = revalidationContext + + let queue = self.queue + self.allTypingDraftsDisposable.set( + (postbox.combinedView(keys: [.allTypingDrafts]) + |> deliverOn(queue)).start(next: { [weak self] view in + self?.handleLiveTypingDraftsUpdate(view) + }) + ) } deinit { @@ -257,8 +274,117 @@ public final class PendingMessageManager { for (_, disposable) in self.newTopicDisposables { disposable.dispose() } + self.allTypingDraftsDisposable.dispose() } + private func handleLiveTypingDraftsUpdate(_ combined: CombinedView) { + assert(self.queue.isCurrent()) + + let new: Set + if let view = combined.views[.allTypingDrafts] as? AllTypingDraftsView { + new = view.keys + } else { + new = [] + } + let cleared = self.liveTypingDraftKeys.subtracting(new) + self.liveTypingDraftKeys = new + for key in cleared { + self.drainSendGate(key: key) + } + } + + private func shouldGateSend(messageId: MessageId, threadId: Int64?) -> Bool { + if messageId.namespace == Namespaces.Message.ScheduledCloud { + return false + } + if messageId.peerId.namespace == Namespaces.Peer.SecretChat { + return false + } + if messageId.peerId == self.accountPeerId { + return false + } + if threadId == Message.newTopicThreadId { + return false + } + return true + } + + private func isSendGateOpen(for key: PeerAndThreadId) -> Bool { + return !self.liveTypingDraftKeys.contains(key) + } + + private func drainSendGate(key: PeerAndThreadId) { + assert(self.queue.isCurrent()) + + // (1) Single-message drain: snapshot then commit in messageId.id order. + var singleDrains: [(context: PendingMessageContext, messageId: MessageId, content: PendingMessageUploadedContentAndReuploadInfo)] = [] + for (id, context) in self.messageContexts { + if id.peerId != key.peerId { + continue + } + if context.threadId != key.threadId { + continue + } + if case let .waitingForSendGate(groupId, content) = context.state, groupId == nil { + singleDrains.append((context, id, content)) + } + } + singleDrains.sort(by: { $0.messageId.id < $1.messageId.id }) + for entry in singleDrains { + self.commitSendingSingleMessage(messageContext: entry.context, messageId: entry.messageId, content: entry.content) + } + + // (2) Grouped-album drain: collect distinct groupIds whose members match the key, + // iterate ascending by min messageId.id, fire commitSendingMessageGroup. + var groupKeys: [(groupId: Int64, minMessageId: Int32)] = [] + var seenGroupIds = Set() + for (id, context) in self.messageContexts { + if id.peerId != key.peerId { + continue + } + if context.threadId != key.threadId { + continue + } + if case let .waitingForSendGate(groupId, _) = context.state, let groupId = groupId { + if !seenGroupIds.contains(groupId) { + seenGroupIds.insert(groupId) + groupKeys.append((groupId, id.id)) + } else { + if let index = groupKeys.firstIndex(where: { $0.groupId == groupId }), id.id < groupKeys[index].minMessageId { + groupKeys[index].minMessageId = id.id + } + } + } + } + groupKeys.sort(by: { $0.minMessageId < $1.minMessageId }) + for (groupId, _) in groupKeys { + if let data = self.dataForPendingMessageGroup(groupId) { + self.commitSendingMessageGroup(groupId: groupId, messages: data) + } + } + + // (3) Forward drain: pop parked groups for this key in FIFO order; fire each. + if let parkedGroups = self.forwardSendGateGroups.removeValue(forKey: key) { + for messages in parkedGroups { + for (context, _, _) in messages { + context.state = .sending(groupId: nil) + } + let sendMessage: Signal = self.sendGroupMessagesContent(network: self.network, postbox: self.postbox, stateManager: self.stateManager, accountPeerId: self.accountPeerId, group: messages.map { data in + let (_, message, forwardInfo) = data + return (message.id, PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil, cacheReferenceKey: nil)) + }) + |> map { _ -> PendingMessageResult in + return .progress(1.0) + } + messages[0].0.sendDisposable.set((sendMessage + |> deliverOn(self.queue)).start()) + } + } + + self.updateWaitingUploads(peerId: key.peerId) + self.updatePendingMediaUploads() + } + private func updatePendingMediaUploads() { assert(self.queue.isCurrent()) @@ -321,7 +447,29 @@ public final class PendingMessageManager { } } } - + + if !removedMessageIds.isEmpty && !self.forwardSendGateGroups.isEmpty { + for key in Array(self.forwardSendGateGroups.keys) { + guard let parkedGroups = self.forwardSendGateGroups[key] else { + continue + } + var rebuilt: [[(PendingMessageContext, Message, ForwardSourceInfoAttribute)]] = [] + for group in parkedGroups { + let filtered = group.filter { entry in + return !removedMessageIds.contains(entry.1.id) + } + if !filtered.isEmpty { + rebuilt.append(filtered) + } + } + if rebuilt.isEmpty { + self.forwardSendGateGroups.removeValue(forKey: key) + } else { + self.forwardSendGateGroups[key] = rebuilt + } + } + } + if !addedMessageIds.isEmpty { Logger.shared.log("PendingMessageManager", "added messages: \(addedMessageIds)") self.beginSendingMessages(Array(addedMessageIds).sorted()) @@ -715,11 +863,24 @@ public final class PendingMessageManager { if messages.isEmpty { continue } - + + let firstMessage = messages[0].1 + let key = PeerAndThreadId(peerId: firstMessage.id.peerId, threadId: firstMessage.threadId) + if strongSelf.shouldGateSend(messageId: firstMessage.id, threadId: firstMessage.threadId) && !strongSelf.isSendGateOpen(for: key) { + for (context, _, _) in messages { + context.state = .waitingForForwardSendGate + } + if strongSelf.forwardSendGateGroups[key] == nil { + strongSelf.forwardSendGateGroups[key] = [] + } + strongSelf.forwardSendGateGroups[key]!.append(messages) + continue + } + for (context, _, _) in messages { context.state = .sending(groupId: nil) } - + let sendMessage: Signal = strongSelf.sendGroupMessagesContent(network: strongSelf.network, postbox: strongSelf.postbox, stateManager: strongSelf.stateManager, accountPeerId: strongSelf.accountPeerId, group: messages.map { data in let (_, message, forwardInfo) = data return (message.id, PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil, cacheReferenceKey: nil)) @@ -739,7 +900,12 @@ public final class PendingMessageManager { if let groupId = groupId { messageContext.state = .waitingToBeSent(groupId: groupId, content: content) } else { - self.commitSendingSingleMessage(messageContext: messageContext, messageId: messageId, content: content) + let key = PeerAndThreadId(peerId: messageId.peerId, threadId: messageContext.threadId) + if self.shouldGateSend(messageId: messageId, threadId: messageContext.threadId) && !self.isSendGateOpen(for: key) { + messageContext.state = .waitingForSendGate(groupId: nil, content: content) + } else { + self.commitSendingSingleMessage(messageContext: messageContext, messageId: messageId, content: content) + } } self.updatePendingMediaUploads() } @@ -757,6 +923,8 @@ public final class PendingMessageManager { switch context.state { case .none: continue loop + case .waitingForForwardSendGate: + continue loop case let .collectingInfo(message): if message.groupingKey == groupId { return nil @@ -773,6 +941,10 @@ public final class PendingMessageManager { if contextGroupId == groupId { result.append((context, id, content)) } + case let .waitingForSendGate(contextGroupId, content): + if contextGroupId == groupId { + result.append((context, id, content)) + } case let .sending(contextGroupId): if contextGroupId == groupId { return nil @@ -792,6 +964,16 @@ public final class PendingMessageManager { } private func commitSendingMessageGroup(groupId: Int64, messages: [(messageContext: PendingMessageContext, messageId: MessageId, content: PendingMessageUploadedContentAndReuploadInfo)]) { + let firstMessageId = messages[0].messageId + let firstThreadId = messages[0].messageContext.threadId + let key = PeerAndThreadId(peerId: firstMessageId.peerId, threadId: firstThreadId) + if self.shouldGateSend(messageId: firstMessageId, threadId: firstThreadId) && !self.isSendGateOpen(for: key) { + for entry in messages { + entry.messageContext.state = .waitingForSendGate(groupId: groupId, content: entry.content) + } + return + } + for (context, _, _) in messages { context.state = .sending(groupId: groupId) } @@ -1423,7 +1605,7 @@ public final class PendingMessageManager { if let context = strongSelf.peerSummaryContexts[message.id.peerId] { for subscriber in context.messageFailedSubscribers.copyItems() { - subscriber(failureReason) + subscriber(message.id.namespace, failureReason) } } } @@ -2049,7 +2231,7 @@ public final class PendingMessageManager { if let context = strongSelf.peerSummaryContexts[message.id.peerId] { for subscriber in context.messageFailedSubscribers.copyItems() { - subscriber(failureReason) + subscriber(message.id.namespace, failureReason) } } } @@ -2252,7 +2434,7 @@ public final class PendingMessageManager { } } - public func failedMessageEvents(peerId: PeerId) -> Signal { + public func failedMessageEvents(peerId: PeerId, isScheduled: Bool) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() @@ -2265,8 +2447,16 @@ public final class PendingMessageManager { self.peerSummaryContexts[peerId] = summaryContext } - let index = summaryContext.messageFailedSubscribers.add({ reason in - subscriber.putNext(reason) + let index = summaryContext.messageFailedSubscribers.add({ namespace, reason in + if isScheduled { + if Namespaces.Message.allScheduled.contains(namespace) { + subscriber.putNext(reason) + } + } else { + if !Namespaces.Message.allScheduled.contains(namespace) { + subscriber.putNext(reason) + } + } }) disposable.set(ActionDisposable { diff --git a/submodules/TelegramCore/Sources/State/PremiumRequiredToContact.swift b/submodules/TelegramCore/Sources/State/PremiumRequiredToContact.swift index d90c362ca2..d471cf313f 100644 --- a/submodules/TelegramCore/Sources/State/PremiumRequiredToContact.swift +++ b/submodules/TelegramCore/Sources/State/PremiumRequiredToContact.swift @@ -12,6 +12,9 @@ internal func _internal_updateIsPremiumRequiredToContact(account: Account, peerI var inputUsers: [Api.InputUser] = [] var ids: [PeerId] = [] for id in peerIds { + guard id != account.peerId else { + continue + } if let peer = transaction.getPeer(id), let inputUser = apiInputUser(peer) { if peer.isPremium { if let cachedData = transaction.getPeerCachedData(peerId: id) as? CachedUserData { diff --git a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift index 2867a94d87..425e5617da 100644 --- a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift +++ b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift @@ -448,7 +448,7 @@ extension SecretChatServiceAction { self = .rekeyAction(.pfsCommitKey(rekeySessionId: exchangeId, keyFingerprint: keyFingerprint)) case let .decryptedMessageActionAbortKey(exchangeId): self = .rekeyAction(.pfsAbortSession(rekeySessionId: exchangeId)) - case .decryptedMessageActionNoop: + case .decryptedMessageActionNoop, .decryptedMessageActionTyping: return nil } } @@ -484,7 +484,7 @@ extension SecretChatServiceAction { self = .rekeyAction(.pfsCommitKey(rekeySessionId: exchangeId, keyFingerprint: keyFingerprint)) case let .decryptedMessageActionAbortKey(exchangeId): self = .rekeyAction(.pfsAbortSession(rekeySessionId: exchangeId)) - case .decryptedMessageActionNoop: + case .decryptedMessageActionNoop, .decryptedMessageActionTyping: return nil } } @@ -520,7 +520,7 @@ extension SecretChatServiceAction { self = .rekeyAction(.pfsCommitKey(rekeySessionId: exchangeId, keyFingerprint: keyFingerprint)) case let .decryptedMessageActionAbortKey(exchangeId): self = .rekeyAction(.pfsAbortSession(rekeySessionId: exchangeId)) - case .decryptedMessageActionNoop: + case .decryptedMessageActionNoop, .decryptedMessageActionTyping: return nil } } @@ -556,7 +556,7 @@ extension SecretChatServiceAction { self = .rekeyAction(.pfsCommitKey(rekeySessionId: exchangeId, keyFingerprint: keyFingerprint)) case let .decryptedMessageActionAbortKey(exchangeId): self = .rekeyAction(.pfsAbortSession(rekeySessionId: exchangeId)) - case .decryptedMessageActionNoop: + case .decryptedMessageActionNoop, .decryptedMessageActionTyping: return nil } } @@ -919,7 +919,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 return nil case .decryptedMessageActionAbortKey: return nil - case .decryptedMessageActionNoop: + case .decryptedMessageActionNoop, .decryptedMessageActionTyping: return nil } } @@ -1151,7 +1151,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 return nil case .decryptedMessageActionAbortKey: return nil - case .decryptedMessageActionNoop: + case .decryptedMessageActionNoop, .decryptedMessageActionTyping: return nil } } @@ -1430,7 +1430,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 return nil case .decryptedMessageActionAbortKey: return nil - case .decryptedMessageActionNoop: + case .decryptedMessageActionNoop, .decryptedMessageActionTyping: return nil } } @@ -1631,7 +1631,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 return nil case .decryptedMessageActionAbortKey: return nil - case .decryptedMessageActionNoop: + case .decryptedMessageActionNoop, .decryptedMessageActionTyping: return nil } } diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 913e49ab09..832c5530cc 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -260,7 +260,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 224 + return 225 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/State/UpdateMessageService.swift b/submodules/TelegramCore/Sources/State/UpdateMessageService.swift index 5207a6c23c..3a7916a82e 100644 --- a/submodules/TelegramCore/Sources/State/UpdateMessageService.swift +++ b/submodules/TelegramCore/Sources/State/UpdateMessageService.swift @@ -62,7 +62,7 @@ class UpdateMessageService: NSObject, MTMessageService { } case let .updateShortChatMessage(updateShortChatMessageData): let (flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, ttlPeriod) = (updateShortChatMessageData.flags, updateShortChatMessageData.id, updateShortChatMessageData.fromId, updateShortChatMessageData.chatId, updateShortChatMessageData.message, updateShortChatMessageData.pts, updateShortChatMessageData.ptsCount, updateShortChatMessageData.date, updateShortChatMessageData.fwdFrom, updateShortChatMessageData.viaBotId, updateShortChatMessageData.replyTo, updateShortChatMessageData.entities, updateShortChatMessageData.ttlPeriod) - let generatedMessage = Api.Message.message(.init(flags: flags, flags2: 0, id: id, fromId: .peerUser(.init(userId: fromId)), fromBoostsApplied: nil, fromRank: nil, peerId: Api.Peer.peerChat(.init(chatId: chatId)), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil, summaryFromLanguage: nil)) + let generatedMessage = Api.Message.message(.init(flags: flags, flags2: 0, id: id, fromId: .peerUser(.init(userId: fromId)), fromBoostsApplied: nil, fromRank: nil, peerId: Api.Peer.peerChat(.init(chatId: chatId)), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, guestchatViaFrom: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil, summaryFromLanguage: nil)) let update = Api.Update.updateNewMessage(.init(message: generatedMessage, pts: pts, ptsCount: ptsCount)) let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil) if groups.count != 0 { @@ -79,7 +79,7 @@ class UpdateMessageService: NSObject, MTMessageService { let generatedPeerId = Api.Peer.peerUser(.init(userId: userId)) - let generatedMessage = Api.Message.message(.init(flags: flags, flags2: 0, id: id, fromId: generatedFromId, fromBoostsApplied: nil, fromRank: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil, summaryFromLanguage: nil)) + let generatedMessage = Api.Message.message(.init(flags: flags, flags2: 0, id: id, fromId: generatedFromId, fromBoostsApplied: nil, fromRank: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, guestchatViaFrom: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil, summaryFromLanguage: nil)) let update = Api.Update.updateNewMessage(.init(message: generatedMessage, pts: pts, ptsCount: ptsCount)) let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil) if groups.count != 0 { diff --git a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift index 04720616ac..69f123c044 100644 --- a/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift +++ b/submodules/TelegramCore/Sources/State/UserLimitsConfiguration.swift @@ -30,6 +30,7 @@ public struct UserLimitsConfiguration: Equatable { public var maxChannelRecommendationsCount: Int32 public var maxConferenceParticipantCount: Int32 public var maxBotsCreated: Int32 + public var maxOwnedAITextStyles: Int32 public static var defaultValue: UserLimitsConfiguration { return UserLimitsConfiguration( @@ -60,7 +61,8 @@ public struct UserLimitsConfiguration: Equatable { maxGiveawayPeriodSeconds: 86400 * 31, maxChannelRecommendationsCount: 10, maxConferenceParticipantCount: 100, - maxBotsCreated: 20 + maxBotsCreated: 20, + maxOwnedAITextStyles: 5 ) } @@ -92,7 +94,8 @@ public struct UserLimitsConfiguration: Equatable { maxGiveawayPeriodSeconds: Int32, maxChannelRecommendationsCount: Int32, maxConferenceParticipantCount: Int32, - maxBotsCreated: Int32 + maxBotsCreated: Int32, + maxOwnedAITextStyles: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxPinnedSavedChatCount = maxPinnedSavedChatCount @@ -122,6 +125,7 @@ public struct UserLimitsConfiguration: Equatable { self.maxChannelRecommendationsCount = maxChannelRecommendationsCount self.maxConferenceParticipantCount = maxConferenceParticipantCount self.maxBotsCreated = maxBotsCreated + self.maxOwnedAITextStyles = maxOwnedAITextStyles } } @@ -177,5 +181,6 @@ extension UserLimitsConfiguration { self.maxChannelRecommendationsCount = getValue("recommended_channels_limit", orElse: defaultValue.maxChannelRecommendationsCount) self.maxConferenceParticipantCount = getGeneralValue("conference_call_size_limit", orElse: defaultValue.maxConferenceParticipantCount) self.maxBotsCreated = getValue("bots_create_limit", orElse: defaultValue.maxBotsCreated) + self.maxOwnedAITextStyles = getValue("aicompose_tone_saved_limit", orElse: defaultValue.maxBotsCreated) } } diff --git a/submodules/TelegramCore/Sources/Statistics/PollStatistics.swift b/submodules/TelegramCore/Sources/Statistics/PollStatistics.swift new file mode 100644 index 0000000000..ff24a510a2 --- /dev/null +++ b/submodules/TelegramCore/Sources/Statistics/PollStatistics.swift @@ -0,0 +1,193 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramApi +import MtProtoKit + +public struct PollStats: Equatable { + public let votesGraph: StatsGraph + + init(votesGraph: StatsGraph) { + self.votesGraph = votesGraph + } + + public static func == (lhs: PollStats, rhs: PollStats) -> Bool { + if lhs.votesGraph != rhs.votesGraph { + return false + } + return true + } + + public func withUpdatedVotesGraph(_ votesGraph: StatsGraph) -> PollStats { + return PollStats(votesGraph: votesGraph) + } +} + +public struct PollStatsContextState: Equatable { + public var stats: PollStats? +} + +private func requestPollStats(postbox: Postbox, network: Network, messageId: MessageId, dark: Bool = false) -> Signal { + return postbox.transaction { transaction -> (Int32, Peer)? in + if let peer = transaction.getPeer(messageId.peerId){ + if let cachedData = transaction.getPeerCachedData(peerId: messageId.peerId) as? CachedChannelData, cachedData.statsDatacenterId != 0 { + return (cachedData.statsDatacenterId, peer) + } else { + return (Int32(network.datacenterId), peer) + } + } else { + return nil + } + } |> mapToSignal { data -> Signal in + guard let (datacenterId, peer) = data, let inputPeer = apiInputPeer(peer) else { + return .never() + } + + var flags: Int32 = 0 + if dark { + flags |= (1 << 1) + } + + let request = Api.functions.stats.getPollStats(flags: flags, peer: inputPeer, msgId: messageId.id) + let signal: Signal + if network.datacenterId != datacenterId { + signal = network.download(datacenterId: Int(datacenterId), isMedia: false, tag: nil) + |> castError(MTRpcError.self) + |> mapToSignal { worker in + return worker.request(request) + } + } else { + signal = network.request(request) + } + + return signal + |> mapToSignal { result -> Signal in + switch result { + case let .pollStats(pollStatsData): + let votesGraph = StatsGraph(apiStatsGraph: pollStatsData.votesGraph) + return .single(PollStats(votesGraph: votesGraph)) + } + } + |> retryRequest + } +} + +private final class PollStatsContextImpl { + private let postbox: Postbox + private let network: Network + private let messageId: MessageId + + private var _state: PollStatsContextState { + didSet { + if self._state != oldValue { + self._statePromise.set(.single(self._state)) + } + } + } + private let _statePromise = Promise() + var state: Signal { + return self._statePromise.get() + } + + private let disposable = MetaDisposable() + private let disposables = DisposableDict() + + init(postbox: Postbox, network: Network, messageId: MessageId) { + assert(Queue.mainQueue().isCurrent()) + + self.postbox = postbox + self.network = network + self.messageId = messageId + self._state = PollStatsContextState(stats: nil) + self._statePromise.set(.single(self._state)) + + self.load() + } + + deinit { + assert(Queue.mainQueue().isCurrent()) + self.disposable.dispose() + self.disposables.dispose() + } + + private func load() { + assert(Queue.mainQueue().isCurrent()) + + self.disposable.set((requestPollStats(postbox: self.postbox, network: self.network, messageId: self.messageId) + |> deliverOnMainQueue).start(next: { [weak self] stats in + if let strongSelf = self { + strongSelf._state = PollStatsContextState(stats: stats) + strongSelf._statePromise.set(.single(strongSelf._state)) + } + })) + } + + func loadVotesGraph() { + assert(Queue.mainQueue().isCurrent()) + + guard let stats = self._state.stats else { + return + } + if case let .OnDemand(token) = stats.votesGraph { + guard !token.isEmpty else { + return + } + self.disposables.set((requestGraph(postbox: self.postbox, network: self.network, peerId: self.messageId.peerId, token: token) + |> deliverOnMainQueue).start(next: { [weak self] graph in + if let strongSelf = self, let graph = graph { + strongSelf._state = PollStatsContextState(stats: strongSelf._state.stats?.withUpdatedVotesGraph(graph)) + strongSelf._statePromise.set(.single(strongSelf._state)) + } + }), forKey: token) + } + } + + func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal { + if let token = graph.token { + return requestGraph(postbox: self.postbox, network: self.network, peerId: self.messageId.peerId, token: token, x: x) + } else { + return .single(nil) + } + } +} + +public final class PollStatsContext { + private let impl: QueueLocalObject + + public var state: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.state.start(next: { value in + subscriber.putNext(value) + })) + } + return disposable + } + } + + public init(account: Account, messageId: MessageId) { + self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { + return PollStatsContextImpl(postbox: account.postbox, network: account.network, messageId: messageId) + }) + } + + public func loadVotesGraph() { + self.impl.with { impl in + impl.loadVotesGraph() + } + } + + public func loadDetailedGraph(_ graph: StatsGraph, x: Int64) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.loadDetailedGraph(graph, x: x).start(next: { value in + subscriber.putNext(value) + subscriber.putCompletion() + })) + } + return disposable + } + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/GuestChatMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/GuestChatMessageAttribute.swift new file mode 100644 index 0000000000..cb5b4d2511 --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/GuestChatMessageAttribute.swift @@ -0,0 +1,22 @@ +import Foundation +import Postbox + +public class GuestChatMessageAttribute: MessageAttribute { + public let peerId: EnginePeer.Id + + public var associatedPeerIds: [PeerId] { + return [self.peerId] + } + + public init(peerId: EnginePeer.Id) { + self.peerId = peerId + } + + required public init(decoder: PostboxDecoder) { + self.peerId = EnginePeer.Id(decoder.decodeInt64ForKey("p", orElse: 0)) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.peerId.toInt64(), forKey: "p") + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/ParticipantRankMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/ParticipantRankMessageAttribute.swift index e90d25c956..b4057267bb 100644 --- a/submodules/TelegramCore/Sources/SyncCore/ParticipantRankMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/ParticipantRankMessageAttribute.swift @@ -4,8 +4,6 @@ import Postbox public class ParticipantRankMessageAttribute: MessageAttribute { public let rank: String - public var associatedMessageIds: [MessageId] = [] - public init(rank: String) { self.rank = rank } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift index a721277bd6..0e0574d48b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift @@ -32,6 +32,8 @@ private enum InstantPageBlockType: Int32 { case details = 25 case relatedArticles = 26 case map = 27 + case heading = 28 + case formula = 29 } private func decodeListItems(_ decoder: PostboxDecoder) -> [InstantPageListItem] { @@ -60,8 +62,10 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { case authorDate(author: RichText, date: Int32) case header(RichText) case subheader(RichText) + case heading(text: RichText, level: Int32) + case formula(latex: String) case paragraph(RichText) - case preformatted(RichText) + case preformatted(text: RichText, language: String?) case footer(RichText) case divider case anchor(String) @@ -97,10 +101,17 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { self = .header(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) case InstantPageBlockType.subheader.rawValue: self = .subheader(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) + case InstantPageBlockType.heading.rawValue: + self = .heading(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, level: decoder.decodeInt32ForKey("l", orElse: 3)) + case InstantPageBlockType.formula.rawValue: + self = .formula(latex: decoder.decodeStringForKey("l", orElse: "")) case InstantPageBlockType.paragraph.rawValue: self = .paragraph(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) case InstantPageBlockType.preformatted.rawValue: - self = .preformatted(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) + self = .preformatted( + text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, + language: decoder.decodeOptionalStringForKey("l") + ) case InstantPageBlockType.footer.rawValue: self = .footer(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) case InstantPageBlockType.divider.rawValue: @@ -184,12 +195,24 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { case let .subheader(text): encoder.encodeInt32(InstantPageBlockType.subheader.rawValue, forKey: "r") encoder.encodeObject(text, forKey: "t") + case let .heading(text, level): + encoder.encodeInt32(InstantPageBlockType.heading.rawValue, forKey: "r") + encoder.encodeObject(text, forKey: "t") + encoder.encodeInt32(level, forKey: "l") + case let .formula(latex): + encoder.encodeInt32(InstantPageBlockType.formula.rawValue, forKey: "r") + encoder.encodeString(latex, forKey: "l") case let .paragraph(text): encoder.encodeInt32(InstantPageBlockType.paragraph.rawValue, forKey: "r") encoder.encodeObject(text, forKey: "t") - case let .preformatted(text): + case let .preformatted(text, language): encoder.encodeInt32(InstantPageBlockType.preformatted.rawValue, forKey: "r") encoder.encodeObject(text, forKey: "t") + if let language { + encoder.encodeString(language, forKey: "l") + } else { + encoder.encodeNil(forKey: "l") + } case let .footer(text): encoder.encodeInt32(InstantPageBlockType.footer.rawValue, forKey: "r") encoder.encodeObject(text, forKey: "t") @@ -374,14 +397,26 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { } else { return false } + case let .heading(lhsText, lhsLevel): + if case let .heading(rhsText, rhsLevel) = rhs, lhsText == rhsText, lhsLevel == rhsLevel { + return true + } else { + return false + } + case let .formula(lhsLatex): + if case let .formula(rhsLatex) = rhs, lhsLatex == rhsLatex { + return true + } else { + return false + } case let .paragraph(text): if case .paragraph(text) = rhs { return true } else { return false } - case let .preformatted(text): - if case .preformatted(text) = rhs { + case let .preformatted(lhsText, lhsLanguage): + if case let .preformatted(rhsText, rhsLanguage) = rhs, lhsText == rhsText, lhsLanguage == rhsLanguage { return true } else { return false @@ -545,6 +580,16 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { throw FlatBuffersError.missingRequiredField() } self = .subheader(try RichText(flatBuffersObject: value.text)) + case .instantpageblockHeading: + guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Heading.self) else { + throw FlatBuffersError.missingRequiredField() + } + self = .heading(text: try RichText(flatBuffersObject: value.text), level: value.level) + case .instantpageblockFormula: + guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Formula.self) else { + throw FlatBuffersError.missingRequiredField() + } + self = .formula(latex: value.latex) case .instantpageblockParagraph: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Paragraph.self) else { throw FlatBuffersError.missingRequiredField() @@ -554,7 +599,7 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Preformatted.self) else { throw FlatBuffersError.missingRequiredField() } - self = .preformatted(try RichText(flatBuffersObject: value.text)) + self = .preformatted(text: try RichText(flatBuffersObject: value.text), language: value.language) case .instantpageblockFooter: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Footer.self) else { throw FlatBuffersError.missingRequiredField() @@ -698,17 +743,34 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { let start = TelegramCore_InstantPageBlock_Subheader.startInstantPageBlock_Subheader(&builder) TelegramCore_InstantPageBlock_Subheader.add(text: textOffset, &builder) offset = TelegramCore_InstantPageBlock_Subheader.endInstantPageBlock_Subheader(&builder, start: start) + case let .heading(text, level): + valueType = .instantpageblockHeading + let textOffset = text.encodeToFlatBuffers(builder: &builder) + let start = TelegramCore_InstantPageBlock_Heading.startInstantPageBlock_Heading(&builder) + TelegramCore_InstantPageBlock_Heading.add(text: textOffset, &builder) + TelegramCore_InstantPageBlock_Heading.add(level: level, &builder) + offset = TelegramCore_InstantPageBlock_Heading.endInstantPageBlock_Heading(&builder, start: start) + case let .formula(latex): + valueType = .instantpageblockFormula + let latexOffset = builder.create(string: latex) + let start = TelegramCore_InstantPageBlock_Formula.startInstantPageBlock_Formula(&builder) + TelegramCore_InstantPageBlock_Formula.add(latex: latexOffset, &builder) + offset = TelegramCore_InstantPageBlock_Formula.endInstantPageBlock_Formula(&builder, start: start) case let .paragraph(text): valueType = .instantpageblockParagraph let textOffset = text.encodeToFlatBuffers(builder: &builder) let start = TelegramCore_InstantPageBlock_Paragraph.startInstantPageBlock_Paragraph(&builder) TelegramCore_InstantPageBlock_Paragraph.add(text: textOffset, &builder) offset = TelegramCore_InstantPageBlock_Paragraph.endInstantPageBlock_Paragraph(&builder, start: start) - case let .preformatted(text): + case let .preformatted(text, language): valueType = .instantpageblockPreformatted let textOffset = text.encodeToFlatBuffers(builder: &builder) + let languageOffset = language.flatMap { builder.create(string: $0) } let start = TelegramCore_InstantPageBlock_Preformatted.startInstantPageBlock_Preformatted(&builder) TelegramCore_InstantPageBlock_Preformatted.add(text: textOffset, &builder) + if let languageOffset { + TelegramCore_InstantPageBlock_Preformatted.add(language: languageOffset, &builder) + } offset = TelegramCore_InstantPageBlock_Preformatted.endInstantPageBlock_Preformatted(&builder, start: start) case let .footer(text): valueType = .instantpageblockFooter diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index d066310c99..753e3024ab 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -149,6 +149,7 @@ public struct Namespaces { public static let cachedChatThemes: Int8 = 50 public static let cachedLiveStorySendAsPeers: Int8 = 51 public static let cachedGiftUpgradesAttributes: Int8 = 52 + public static let cachedCloudAITextStyles: Int8 = 53 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RichText.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RichText.swift index 69311fda00..cce0025996 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RichText.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RichText.swift @@ -19,6 +19,7 @@ private enum RichTextTypes: Int32 { case phone = 13 case image = 14 case anchor = 15 + case formula = 16 } public indirect enum RichText: PostboxCoding, Equatable { @@ -38,6 +39,7 @@ public indirect enum RichText: PostboxCoding, Equatable { case phone(text: RichText, phone: String) case image(id: MediaId, dimensions: PixelDimensions) case anchor(text: RichText, name: String) + case formula(latex: String) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("r", orElse: 0) { @@ -79,6 +81,8 @@ public indirect enum RichText: PostboxCoding, Equatable { self = .image(id: MediaId(namespace: decoder.decodeInt32ForKey("i.n", orElse: 0), id: decoder.decodeInt64ForKey("i.i", orElse: 0)), dimensions: PixelDimensions(width: decoder.decodeInt32ForKey("sw", orElse: 0), height: decoder.decodeInt32ForKey("sh", orElse: 0))) case RichTextTypes.anchor.rawValue: self = .anchor(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, name: decoder.decodeStringForKey("n", orElse: "")) + case RichTextTypes.formula.rawValue: + self = .formula(latex: decoder.decodeStringForKey("l", orElse: "")) default: self = .empty } @@ -147,6 +151,9 @@ public indirect enum RichText: PostboxCoding, Equatable { encoder.encodeInt32(RichTextTypes.anchor.rawValue, forKey: "r") encoder.encodeObject(text, forKey: "t") encoder.encodeString(name, forKey: "n") + case let .formula(latex): + encoder.encodeInt32(RichTextTypes.formula.rawValue, forKey: "r") + encoder.encodeString(latex, forKey: "l") } } @@ -248,6 +255,12 @@ public indirect enum RichText: PostboxCoding, Equatable { } else { return false } + case let .formula(lhsLatex): + if case let .formula(rhsLatex) = rhs, lhsLatex == rhsLatex { + return true + } else { + return false + } } } } @@ -291,6 +304,8 @@ public extension RichText { return "" case let .anchor(text, _): return text.plainText + case let .formula(latex): + return latex } } } @@ -378,6 +393,11 @@ extension RichText { } self = .anchor(text: try RichText(flatBuffersObject: value.text), name: value.name) + case .richtextFormula: + guard let value = flatBuffersObject.value(type: TelegramCore_RichText_Formula.self) else { + throw FlatBuffersError.missingRequiredField() + } + self = .formula(latex: value.latex) case .none_: self = .empty } @@ -494,6 +514,12 @@ extension RichText { TelegramCore_RichText_Anchor.add(text: textOffset, &builder) TelegramCore_RichText_Anchor.add(name: nameOffset, &builder) offset = TelegramCore_RichText_Anchor.endRichText_Anchor(&builder, start: start) + case let .formula(latex): + valueType = .richtextFormula + let latexOffset = builder.create(string: latex) + let start = TelegramCore_RichText_Formula.startRichText_Formula(&builder) + TelegramCore_RichText_Formula.add(latex: latexOffset, &builder) + offset = TelegramCore_RichText_Formula.endRichText_Formula(&builder, start: start) } return TelegramCore_RichText.createRichText(&builder, valueType: valueType, valueOffset: offset) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatBannedRights.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatBannedRights.swift index f0ca1387f8..628c94ccd3 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatBannedRights.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatBannedRights.swift @@ -32,6 +32,7 @@ public struct TelegramChatBannedRightsFlags: OptionSet, Hashable { public static let banSendVoice = TelegramChatBannedRightsFlags(rawValue: 1 << 23) public static let banSendFiles = TelegramChatBannedRightsFlags(rawValue: 1 << 24) public static let banSendText = TelegramChatBannedRightsFlags(rawValue: 1 << 25) + public static let banSendReactions = TelegramChatBannedRightsFlags(rawValue: 1 << 27) public static let banEditRank = TelegramChatBannedRightsFlags(rawValue: 1 << 26) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaPoll.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaPoll.swift index 703a6a0f46..3038d37171 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaPoll.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaPoll.swift @@ -140,13 +140,15 @@ public struct TelegramMediaPollResults: Equatable, PostboxCoding { public let recentVoters: [PeerId] public let solution: TelegramMediaPollResults.Solution? public let hasUnseenVotes: Bool? + public let canViewStats: Bool - public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?, recentVoters: [PeerId], solution: TelegramMediaPollResults.Solution?, hasUnseenVotes: Bool?) { + public init(voters: [TelegramMediaPollOptionVoters]?, totalVoters: Int32?, recentVoters: [PeerId], solution: TelegramMediaPollResults.Solution?, hasUnseenVotes: Bool?, canViewStats: Bool) { self.voters = voters self.totalVoters = totalVoters self.recentVoters = recentVoters self.solution = solution self.hasUnseenVotes = hasUnseenVotes + self.canViewStats = canViewStats } public init(decoder: PostboxDecoder) { @@ -161,6 +163,7 @@ public struct TelegramMediaPollResults: Equatable, PostboxCoding { self.solution = nil } self.hasUnseenVotes = decoder.decodeOptionalBoolForKey("uns") + self.canViewStats = decoder.decodeBoolForKey("cvs", orElse: false) } public func encode(_ encoder: PostboxEncoder) { @@ -191,6 +194,7 @@ public struct TelegramMediaPollResults: Equatable, PostboxCoding { } else { encoder.encodeNil(forKey: "uns") } + encoder.encodeBool(self.canViewStats, forKey: "cvs") } } @@ -273,8 +277,10 @@ public final class TelegramMediaPoll: Media, Equatable { public let hideResultsUntilClose: Bool public let isCreator: Bool public let attachedMedia: Media? + public let restrictToSubscribers: Bool + public let countries: [String] - public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, textEntities: [MessageTextEntity], options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool, deadlineTimeout: Int32?, deadlineDate: Int32?, pollHash: Int64, openAnswers: Bool = false, revotingDisabled: Bool = false, shuffleAnswers: Bool = false, hideResultsUntilClose: Bool = false, isCreator: Bool = false, attachedMedia: Media? = nil) { + public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, textEntities: [MessageTextEntity], options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool, deadlineTimeout: Int32?, deadlineDate: Int32?, pollHash: Int64, openAnswers: Bool = false, revotingDisabled: Bool = false, shuffleAnswers: Bool = false, hideResultsUntilClose: Bool = false, isCreator: Bool = false, attachedMedia: Media? = nil, restrictToSubscribers: Bool = false, countries: [String] = []) { self.pollId = pollId self.publicity = publicity self.kind = kind @@ -293,6 +299,8 @@ public final class TelegramMediaPoll: Media, Equatable { self.hideResultsUntilClose = hideResultsUntilClose self.isCreator = isCreator self.attachedMedia = attachedMedia + self.restrictToSubscribers = restrictToSubscribers + self.countries = countries } public init(decoder: PostboxDecoder) { @@ -307,7 +315,7 @@ public final class TelegramMediaPoll: Media, Equatable { self.textEntities = decoder.decodeObjectArrayWithDecoderForKey("te") self.options = decoder.decodeObjectArrayWithDecoderForKey("os") self.correctAnswers = decoder.decodeOptionalDataArrayForKey("ca") - self.results = decoder.decodeObjectForKey("rs", decoder: { TelegramMediaPollResults(decoder: $0) }) as? TelegramMediaPollResults ?? TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil, hasUnseenVotes: nil) + self.results = decoder.decodeObjectForKey("rs", decoder: { TelegramMediaPollResults(decoder: $0) }) as? TelegramMediaPollResults ?? TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil, hasUnseenVotes: nil, canViewStats: false) self.isClosed = decoder.decodeInt32ForKey("ic", orElse: 0) != 0 self.deadlineTimeout = decoder.decodeOptionalInt32ForKey("dt") self.deadlineDate = decoder.decodeOptionalInt32ForKey("dd") @@ -318,6 +326,8 @@ public final class TelegramMediaPoll: Media, Equatable { self.hideResultsUntilClose = decoder.decodeInt32ForKey("hr", orElse: 0) != 0 self.isCreator = decoder.decodeInt32ForKey("cr", orElse: 0) != 0 self.attachedMedia = decoder.decodeObjectForKey("am") as? Media + self.restrictToSubscribers = decoder.decodeInt32ForKey("sub", orElse: 0) != 0 + self.countries = decoder.decodeStringArrayForKey("cnt") } public func encode(_ encoder: PostboxEncoder) { @@ -357,6 +367,8 @@ public final class TelegramMediaPoll: Media, Equatable { } else { encoder.encodeNil(forKey: "am") } + encoder.encodeInt32(self.restrictToSubscribers ? 1 : 0, forKey: "sub") + encoder.encodeStringArray(self.countries, forKey: "cnt") } public func isEqual(to other: Media) -> Bool { @@ -427,6 +439,12 @@ public final class TelegramMediaPoll: Media, Equatable { } else if (lhs.attachedMedia == nil) != (rhs.attachedMedia == nil) { return false } + if lhs.restrictToSubscribers != rhs.restrictToSubscribers { + return false + } + if lhs.countries != rhs.countries { + return false + } return true } @@ -446,21 +464,21 @@ public final class TelegramMediaPoll: Media, Equatable { } updatedResults = TelegramMediaPollResults(voters: updatedVoters.map({ voters in return TelegramMediaPollOptionVoters(selected: selectedOpaqueIdentifiers.contains(voters.opaqueIdentifier), opaqueIdentifier: voters.opaqueIdentifier, count: voters.count, isCorrect: correctOpaqueIdentifiers.contains(voters.opaqueIdentifier), recentVoters: voters.recentVoters) - }), totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution ?? self.results.solution, hasUnseenVotes: results.hasUnseenVotes ?? self.results.hasUnseenVotes) + }), totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution ?? self.results.solution, hasUnseenVotes: results.hasUnseenVotes ?? self.results.hasUnseenVotes, canViewStats: results.canViewStats) } else if let updatedVoters = results.voters { - updatedResults = TelegramMediaPollResults(voters: updatedVoters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution ?? self.results.solution, hasUnseenVotes: results.hasUnseenVotes ?? self.results.hasUnseenVotes) + updatedResults = TelegramMediaPollResults(voters: updatedVoters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution ?? self.results.solution, hasUnseenVotes: results.hasUnseenVotes ?? self.results.hasUnseenVotes, canViewStats: results.canViewStats) } else { - updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution ?? self.results.solution, hasUnseenVotes: results.hasUnseenVotes ?? self.results.hasUnseenVotes) + updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: results.totalVoters, recentVoters: results.recentVoters, solution: results.solution ?? self.results.solution, hasUnseenVotes: results.hasUnseenVotes ?? self.results.hasUnseenVotes, canViewStats: results.canViewStats) } } else { updatedResults = results } - return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, textEntities: self.textEntities, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed, deadlineTimeout: self.deadlineTimeout, deadlineDate: self.deadlineDate, pollHash: self.pollHash, openAnswers: self.openAnswers, revotingDisabled: self.revotingDisabled, shuffleAnswers: self.shuffleAnswers, hideResultsUntilClose: self.hideResultsUntilClose, isCreator: self.isCreator, attachedMedia: self.attachedMedia) + return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, textEntities: self.textEntities, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed, deadlineTimeout: self.deadlineTimeout, deadlineDate: self.deadlineDate, pollHash: self.pollHash, openAnswers: self.openAnswers, revotingDisabled: self.revotingDisabled, shuffleAnswers: self.shuffleAnswers, hideResultsUntilClose: self.hideResultsUntilClose, isCreator: self.isCreator, attachedMedia: self.attachedMedia, restrictToSubscribers: self.restrictToSubscribers, countries: self.countries) } public func withoutUnreadResults() -> TelegramMediaPoll { - let updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: self.results.totalVoters, recentVoters: self.results.recentVoters, solution: self.results.solution, hasUnseenVotes: false) - return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, textEntities: self.textEntities, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed, deadlineTimeout: self.deadlineTimeout, deadlineDate: self.deadlineDate, pollHash: self.pollHash, openAnswers: self.openAnswers, revotingDisabled: self.revotingDisabled, shuffleAnswers: self.shuffleAnswers, hideResultsUntilClose: self.hideResultsUntilClose, isCreator: self.isCreator, attachedMedia: self.attachedMedia) + let updatedResults = TelegramMediaPollResults(voters: self.results.voters, totalVoters: self.results.totalVoters, recentVoters: self.results.recentVoters, solution: self.results.solution, hasUnseenVotes: false, canViewStats: self.results.canViewStats) + return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, textEntities: self.textEntities, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed, deadlineTimeout: self.deadlineTimeout, deadlineDate: self.deadlineDate, pollHash: self.pollHash, openAnswers: self.openAnswers, revotingDisabled: self.revotingDisabled, shuffleAnswers: self.shuffleAnswers, hideResultsUntilClose: self.hideResultsUntilClose, isCreator: self.isCreator, attachedMedia: self.attachedMedia, restrictToSubscribers: self.restrictToSubscribers, countries: self.countries) } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift index bef68016e0..fae9ea4809 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift @@ -9,6 +9,7 @@ private enum TelegramMediaWebpageAttributeTypes: Int32 { case starGift case giftCollection case giftAuction + case aiTextStyle } public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { @@ -18,6 +19,7 @@ public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { case starGift(TelegramMediaWebpageStarGiftAttribute) case giftCollection(TelegramMediaWebpageGiftCollectionAttribute) case giftAuction(TelegramMediaWebpageGiftAuctionAttribute) + case aiTextStyle(TelegramMediaWebpageAITextStyleAttribute) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("r", orElse: 0) { @@ -31,6 +33,8 @@ public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { self = .giftCollection(decoder.decodeObjectForKey("a", decoder: { TelegramMediaWebpageGiftCollectionAttribute(decoder: $0) }) as! TelegramMediaWebpageGiftCollectionAttribute) case TelegramMediaWebpageAttributeTypes.giftAuction.rawValue: self = .giftAuction(decoder.decodeObjectForKey("a", decoder: { TelegramMediaWebpageGiftAuctionAttribute(decoder: $0) }) as! TelegramMediaWebpageGiftAuctionAttribute) + case TelegramMediaWebpageAttributeTypes.aiTextStyle.rawValue: + self = .aiTextStyle(decoder.decodeObjectForKey("a", decoder: { TelegramMediaWebpageAITextStyleAttribute(decoder: $0) }) as! TelegramMediaWebpageAITextStyleAttribute) default: self = .unsupported } @@ -55,6 +59,17 @@ public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { case let .giftAuction(attribute): encoder.encodeInt32(TelegramMediaWebpageAttributeTypes.giftAuction.rawValue, forKey: "r") encoder.encodeObject(attribute, forKey: "a") + case let .aiTextStyle(attribute): + encoder.encodeInt32(TelegramMediaWebpageAttributeTypes.aiTextStyle.rawValue, forKey: "r") + encoder.encodeObject(attribute, forKey: "a") + } + } + + public var mediaIds: [MediaId] { + if case let .aiTextStyle(attribute) = self { + return [MediaId(namespace: Namespaces.Media.CloudFile, id: attribute.emojiFileId)] + } else { + return [] } } } @@ -235,6 +250,32 @@ public final class TelegramMediaWebpageGiftAuctionAttribute: PostboxCoding, Equa } } +public final class TelegramMediaWebpageAITextStyleAttribute: PostboxCoding, Equatable { + public static func == (lhs: TelegramMediaWebpageAITextStyleAttribute, rhs: TelegramMediaWebpageAITextStyleAttribute) -> Bool { + if lhs.emojiFileId != rhs.emojiFileId { + return false + } + return true + } + + public let emojiFileId: Int64 + + public init( + emojiFileId: Int64 + ) { + self.emojiFileId = emojiFileId + } + + public init(decoder: PostboxDecoder) { + self.emojiFileId = decoder.decodeInt64ForKey("eid", orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.emojiFileId, forKey: "eid") + } +} + + public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { public let url: String public let displayUrl: String @@ -554,6 +595,17 @@ public final class TelegramMediaWebpage: Media, Equatable { } } + public var mediaIds: [MediaId] { + guard case let .Loaded(content) = self.content else { + return [] + } + var result: [MediaId] = [] + for attribute in content.attributes { + result.append(contentsOf: attribute.mediaIds) + } + return result + } + public let webpageId: MediaId public let content: TelegramMediaWebpageContent diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index bcc02ef022..2c7c1ba4b2 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -45,6 +45,7 @@ public struct BotUserInfoFlags: OptionSet { public static let hasForum = BotUserInfoFlags(rawValue: (1 << 8)) public static let forumManagedByUser = BotUserInfoFlags(rawValue: (1 << 9)) public static let canManageBots = BotUserInfoFlags(rawValue: (1 << 10)) + public static let isGuestChat = BotUserInfoFlags(rawValue: (1 << 11)) } public struct BotUserInfo: PostboxCoding, Equatable { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift index 7a43a6dea9..4389519a95 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_UnauthorizedAccountState.swift @@ -182,7 +182,7 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable case passwordRecovery(hint: String, number: String?, code: AuthorizationCode?, emailPattern: String, syncContacts: Bool) case awaitingAccountReset(protectedUntil: Int32, number: String?, syncContacts: Bool) case signUp(number: String, codeHash: String, firstName: String, lastName: String, termsOfService: UnauthorizedAccountTermsOfService?, syncContacts: Bool) - case payment(number: String, codeHash: String, storeProduct: String, supportEmailAddress: String, supportEmailSubject: String, syncContacts: Bool) + case payment(number: String, codeHash: String, storeProduct: String, premiumDays: Int32, supportEmailAddress: String, supportEmailSubject: String, syncContacts: Bool) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("v", orElse: 0) { @@ -217,7 +217,7 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable case UnauthorizedAccountStateContentsValue.signUp.rawValue: self = .signUp(number: decoder.decodeStringForKey("n", orElse: ""), codeHash: decoder.decodeStringForKey("h", orElse: ""), firstName: decoder.decodeStringForKey("f", orElse: ""), lastName: decoder.decodeStringForKey("l", orElse: ""), termsOfService: decoder.decodeObjectForKey("tos", decoder: { UnauthorizedAccountTermsOfService(decoder: $0) }) as? UnauthorizedAccountTermsOfService, syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0) case UnauthorizedAccountStateContentsValue.payment.rawValue: - self = .payment(number: decoder.decodeStringForKey("n", orElse: ""), codeHash: decoder.decodeStringForKey("h", orElse: ""), storeProduct: decoder.decodeStringForKey("storeProduct", orElse: ""), supportEmailAddress: decoder.decodeStringForKey("supportEmailAddress", orElse: ""), supportEmailSubject: decoder.decodeStringForKey("supportEmailSubject", orElse: ""), syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0) + self = .payment(number: decoder.decodeStringForKey("n", orElse: ""), codeHash: decoder.decodeStringForKey("h", orElse: ""), storeProduct: decoder.decodeStringForKey("storeProduct", orElse: ""), premiumDays: decoder.decodeInt32ForKey("premiumDays", orElse: 7), supportEmailAddress: decoder.decodeStringForKey("supportEmailAddress", orElse: ""), supportEmailSubject: decoder.decodeStringForKey("supportEmailSubject", orElse: ""), syncContacts: decoder.decodeInt32ForKey("syncContacts", orElse: 1) != 0) default: assertionFailure() self = .empty @@ -307,11 +307,12 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable encoder.encodeNil(forKey: "tos") } encoder.encodeInt32(syncContacts ? 1 : 0, forKey: "syncContacts") - case let .payment(number, codeHash, storeProduct, supportEmailAddress, supportEmailSubject, syncContacts): + case let .payment(number, codeHash, storeProduct, premiumDays, supportEmailAddress, supportEmailSubject, syncContacts): encoder.encodeInt32(UnauthorizedAccountStateContentsValue.payment.rawValue, forKey: "v") encoder.encodeString(number, forKey: "n") encoder.encodeString(codeHash, forKey: "h") encoder.encodeString(storeProduct, forKey: "storeProduct") + encoder.encodeInt32(premiumDays, forKey: "premiumDays") encoder.encodeString(supportEmailAddress, forKey: "supportEmailAddress") encoder.encodeString(supportEmailSubject, forKey: "supportEmailSubject") encoder.encodeInt32(syncContacts ? 1 : 0, forKey: "syncContacts") @@ -386,8 +387,8 @@ public indirect enum UnauthorizedAccountStateContents: PostboxCoding, Equatable } else { return false } - case let .payment(number, codeHash, storeProduct, supportEmailAddress, supportEmailSubject, syncContacts): - if case .payment(number, codeHash, storeProduct, supportEmailAddress, supportEmailSubject, syncContacts) = rhs { + case let .payment(number, codeHash, storeProduct, premiumDays, supportEmailAddress, supportEmailSubject, syncContacts): + if case .payment(number, codeHash, storeProduct, premiumDays, supportEmailAddress, supportEmailSubject, syncContacts) = rhs { return true } else { return false diff --git a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift index d2b33e6456..aeda4da023 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/AccountData/TelegramEngineAccountData.swift @@ -55,8 +55,10 @@ public extension TelegramEngine { return _internal_registerNotificationToken(account: self.account, token: token, type: type, sandbox: sandbox, otherAccountUserIds: otherAccountUserIds, excludeMutedChats: excludeMutedChats) } - public func updateAccountPhoto(resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { - return _internal_updateAccountPhoto(account: self.account, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, fallback: false, mapResourceToAvatarSizes: mapResourceToAvatarSizes) + public func updateAccountPhoto(resource: EngineMediaResource?, videoResource: EngineMediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mapResourceToAvatarSizes: @escaping (EngineMediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updateAccountPhoto(account: self.account, resource: resource?._asResource(), videoResource: videoResource?._asResource(), videoStartTimestamp: videoStartTimestamp, markup: markup, fallback: false, mapResourceToAvatarSizes: { rawResource, representations in + return mapResourceToAvatarSizes(EngineMediaResource(rawResource), representations) + }) } public func updatePeerPhotoExisting(reference: TelegramMediaImageReference) -> Signal { @@ -67,8 +69,10 @@ public extension TelegramEngine { return _internal_removeAccountPhoto(account: self.account, reference: reference, fallback: false) } - public func updateFallbackPhoto(resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { - return _internal_updateAccountPhoto(account: self.account, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, fallback: true, mapResourceToAvatarSizes: mapResourceToAvatarSizes) + public func updateFallbackPhoto(resource: EngineMediaResource?, videoResource: EngineMediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mapResourceToAvatarSizes: @escaping (EngineMediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updateAccountPhoto(account: self.account, resource: resource?._asResource(), videoResource: videoResource?._asResource(), videoStartTimestamp: videoStartTimestamp, markup: markup, fallback: true, mapResourceToAvatarSizes: { rawResource, representations in + return mapResourceToAvatarSizes(EngineMediaResource(rawResource), representations) + }) } public func removeFallbackPhoto(reference: TelegramMediaImageReference?) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift b/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift index 6fbca60760..89f3553c61 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Auth/TelegramEngineAuth.swift @@ -48,8 +48,8 @@ public extension TelegramEngineUnauthorized { return _internal_resendTwoStepRecoveryEmail(network: self.account.network) } - public func uploadedPeerVideo(resource: MediaResource) -> Signal { - return _internal_uploadedPeerVideo(postbox: self.account.postbox, network: self.account.network, messageMediaPreuploadManager: nil, resource: resource) + public func uploadedPeerVideo(resource: EngineMediaResource) -> Signal { + return _internal_uploadedPeerVideo(postbox: self.account.postbox, network: self.account.network, messageMediaPreuploadManager: nil, resource: resource._asResource()) } public func reportMissingCode(phoneNumber: String, phoneCodeHash: String, mnc: String) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index e829e578d6..c82eabeb16 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -2957,7 +2957,7 @@ func _internal_groupCallDisplayAsAvailablePeers(accountPeerId: PeerId, network: } } - return peers.map { FoundPeer(peer: $0, subscribers: subscribers[$0.id]) } + return peers.map { FoundPeer(peer: EnginePeer($0), subscribers: subscribers[$0.id]) } } } } @@ -3011,7 +3011,7 @@ func _internal_cachedGroupCallDisplayAsAvailablePeers(account: Account, peerId: if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData { subscribers = cachedData.participantsSummary.memberCount } - peers.append(FoundPeer(peer: peer, subscribers: subscribers)) + peers.append(FoundPeer(peer: EnginePeer(peer), subscribers: subscribers)) } } return (peers, cached.timestamp) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift index c3ecf5a4e2..bc41811d57 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/TelegramEngineContacts.swift @@ -30,8 +30,10 @@ public extension TelegramEngine { return _internal_updateContactName(account: self.account, peerId: peerId, firstName: firstName, lastName: lastName) } - public func updateContactPhoto(peerId: PeerId, resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mode: SetCustomPeerPhotoMode, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { - return _internal_updateContactPhoto(account: self.account, peerId: peerId, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: mode, mapResourceToAvatarSizes: mapResourceToAvatarSizes) + public func updateContactPhoto(peerId: PeerId, resource: EngineMediaResource?, videoResource: EngineMediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mode: SetCustomPeerPhotoMode, mapResourceToAvatarSizes: @escaping (EngineMediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updateContactPhoto(account: self.account, peerId: peerId, resource: resource?._asResource(), videoResource: videoResource?._asResource(), videoStartTimestamp: videoStartTimestamp, markup: markup, mode: mode, mapResourceToAvatarSizes: { rawResource, representations in + return mapResourceToAvatarSizes(EngineMediaResource(rawResource), representations) + }) } public func updateContactNote(peerId: PeerId, text: String, entities: [MessageTextEntity]) -> Signal { @@ -58,8 +60,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/Data/ConfigurationData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift index 15e164f6ed..3991971896 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/ConfigurationData.swift @@ -64,6 +64,7 @@ public enum EngineConfiguration { public let maxChannelRecommendationsCount: Int32 public let maxConferenceParticipantCount: Int32 public let maxBotsCreated: Int32 + public let maxOwnedAITextStyles: Int32 public static var defaultValue: UserLimits { return UserLimits(UserLimitsConfiguration.defaultValue) @@ -97,7 +98,8 @@ public enum EngineConfiguration { maxGiveawayPeriodSeconds: Int32, maxChannelRecommendationsCount: Int32, maxConferenceParticipantCount: Int32, - maxBotsCreated: Int32 + maxBotsCreated: Int32, + maxOwnedAITextStyles: Int32 ) { self.maxPinnedChatCount = maxPinnedChatCount self.maxPinnedSavedChatCount = maxPinnedSavedChatCount @@ -127,6 +129,7 @@ public enum EngineConfiguration { self.maxChannelRecommendationsCount = maxChannelRecommendationsCount self.maxConferenceParticipantCount = maxConferenceParticipantCount self.maxBotsCreated = maxBotsCreated + self.maxOwnedAITextStyles = maxOwnedAITextStyles } } } @@ -191,7 +194,8 @@ public extension EngineConfiguration.UserLimits { maxGiveawayPeriodSeconds: userLimitsConfiguration.maxGiveawayPeriodSeconds, maxChannelRecommendationsCount: userLimitsConfiguration.maxChannelRecommendationsCount, maxConferenceParticipantCount: userLimitsConfiguration.maxConferenceParticipantCount, - maxBotsCreated: userLimitsConfiguration.maxBotsCreated + maxBotsCreated: userLimitsConfiguration.maxBotsCreated, + maxOwnedAITextStyles: userLimitsConfiguration.maxOwnedAITextStyles ) } } @@ -576,6 +580,27 @@ public extension TelegramEngine.EngineData.Item { } } + public struct ContactsSettings: TelegramEngineDataItem, PostboxViewDataItem { + public typealias Result = TelegramCore.ContactsSettings + + public init() { + } + + var key: PostboxViewKey { + return .preferences(keys: Set([PreferencesKeys.contactsSettings])) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? PreferencesView else { + preconditionFailure() + } + guard let value = view.values[PreferencesKeys.contactsSettings]?.get(TelegramCore.ContactsSettings.self) else { + return TelegramCore.ContactsSettings.defaultSettings + } + return value + } + } + public struct EmojiGame: TelegramEngineDataItem, PostboxViewDataItem { public typealias Result = EmojiGameInfo diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index a793b7d779..0cfb3c20bc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -1688,6 +1688,9 @@ public extension TelegramEngine.EngineData.Item { } func _extract(data: TelegramEngine.EngineData, views: [PostboxViewKey: PostboxView]) -> Any { + guard self.id != data.accountPeerId else { + return false + } guard let basicPeerView = views[.basicPeer(data.accountPeerId)] as? BasicPeerView else { assertionFailure() return false diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift index 65a2d3a3d2..63967e2685 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ChatList.swift @@ -474,10 +474,13 @@ public extension EngineChatList.RelativePosition { } } -private func calculateIsPremiumRequiredToMessage(isPremium: Bool, targetPeer: Peer, cachedIsPremiumRequired: Bool) -> Bool { +private func calculateIsPremiumRequiredToMessage(accountPeerId: PeerId?, isPremium: Bool, targetPeer: Peer, cachedIsPremiumRequired: Bool) -> Bool { if isPremium { return false } + if let accountPeerId, targetPeer.id == accountPeerId { + return false + } guard let targetPeer = targetPeer as? TelegramUser else { return false } @@ -488,7 +491,7 @@ private func calculateIsPremiumRequiredToMessage(isPremium: Bool, targetPeer: Pe } extension EngineChatList.Item { - convenience init?(_ entry: ChatListEntry, isPremium: Bool, displayAsTopicList: Bool) { + convenience init?(_ entry: ChatListEntry, accountPeerId: PeerId?, isPremium: Bool, displayAsTopicList: Bool) { switch entry { case let .MessageEntry(entryData): let index = entryData.index @@ -507,7 +510,7 @@ extension EngineChatList.Item { var isPremiumRequiredToMessage = false if let targetPeer = renderedPeer.chatMainPeer, let extractedData = entryData.extractedCachedData?.base as? ExtractedChatListItemCachedData { - isPremiumRequiredToMessage = calculateIsPremiumRequiredToMessage(isPremium: isPremium, targetPeer: targetPeer, cachedIsPremiumRequired: extractedData.isPremiumRequiredToMessage) + isPremiumRequiredToMessage = calculateIsPremiumRequiredToMessage(accountPeerId: accountPeerId, isPremium: isPremium, targetPeer: targetPeer, cachedIsPremiumRequired: extractedData.isPremiumRequiredToMessage) } var draft: EngineChatList.Draft? @@ -632,7 +635,7 @@ extension EngineChatList.AdditionalItem.PromoInfo { extension EngineChatList.AdditionalItem { convenience init?(_ entry: ChatListAdditionalItemEntry) { - guard let item = EngineChatList.Item(entry.entry, isPremium: false, displayAsTopicList: false) else { + guard let item = EngineChatList.Item(entry.entry, accountPeerId: nil, isPremium: false, displayAsTopicList: false) else { return nil } guard let promoInfo = (entry.info as? PromoChatListItem).flatMap(EngineChatList.AdditionalItem.PromoInfo.init) else { @@ -657,7 +660,7 @@ public extension EngineChatList { loop: for entry in view.entries { switch entry { case .MessageEntry: - if let item = EngineChatList.Item(entry, isPremium: isPremium, displayAsTopicList: entry.index.messageIndex.id.peerId == accountPeerId ? displaySavedMessagesAsTopicList : false) { + if let item = EngineChatList.Item(entry, accountPeerId: accountPeerId, isPremium: isPremium, displayAsTopicList: entry.index.messageIndex.id.peerId == accountPeerId ? displaySavedMessagesAsTopicList : false) { items.append(item) } case .HoleEntry: diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift index 001fcc1282..84878e8508 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift @@ -76,6 +76,203 @@ func _internal_deleteAllMessagesWithForwardAuthor(transaction: Transaction, medi } } +func _internal_deleteAllReactionsWithAuthor(account: Account, peerId: PeerId, authorId: PeerId, aroundMessageId: MessageId?) -> Signal { + return account.postbox.transaction { transaction -> (Peer?, Peer?) in + let peer = transaction.getPeer(peerId) + let author = transaction.getPeer(authorId) + + if peer.flatMap(apiInputPeer) != nil && author.flatMap(apiInputPeer) != nil { + let anchor: HistoryViewInputAnchor + if let aroundMessageId, aroundMessageId.peerId == peerId { + anchor = .message(aroundMessageId) + } else { + anchor = .upperBound + } + let historyView = transaction.getMessagesHistoryViewState(input: .single(peerId: peerId, threadId: nil), ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 50, clipHoles: true, anchor: anchor, namespaces: .just(Set([Namespaces.Message.Cloud]))) + for entry in historyView.entries { + transaction.updateMessage(entry.message.id, update: { currentMessage in + var attributes = currentMessage.attributes + var updated = false + + for i in 0 ..< attributes.count { + guard let attribute = attributes[i] as? ReactionsMessageAttribute else { + continue + } + + let removedRecentPeers = attribute.recentPeers.filter { $0.peerId == authorId } + var updatedTopPeers = attribute.topPeers + var removedStarsTopPeerCount: Int32 = 0 + for j in (0 ..< updatedTopPeers.count).reversed() { + if updatedTopPeers[j].peerId == authorId { + removedStarsTopPeerCount += updatedTopPeers[j].count + updatedTopPeers.remove(at: j) + } + } + + if removedRecentPeers.isEmpty && removedStarsTopPeerCount == 0 { + continue + } + + var updatedReactions = attribute.reactions + for removedRecentPeer in removedRecentPeers { + if let index = updatedReactions.firstIndex(where: { $0.value == removedRecentPeer.value }) { + if removedRecentPeer.value != .stars || removedStarsTopPeerCount == 0 { + updatedReactions[index].count -= 1 + } + if removedRecentPeer.isMy { + updatedReactions[index].chosenOrder = nil + } + } + } + if removedStarsTopPeerCount != 0, let index = updatedReactions.firstIndex(where: { $0.value == .stars }) { + updatedReactions[index].count -= removedStarsTopPeerCount + } + for j in (0 ..< updatedReactions.count).reversed() { + if updatedReactions[j].count <= 0 { + updatedReactions.remove(at: j) + } + } + + let updatedRecentPeers = attribute.recentPeers.filter { $0.peerId != authorId } + let updatedAttribute = ReactionsMessageAttribute(canViewList: attribute.canViewList, isTags: attribute.isTags, reactions: updatedReactions, recentPeers: updatedRecentPeers, topPeers: updatedTopPeers) + if updatedAttribute != attribute { + attributes[i] = updatedAttribute + updated = true + } + } + + if !updated { + return .skip + } + + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) + } + + var tags = currentMessage.tags + if attributes.contains(where: { ($0 as? ReactionsMessageAttribute)?.hasUnseen == true }) { + tags.insert(.unseenReaction) + } else { + tags.remove(.unseenReaction) + } + + return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } + } + + return (peer, author) + } + |> mapToSignal { peer, author in + guard let inputPeer = peer.flatMap(apiInputPeer), let inputAuthor = author.flatMap(apiInputPeer) else { + return .complete() + } + return account.network.request(Api.functions.messages.deleteParticipantReactions(peer: inputPeer, participant: inputAuthor)) + |> ignoreValues + |> `catch` { _ -> Signal in + return .complete() + } + } +} + +func _internal_deleteReaction(account: Account, messageId: MessageId, authorId: PeerId) -> Signal { + return account.postbox.transaction { transaction -> (Peer?, Peer?) in + let peer = transaction.getPeer(messageId.peerId) + let author = transaction.getPeer(authorId) + + if peer.flatMap(apiInputPeer) != nil && author.flatMap(apiInputPeer) != nil { + transaction.updateMessage(messageId, update: { currentMessage in + var attributes = currentMessage.attributes + var updated = false + + for i in 0 ..< attributes.count { + guard let attribute = attributes[i] as? ReactionsMessageAttribute else { + continue + } + + let removedRecentPeers = attribute.recentPeers.filter { $0.peerId == authorId } + var updatedTopPeers = attribute.topPeers + var removedStarsTopPeerCount: Int32 = 0 + for j in (0 ..< updatedTopPeers.count).reversed() { + if updatedTopPeers[j].peerId == authorId { + removedStarsTopPeerCount += updatedTopPeers[j].count + updatedTopPeers.remove(at: j) + } + } + + if removedRecentPeers.isEmpty && removedStarsTopPeerCount == 0 { + continue + } + + var updatedReactions = attribute.reactions + for removedRecentPeer in removedRecentPeers { + if let index = updatedReactions.firstIndex(where: { $0.value == removedRecentPeer.value }) { + if removedRecentPeer.value != .stars || removedStarsTopPeerCount == 0 { + updatedReactions[index].count -= 1 + } + if removedRecentPeer.isMy { + updatedReactions[index].chosenOrder = nil + } + } + } + if removedStarsTopPeerCount != 0, let index = updatedReactions.firstIndex(where: { $0.value == .stars }) { + updatedReactions[index].count -= removedStarsTopPeerCount + } + for j in (0 ..< updatedReactions.count).reversed() { + if updatedReactions[j].count <= 0 { + updatedReactions.remove(at: j) + } + } + + let updatedRecentPeers = attribute.recentPeers.filter { $0.peerId != authorId } + let updatedAttribute = ReactionsMessageAttribute(canViewList: attribute.canViewList, isTags: attribute.isTags, reactions: updatedReactions, recentPeers: updatedRecentPeers, topPeers: updatedTopPeers) + if updatedAttribute != attribute { + attributes[i] = updatedAttribute + updated = true + } + } + + if !updated { + return .skip + } + + var storeForwardInfo: StoreMessageForwardInfo? + if let forwardInfo = currentMessage.forwardInfo { + storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) + } + + var tags = currentMessage.tags + if attributes.contains(where: { ($0 as? ReactionsMessageAttribute)?.hasUnseen == true }) { + tags.insert(.unseenReaction) + } else { + tags.remove(.unseenReaction) + } + + return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } + + return (peer, author) + } + |> mapToSignal { peer, author in + guard let inputPeer = peer.flatMap(apiInputPeer), let inputAuthor = author.flatMap(apiInputPeer) else { + return .complete() + } + return account.network.request(Api.functions.messages.deleteParticipantReaction(peer: inputPeer, msgId: messageId.id, participant: inputAuthor)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates { + account.stateManager.addUpdates(updates) + } + return .complete() + } + } +} + func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, threadId: Int64?, namespaces: MessageIdNamespaces) { if peerId.namespace == Namespaces.Peer.SecretChat { var resourceIds: [MediaResourceId] = [] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift index 95e6913650..ef127aa336 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift @@ -3,6 +3,38 @@ import Postbox import TelegramApi import SwiftSignalKit +public enum SendBotGameError { + case generic +} + +func _internal_sendBotGame(account: Account, botPeerId: PeerId, game: String, to peerId: PeerId, threadId: Int64?) -> Signal { + return account.postbox.transaction { transaction -> Signal in + guard !game.isEmpty, let botPeer = transaction.getPeer(botPeerId), let inputBot = apiInputUser(botPeer), let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) else { + return .fail(.generic) + } + + var flags: Int32 = 1 << 7 + var replyTo: Api.InputReplyTo? + if let threadId { + flags |= 1 << 0 + let threadMessageId = Int32(clamping: threadId) + let replyFlags: Int32 = 1 << 0 + replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: threadMessageId, topMsgId: threadMessageId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: nil, todoItemId: nil, pollOption: nil)) + } + + return account.network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: .inputMediaGame(.init(id: .inputGameShortName(.init(botId: inputBot, shortName: game)))), message: "", randomId: Int64.random(in: Int64.min ... Int64.max), replyMarkup: nil, entities: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, sendAs: nil, quickReplyShortcut: nil, effect: nil, allowPaidStars: nil, suggestedPost: nil)) + |> mapError { _ -> SendBotGameError in + return .generic + } + |> mapToSignal { updates -> Signal in + account.stateManager.addUpdates(updates) + return .complete() + } + } + |> castError(SendBotGameError.self) + |> switchToLatest +} + func _internal_forwardGameWithScore(account: Account, messageId: MessageId, to peerId: PeerId, threadId: Int64?, as sendAsPeerId: PeerId?) -> Signal { return account.postbox.transaction { transaction -> Signal in if let _ = transaction.getMessage(messageId), let fromPeer = transaction.getPeer(messageId.peerId), let fromInputPeer = apiInputPeer(fromPeer), let toPeer = transaction.getPeer(peerId), let toInputPeer = apiInputPeer(toPeer) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/InstallInteractiveReadMessagesAction.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/InstallInteractiveReadMessagesAction.swift index b53c8606d3..5a936112a3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/InstallInteractiveReadMessagesAction.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/InstallInteractiveReadMessagesAction.swift @@ -100,11 +100,13 @@ func _internal_installInteractiveReadMessagesAction(postbox: Postbox, stateManag for (_, index) in readMessageIndexByNamespace { if let threadId { + var newCountIsZero = false if var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) { if index.id.id >= data.maxIncomingReadId { if let count = transaction.getThreadMessageCount(peerId: peerId, threadId: threadId, namespace: Namespaces.Message.Cloud, fromIdExclusive: data.maxIncomingReadId, toIndex: index) { data.incomingUnreadCount = max(0, data.incomingUnreadCount - Int32(count)) data.maxIncomingReadId = index.id.id + newCountIsZero = data.incomingUnreadCount == 0 } if let topMessageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: threadId, namespaces: Set([Namespaces.Message.Cloud])) { @@ -124,6 +126,23 @@ func _internal_installInteractiveReadMessagesAction(postbox: Postbox, stateManag } } } + + if newCountIsZero, let peer = transaction.getPeer(peerId), peer.isForumOrMonoForum { + var allTopicsAreRead = true + for item in transaction.getMessageHistoryThreadIndex(peerId: peer.id, limit: 100) { + guard let data = transaction.getMessageHistoryThreadInfo(peerId: index.id.peerId, threadId: item.threadId)?.data.get(MessageHistoryThreadData.self) else { + continue + } + if data.incomingUnreadCount != 0 { + allTopicsAreRead = false + break + } + } + + if allTopicsAreRead { + _internal_applyMaxReadIndexInteractively(transaction: transaction, stateManager: stateManager, index: index) + } + } } else { _internal_applyMaxReadIndexInteractively(transaction: transaction, stateManager: stateManager, index: index) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift index fc9335f722..a1a87ca53f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Polls.swift @@ -31,6 +31,7 @@ func pollCloudMediaToInputMedia(_ media: Media) -> Api.InputMedia? { public enum RequestMessageSelectPollOptionError { case generic + case restrictedToSubscribers } func _internal_requestMessageSelectPollOption(account: Account, messageId: MessageId, opaqueIdentifiers: [Data]) -> Signal { @@ -40,7 +41,10 @@ func _internal_requestMessageSelectPollOption(account: Account, messageId: Messa |> mapToSignal { peer in if let inputPeer = apiInputPeer(peer) { return account.network.request(Api.functions.messages.sendVote(peer: inputPeer, msgId: messageId.id, options: opaqueIdentifiers.map { Buffer(data: $0) })) - |> mapError { _ -> RequestMessageSelectPollOptionError in + |> mapError { error -> RequestMessageSelectPollOptionError in + if error.errorDescription == "POLL_MEMBER_RESTRICTED" { + return .restrictedToSubscribers + } return .generic } |> mapToSignal { result -> Signal in @@ -58,7 +62,7 @@ func _internal_requestMessageSelectPollOption(account: Account, messageId: Messa if let poll = poll { switch poll { case let .poll(pollData): - let (flags, question, answers, closePeriod, closeDate, pollHash) = (pollData.flags, pollData.question, pollData.answers, pollData.closePeriod, pollData.closeDate, pollData.hash) + let (flags, question, answers, closePeriod, closeDate, pollHash, countries) = (pollData.flags, pollData.question, pollData.answers, pollData.closePeriod, pollData.closeDate, pollData.hash, pollData.countriesIso2) let publicity: TelegramMediaPollPublicity if (flags & (1 << 1)) != 0 { publicity = .public @@ -75,6 +79,7 @@ func _internal_requestMessageSelectPollOption(account: Account, messageId: Messa let revotingDisabled = (flags & (1 << 7)) != 0 let shuffleAnswers = (flags & (1 << 8)) != 0 let hideResultsUntilClose = (flags & (1 << 9)) != 0 + let restrictToSubscribers = (flags & (1 << 11)) != 0 let questionText: String let questionEntities: [MessageTextEntity] switch question { @@ -83,7 +88,7 @@ func _internal_requestMessageSelectPollOption(account: Account, messageId: Messa questionText = text questionEntities = messageTextEntitiesFromApiEntities(entities) } - resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod, deadlineDate: closeDate, pollHash: pollHash, openAnswers: openAnswers, revotingDisabled: revotingDisabled, shuffleAnswers: shuffleAnswers, hideResultsUntilClose: hideResultsUntilClose, attachedMedia: resultPoll?.attachedMedia) + resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: questionText, textEntities: questionEntities, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod, deadlineDate: closeDate, pollHash: pollHash, openAnswers: openAnswers, revotingDisabled: revotingDisabled, shuffleAnswers: shuffleAnswers, hideResultsUntilClose: hideResultsUntilClose, attachedMedia: resultPoll?.attachedMedia, restrictToSubscribers: restrictToSubscribers, countries: countries ?? []) } } @@ -217,10 +222,17 @@ func _internal_requestClosePoll(postbox: Postbox, network: Network, stateManager pollFlags |= 1 << 1 } var pollMediaFlags: Int32 = 0 - var correctAnswers: [Buffer]? + var correctAnswersIndices: [Int32]? if let correctAnswersValue = poll.correctAnswers { pollMediaFlags |= 1 << 0 - correctAnswers = correctAnswersValue.map { Buffer(data: $0) } + + var indices: [Int32] = [] + for i in 0 ..< poll.options.count { + if correctAnswersValue.contains(where: { poll.options[i].opaqueIdentifier == $0 }) { + indices.append(Int32(i)) + } + } + correctAnswersIndices = indices } pollFlags |= 1 << 0 @@ -228,10 +240,27 @@ func _internal_requestClosePoll(postbox: Postbox, network: Network, stateManager if poll.deadlineTimeout != nil { pollFlags |= 1 << 4 } - if poll.openAnswers { pollFlags |= 1 << 6 } - if poll.revotingDisabled { pollFlags |= 1 << 7 } - if poll.shuffleAnswers { pollFlags |= 1 << 8 } - if poll.hideResultsUntilClose { pollFlags |= 1 << 9 } + if poll.deadlineDate != nil { + pollFlags |= 1 << 5 + } + if poll.openAnswers { + pollFlags |= 1 << 6 + } + if poll.revotingDisabled { + pollFlags |= 1 << 7 + } + if poll.shuffleAnswers { + pollFlags |= 1 << 8 + } + if poll.hideResultsUntilClose { + pollFlags |= 1 << 9 + } + if poll.restrictToSubscribers { + pollFlags |= 1 << 11 + } + if !poll.countries.isEmpty { + pollFlags |= 1 << 12 + } var mappedSolution: String? var mappedSolutionEntities: [Api.MessageEntity]? @@ -240,12 +269,8 @@ func _internal_requestClosePoll(postbox: Postbox, network: Network, stateManager mappedSolutionEntities = apiTextAttributeEntities(TextEntitiesMessageAttribute(entities: solution.entities), associatedPeers: SimpleDictionary()) pollMediaFlags |= 1 << 1 } - - //TODO:fix - let _ = correctAnswers - let correctAnswerss: [Int32] = [] - return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(.init(flags: pollMediaFlags, poll: .poll(.init(id: poll.pollId.id, flags: pollFlags, question: .textWithEntities(.init(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary()))), answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: nil, hash: 0)), correctAnswers: correctAnswerss, attachedMedia: nil, solution: mappedSolution, solutionEntities: mappedSolutionEntities, solutionMedia: nil)), replyMarkup: nil, entities: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, quickReplyShortcutId: nil)) + return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: nil, media: .inputMediaPoll(.init(flags: pollMediaFlags, poll: .poll(.init(id: poll.pollId.id, flags: pollFlags, question: .textWithEntities(.init(text: poll.text, entities: apiEntitiesFromMessageTextEntities(poll.textEntities, associatedPeers: SimpleDictionary()))), answers: poll.options.map({ $0.apiOption }), closePeriod: poll.deadlineTimeout, closeDate: poll.deadlineDate, countriesIso2: poll.countries, hash: 0)), correctAnswers: correctAnswersIndices, attachedMedia: nil, solution: mappedSolution, solutionEntities: mappedSolutionEntities, solutionMedia: nil)), replyMarkup: nil, entities: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, quickReplyShortcutId: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift index 404693261f..1d6e1927c1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift @@ -339,7 +339,6 @@ private class ReplyThreadHistoryContextImpl { } var markMainAsRead = false - markMainAsRead = !"".isEmpty if var data = transaction.getMessageHistoryThreadInfo(peerId: peerId, threadId: threadId)?.data.get(MessageHistoryThreadData.self) { if messageIndex.id.id >= data.maxIncomingReadId { @@ -348,6 +347,7 @@ private class ReplyThreadHistoryContextImpl { data.maxIncomingReadId = messageIndex.id.id } + var newCountIsZero = false if let topMessageIndex = transaction.getMessageHistoryThreadTopMessage(peerId: peerId, threadId: threadId, namespaces: Set([Namespaces.Message.Cloud])) { if messageIndex.id.id >= topMessageIndex.id.id { let containingHole = transaction.getThreadIndexHole(peerId: peerId, threadId: threadId, namespace: topMessageIndex.id.namespace, containing: topMessageIndex.id.id) @@ -357,6 +357,24 @@ private class ReplyThreadHistoryContextImpl { } } } + newCountIsZero = data.incomingUnreadCount == 0 + + if newCountIsZero, let peer = transaction.getPeer(peerId), peer.isForumOrMonoForum { + var allTopicsAreRead = true + for item in transaction.getMessageHistoryThreadIndex(peerId: peer.id, limit: 100) { + guard let data = transaction.getMessageHistoryThreadInfo(peerId: messageIndex.id.peerId, threadId: item.threadId)?.data.get(MessageHistoryThreadData.self) else { + continue + } + if data.incomingUnreadCount != 0 { + allTopicsAreRead = false + break + } + } + + if allTopicsAreRead { + markMainAsRead = true + } + } data.maxKnownMessageId = max(data.maxKnownMessageId, messageIndex.id.id) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportContent.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportContent.swift index 4334c6ace5..bf261814fa 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportContent.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReportContent.swift @@ -21,13 +21,13 @@ public enum ReportContentError { } public enum ReportContentSubject: Equatable { - case peer(EnginePeer.Id) + case peer(EnginePeer.Id, sourceMessageId: EngineMessage.Id? = nil) case messages([EngineMessage.Id]) case stories(EnginePeer.Id, [Int32]) public var peerId: EnginePeer.Id { switch self { - case let .peer(peerId): + case let .peer(peerId, _): return peerId case let .messages(messageIds): return messageIds.first!.peerId @@ -39,7 +39,13 @@ public enum ReportContentSubject: Equatable { func _internal_reportContent(account: Account, subject: ReportContentSubject, option: Data?, message: String?) -> Signal { return account.postbox.transaction { transaction -> Signal in - guard let peer = transaction.getPeer(subject.peerId), let inputPeer = apiInputPeer(peer) else { + let sourceMessageId: MessageId? + if case let .peer(_, messageId) = subject { + sourceMessageId = messageId + } else { + sourceMessageId = nil + } + guard let peer = transaction.getPeer(subject.peerId), let inputPeer = apiInputPeer(peer, sourceMessageId: sourceMessageId, transaction: transaction) else { return .fail(.generic) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift index 47fae09fa3..bd93076922 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift @@ -58,11 +58,11 @@ func _internal_requestStartBotInGroup(account: Account, botPeerId: PeerId, group |> mapToSignal { participant -> Signal in return account.postbox.transaction { transaction -> StartBotInGroupResult in if let participant = participant, let peer = transaction.getPeer(participant.peerId) { - var peers: [PeerId: Peer] = [:] + var peers: [EnginePeer.Id: EnginePeer] = [:] let presences: [PeerId: PeerPresence] = [:] - - peers[peer.id] = peer - return .channelParticipant(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: presences)) + + peers[peer.id] = EnginePeer(peer) + return .channelParticipant(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer), peers: peers, presences: presences)) } else { return .none } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index d89275a8ed..f67efe8a1c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -6,8 +6,7 @@ import MtProtoKit public enum SearchMessagesLocation: Equatable { - case general(scope: TelegramSearchPeersScope, tags: MessageTags?, minDate: Int32?, maxDate: Int32?) - case group(groupId: PeerGroupId, tags: MessageTags?, minDate: Int32?, maxDate: Int32?) + case general(scope: TelegramSearchPeersScope, groupId: PeerGroupId?, tags: MessageTags?, minDate: Int32?, maxDate: Int32?, folderId: Int32?) case peer(peerId: PeerId, fromId: PeerId?, tags: MessageTags?, reactions: [MessageReaction.Reaction]?, threadId: Int64?, minDate: Int32?, maxDate: Int32?) case sentMedia(tags: MessageTags?) } @@ -471,17 +470,17 @@ 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(_, groupId, tags, minDate, maxDate, _): var flags: Int32 = 0 let folderId: Int32? - if case let .group(groupId, _, _, _) = location { + if let groupId { folderId = groupId.rawValue flags |= (1 << 0) } else { 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 +507,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 +566,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 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/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift index 67aa4096db..a0603c10be 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SendAsPeers.swift @@ -5,19 +5,15 @@ import TelegramApi import MtProtoKit public struct SendAsPeer: Equatable { - public let peer: Peer + public let peer: EnginePeer public let subscribers: Int32? public let isPremiumRequired: Bool - - public init(peer: Peer, subscribers: Int32?, isPremiumRequired: Bool) { + + public init(peer: EnginePeer, subscribers: Int32?, isPremiumRequired: Bool) { self.peer = peer self.subscribers = subscribers self.isPremiumRequired = isPremiumRequired } - - public static func ==(lhs: SendAsPeer, rhs: SendAsPeer) -> Bool { - return lhs.peer.isEqual(rhs.peer) && lhs.subscribers == rhs.subscribers && lhs.isPremiumRequired == rhs.isPremiumRequired - } } public final class CachedSendAsPeers: Codable { @@ -61,7 +57,7 @@ func _internal_cachedPeerSendAsAvailablePeers(account: Account, peerId: PeerId) if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData { subscribers = cachedData.participantsSummary.memberCount } - peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id))) + peers.append(SendAsPeer(peer: EnginePeer(peer), subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id))) } } return (peers, cached.timestamp) @@ -167,7 +163,7 @@ func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network, peers.append(peer) } } - return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) } + return peers.map { SendAsPeer(peer: EnginePeer($0), subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) } } } } @@ -233,7 +229,7 @@ func _internal_cachedLiveStorySendAsAvailablePeers(account: Account, peerId: Pee if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData { subscribers = cachedData.participantsSummary.memberCount } - peers.append(SendAsPeer(peer: peer, subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id))) + peers.append(SendAsPeer(peer: EnginePeer(peer), subscribers: subscribers, isPremiumRequired: cached.premiumRequiredPeerIds.contains(peer.id))) } } return (peers, cached.timestamp) @@ -327,7 +323,7 @@ func _internal_liveStorySendAsAvailablePeers(account: Account, peerId: PeerId) - peers.append(peer) } } - return peers.map { SendAsPeer(peer: $0, subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) } + return peers.map { SendAsPeer(peer: EnginePeer($0), subscribers: subscribers[$0.id], isPremiumRequired: premiumRequiredPeerIds.contains($0.id)) } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 5740b8b987..553cf4a3b9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -141,8 +141,11 @@ public extension TelegramEngine { return _internal_searchHashtagPosts(account: self.account, hashtag: hashtag, state: state, limit: limit) } - public func downloadMessage(messageId: MessageId) -> Signal { + public func downloadMessage(messageId: EngineMessage.Id) -> Signal { return _internal_downloadMessage(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, messageId: messageId) + |> map { message -> EngineMessage? in + return message.flatMap(EngineMessage.init) + } } public func searchMessageIdByTimestamp(peerId: PeerId, threadId: Int64?, timestamp: Int32) -> Signal { @@ -166,6 +169,14 @@ public extension TelegramEngine { } |> ignoreValues } + + public func deleteAllReactionsWithAuthor(peerId: EnginePeer.Id, authorId: EnginePeer.Id, aroundMessageId: EngineMessage.Id? = nil) -> Signal { + return _internal_deleteAllReactionsWithAuthor(account: self.account, peerId: peerId, authorId: authorId, aroundMessageId: aroundMessageId) + } + + public func deleteReaction(messageId: EngineMessage.Id, authorId: EnginePeer.Id) -> Signal { + return _internal_deleteReaction(account: self.account, messageId: messageId, authorId: authorId) + } public func clearCallHistory(forEveryone: Bool) -> Signal { return _internal_clearCallHistory(account: self.account, forEveryone: forEveryone) @@ -208,6 +219,10 @@ public extension TelegramEngine { return _internal_forwardGameWithScore(account: self.account, messageId: messageId, to: peerId, threadId: threadId, as: senderPeerId) } + public func sendBotGame(botPeerId: PeerId, game: String, to peerId: PeerId, threadId: Int64?) -> Signal { + return _internal_sendBotGame(account: self.account, botPeerId: botPeerId, game: game, to: peerId, threadId: threadId) + } + public func requestUpdatePinnedMessage(peerId: PeerId, update: PinnedMessageUpdate) -> Signal { return _internal_requestUpdatePinnedMessage(account: self.account, peerId: peerId, update: update) } @@ -461,8 +476,11 @@ public extension TelegramEngine { return _internal_recentlyUsedHashtags(postbox: self.account.postbox) } - public func topPeerActiveLiveLocationMessages(peerId: PeerId) -> Signal<(Peer?, [Message]), NoError> { + public func topPeerActiveLiveLocationMessages(peerId: EnginePeer.Id) -> Signal<(EnginePeer?, [EngineMessage]), NoError> { return _internal_topPeerActiveLiveLocationMessages(viewTracker: self.account.viewTracker, accountPeerId: self.account.peerId, peerId: peerId) + |> map { peer, messages -> (EnginePeer?, [EngineMessage]) in + return (peer.flatMap(EnginePeer.init), messages.map(EngineMessage.init)) + } } public func chatList(group: EngineChatList.Group, count: Int) -> Signal { @@ -655,8 +673,32 @@ public extension TelegramEngine { } } - public func composeMessageWithAI(text: String, entities: [MessageTextEntity], proofread: Bool = false, translateToLang: String? = nil, changeTone: String? = nil, emojify: Bool = false) -> Signal<(String, [MessageTextEntity]), TranslationError> { - return _internal_composeMessageWithAI(network: self.account.network, text: text, entities: entities, proofread: proofread, translateToLang: translateToLang, changeTone: changeTone, emojify: emojify) + public func composeMessageWithAI(text: String, entities: [MessageTextEntity], proofread: Bool = false, translateToLang: String? = nil, changeStyle: TelegramComposeAIMessageMode.CloudStyle.Reference? = nil, emojify: Bool = false) -> Signal<(String, [MessageTextEntity]), TranslationError> { + return _internal_composeMessageWithAI(account: self.account, text: text, entities: entities, proofread: proofread, translateToLang: translateToLang, changeStyle: changeStyle, emojify: emojify) + } + + public func createAITextStyle(displayAuthor: Bool, emojiFileId: Int64, title: String, prompt: String) -> Signal { + return _internal_createAITextStyle(account: self.account, displayAuthor: displayAuthor, emojiFileId: emojiFileId, title: title, prompt: prompt) + } + + public func editAITextStyle(id: Int64, accessHash: Int64, displayAuthor: Bool, emojiFileId: Int64, title: String, prompt: String) -> Signal { + return _internal_editAITextStyle(account: self.account, id: id, accessHash: accessHash, displayAuthor: displayAuthor, emojiFileId: emojiFileId, title: title, prompt: prompt) + } + + public func deleteAITextStyle(id: Int64, accessHash: Int64) -> Signal { + return _internal_deleteAITextStyle(account: self.account, id: id, accessHash: accessHash) + } + + public func unsaveAITextStyle(id: Int64, accessHash: Int64) -> Signal { + return _internal_unsaveAITextStyle(account: self.account, id: id, accessHash: accessHash) + } + + public func getAIComposeToneExample(slug: String, num: Int32) -> Signal { + return _internal_getAIComposeToneExample(network: self.account.network, slug: slug, num: num) + } + + public func getAIComposeToneExample(reference: TelegramComposeAIMessageMode.CloudStyle.Reference, num: Int32) -> Signal { + return _internal_getAIComposeToneExample(network: self.account.network, tone: reference, num: num) } public func translate(texts: [(String, [MessageTextEntity])], toLang: String, tone: TranslationTone = .neutral) -> Signal<[(String, [MessageTextEntity])], TranslationError> { @@ -919,12 +961,6 @@ public extension TelegramEngine { |> ignoreValues } - public func getSynchronizeAutosaveItemOperations() -> Signal<[(index: Int32, message: Message, mediaId: MediaId)], NoError> { - return self.account.postbox.transaction { transaction -> [(index: Int32, message: Message, mediaId: MediaId)] in - return _internal_getSynchronizeAutosaveItemOperations(transaction: transaction) - } - } - func removeSyncrhonizeAutosaveItemOperations(indices: [Int32]) { let _ = (self.account.postbox.transaction { transaction -> Void in _internal_removeSyncrhonizeAutosaveItemOperations(transaction: transaction, indices: indices) @@ -1788,14 +1824,29 @@ public extension TelegramEngine { |> ignoreValues } - public func composeAIMessageStyles() -> Signal<[TelegramComposeAIMessageMode.Style], NoError> { - return _internal_composeAIMessageStyles(account: self.account) + public func composeAIMessageStyles() -> Signal<[TelegramComposeAIMessageMode.CloudStyle], NoError> { + return _internal_cachedCloudAITextStyles(postbox: self.account.postbox) + |> map { value in + return value?.items ?? [] + } + } + + public func requestAIMessageStyle(slug: String) -> Signal<(style: TelegramComposeAIMessageMode.CloudStyle, initialPreview: AIMessageStylePreview?)?, NoError> { + return _internal_requestAIMessageStyle(account: self.account, slug: slug) + } + + public func installAIMessageStyle(style: TelegramComposeAIMessageMode.CloudStyle.Custom) -> Signal { + return _internal_installAIMessageStyle(account: self.account, style: style) } public func composeAIMessage(text: TextWithEntities, mode: TelegramComposeAIMessageMode) -> Signal { return _internal_composeAIMessage(account: self.account, text: text, mode: mode) } + public func requestAIMessageStylePreview(reference: TelegramComposeAIMessageMode.CloudStyle.Reference, index: Int) -> Signal { + return _internal_requestAIMessageStylePreview(account: self.account, reference: reference, index: index) + } + public func requestMiniAppButton(peerId: EnginePeer.Id, requestId: String) -> Signal { let account = self.account return self.account.postbox.transaction { transaction -> Api.InputUser? in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift index 42cd3a48d1..3f0d2e8462 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift @@ -68,7 +68,7 @@ func _internal_translate(network: Network, text: String, toLang: String, entitie } } -func _internal_composeMessageWithAI(network: Network, text: String, entities: [MessageTextEntity], proofread: Bool = false, translateToLang: String? = nil, changeTone: String? = nil, emojify: Bool = false) -> Signal<(String, [MessageTextEntity]), TranslationError> { +func _internal_composeMessageWithAI(account: Account, text: String, entities: [MessageTextEntity], proofread: Bool = false, translateToLang: String? = nil, changeStyle: TelegramComposeAIMessageMode.CloudStyle.Reference? = nil, emojify: Bool = false) -> Signal<(String, [MessageTextEntity]), TranslationError> { var flags: Int32 = 0 if proofread { flags |= (1 << 0) @@ -76,8 +76,11 @@ func _internal_composeMessageWithAI(network: Network, text: String, entities: [M if translateToLang != nil { flags |= (1 << 1) } - if changeTone != nil { + + var changeTone: Api.InputAiComposeTone? + if let changeStyle { flags |= (1 << 2) + changeTone = changeStyle.apiInputStyle } if emojify { flags |= (1 << 3) @@ -85,7 +88,7 @@ func _internal_composeMessageWithAI(network: Network, text: String, entities: [M let apiText: Api.TextWithEntities = .textWithEntities(.init(text: text, entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary()))) - return network.request(Api.functions.messages.composeMessageWithAI(flags: flags, text: apiText, translateToLang: translateToLang, changeTone: changeTone)) + return account.network.request(Api.functions.messages.composeMessageWithAI(flags: flags, text: apiText, translateToLang: translateToLang, tone: changeTone)) |> mapError { error -> TranslationError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded @@ -409,32 +412,289 @@ func _internal_togglePeerMessagesTranslationHidden(account: Account, peerId: Eng } public enum TelegramComposeAIMessageMode { - public enum StyleId: Hashable { - case neutral - case style(String) - } - - public struct Style: Equatable { - public let type: String - public let title: String - public let emojiFileId: Int64? + public final class CloudStyle: Equatable, Codable { + public enum Id: Hashable { + case standard(String) + case custom(Int64) + } + + public enum Reference: Hashable { + case standard(String) + case custom(id: Int64, accessHash: Int64) + + public var id: Id { + switch self { + case let .standard(type): + return .standard(type) + case let .custom(id, _): + return .custom(id) + } + } + } + + public final class Standard: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case type + case title + case emojiFileId + } + + public let type: String + public let title: String + public let emojiFileId: Int64? + + public init(type: String, title: String, emojiFileId: Int64?) { + self.type = type + self.title = title + self.emojiFileId = emojiFileId + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.type = try container.decode(String.self, forKey: .type) + self.title = try container.decode(String.self, forKey: .title) + self.emojiFileId = try container.decodeIfPresent(Int64.self, forKey: .emojiFileId) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.type, forKey: .type) + try container.encode(self.title, forKey: .title) + try container.encodeIfPresent(self.emojiFileId, forKey: .emojiFileId) + } + + public static func ==(lhs: Standard, rhs: Standard) -> Bool { + if lhs.type != rhs.type { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.emojiFileId != rhs.emojiFileId { + return false + } + return true + } + } + + public final class Custom: Codable, Equatable { + private enum CodingKeys: String, CodingKey { + case isCreator + case id + case accessHash + case slug + case emojiFileId + case title + case prompt + case userCount + case authorId + } + + public let isCreator: Bool + public let id: Int64 + public let accessHash: Int64 + public let slug: String + public let emojiFileId: Int64? + public let title: String + public let prompt: String? + public let userCount: Int? + public let authorId: EnginePeer.Id? + + public init(isCreator: Bool, id: Int64, accessHash: Int64, slug: String, emojiFileId: Int64?, title: String, prompt: String?, userCount: Int?, authorId: EnginePeer.Id?) { + self.isCreator = isCreator + self.id = id + self.accessHash = accessHash + self.slug = slug + self.emojiFileId = emojiFileId + self.title = title + self.prompt = prompt + self.userCount = userCount + self.authorId = authorId + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.isCreator = try container.decode(Bool.self, forKey: .isCreator) + self.id = try container.decode(Int64.self, forKey: .id) + self.accessHash = try container.decode(Int64.self, forKey: .accessHash) + self.slug = try container.decode(String.self, forKey: .slug) + self.emojiFileId = try container.decodeIfPresent(Int64.self, forKey: .emojiFileId) + self.title = try container.decode(String.self, forKey: .title) + self.prompt = try container.decodeIfPresent(String.self, forKey: .prompt) + self.userCount = try container.decodeIfPresent(Int32.self, forKey: .userCount).flatMap(Int.init) + self.authorId = try container.decodeIfPresent(EnginePeer.Id.self, forKey: .authorId) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.isCreator, forKey: .isCreator) + try container.encode(self.id, forKey: .id) + try container.encode(self.accessHash, forKey: .accessHash) + try container.encode(self.slug, forKey: .slug) + try container.encodeIfPresent(self.emojiFileId, forKey: .emojiFileId) + try container.encode(self.title, forKey: .title) + try container.encodeIfPresent(self.prompt, forKey: .prompt) + try container.encodeIfPresent(self.userCount.flatMap(Int32.init(clamping:)), forKey: .userCount) + try container.encodeIfPresent(self.authorId, forKey: .authorId) + } + + public static func ==(lhs: Custom, rhs: Custom) -> Bool { + if lhs.isCreator != rhs.isCreator { + return false + } + if lhs.id != rhs.id { + return false + } + if lhs.accessHash != rhs.accessHash { + return false + } + if lhs.slug != rhs.slug { + return false + } + if lhs.emojiFileId != rhs.emojiFileId { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.prompt != rhs.prompt { + return false + } + if lhs.userCount != rhs.userCount { + return false + } + if lhs.authorId != rhs.authorId { + return false + } + return true + } + } + + public enum Content: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case discriminator = "d" + case value = "v" + } + + case standard(Standard) + case custom(Custom) + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + switch try container.decode(Int32.self, forKey: .discriminator) { + case 0: + self = .standard(try container.decode(Standard.self, forKey: .value)) + case 1: + self = .custom(try container.decode(Custom.self, forKey: .value)) + default: + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "")) + } + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case let .standard(standard): + try container.encode(0 as Int32, forKey: .discriminator) + try container.encode(standard, forKey: .value) + case let .custom(custom): + try container.encode(1 as Int32, forKey: .discriminator) + try container.encode(custom, forKey: .value) + } + } + } + + public let content: Content public var id: StyleId { - return .style(self.type) + switch self.content { + case let .standard(standard): + return .style(.standard(standard.type)) + case let .custom(custom): + return .style(.custom(custom.id)) + } } - public init(type: String, title: String, emojiFileId: Int64?) { - self.type = type - self.title = title - self.emojiFileId = emojiFileId + public var reference: Reference { + switch self.content { + case let .standard(standard): + return .standard(standard.type) + case let .custom(custom): + return .custom(id: custom.id, accessHash: custom.accessHash) + } + } + + public init(content: Content) { + self.content = content + } + + public static func ==(lhs: CloudStyle, rhs: CloudStyle) -> Bool { + if lhs.content != rhs.content { + return false + } + return true } } - case translate(toLanguage: String, emojify: Bool, style: StyleId) - case stylize(emojify: Bool, style: StyleId) + public enum StyleId: Hashable { + case neutral + case style(CloudStyle.Id) + } + + public enum StyleReference: Hashable { + case neutral + case style(CloudStyle.Reference) + + public var id: StyleId { + switch self { + case .neutral: + return .neutral + case let .style(style): + return .style(style.id) + } + } + } + + case translate(toLanguage: String, emojify: Bool, style: StyleReference) + case stylize(emojify: Bool, style: StyleReference) case proofread } +extension TelegramComposeAIMessageMode.CloudStyle { + convenience init(apiTone: Api.AiComposeTone) { + /* + aiComposeTone#12ea1465 flags:# creator:flags.0?true id:long access_hash:long slug:string title:string emoji_id:flags.1?long prompt:string installs_count:flags.2?int author_id:flags.3?long = AiComposeTone; + aiComposeToneDefault#9bad6414 tone:string emoji_id:long title:string = AiComposeTone; + */ + switch apiTone { + case let .aiComposeTone(aiComposeTone): + self.init(content: .custom(Custom( + isCreator: (aiComposeTone.flags & (1 << 0)) != 0, + id: aiComposeTone.id, + accessHash: aiComposeTone.accessHash, + slug: aiComposeTone.slug, + emojiFileId: aiComposeTone.emojiId, + title: aiComposeTone.title, + prompt: aiComposeTone.prompt, + userCount: aiComposeTone.installsCount.flatMap(Int.init), + authorId: aiComposeTone.authorId.flatMap { id -> EnginePeer.Id in + return EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)) + } + ))) + case let .aiComposeToneDefault(aiComposeToneDefault): + self.init(content: .standard(Standard( + type: aiComposeToneDefault.tone, + title: aiComposeToneDefault.title, + emojiFileId: aiComposeToneDefault.emojiId + ))) + } + } +} + public final class TelegramAIComposeMessageResult { public let text: TextWithEntities public let diffRanges: [Range] @@ -454,23 +714,14 @@ extension TextWithEntities { } } -func _internal_composeAIMessageStyles(account: Account) -> Signal<[TelegramComposeAIMessageMode.Style], NoError> { - return account.postbox.transaction { transaction -> [TelegramComposeAIMessageMode.Style] in - var result: [TelegramComposeAIMessageMode.Style] = [] - if let data = currentAppConfiguration(transaction: transaction).data, let value = data["ai_compose_styles"] as? [[String]] { - for item in value { - if item.count >= 3 { - let emojiFileId: Int64? = Int64(item[1]) - result.append(TelegramComposeAIMessageMode.Style(type: item[0], title: item[2], emojiFileId: emojiFileId)) - } - } +extension TelegramComposeAIMessageMode.CloudStyle.Reference { + var apiInputStyle: Api.InputAiComposeTone { + switch self { + case let .standard(type): + return .inputAiComposeToneDefault(Api.InputAiComposeTone.Cons_inputAiComposeToneDefault(tone: type)) + case let .custom(id, accessHash): + return .inputAiComposeToneID(Api.InputAiComposeTone.Cons_inputAiComposeToneID(id: id, accessHash: accessHash)) } - #if DEBUG && false - for item in Array(result) { - result.append(TelegramComposeAIMessageMode.Style(name: item.name + "_", emoji: item.emoji)) - } - #endif - return result } } @@ -482,7 +733,7 @@ public enum TelegramAIComposeMessageError { func _internal_composeAIMessage(account: Account, text: TextWithEntities, mode: TelegramComposeAIMessageMode) -> Signal { var flags: Int32 = 0 var translateToLang: String? - var changeTone: String? + var changeTone: Api.InputAiComposeTone? switch mode { case let .translate(toLanguage, emojify, style): translateToLang = toLanguage @@ -492,8 +743,8 @@ func _internal_composeAIMessage(account: Account, text: TextWithEntities, mode: flags |= (1 << 3) } - if case let .style(name) = style { - changeTone = name + if case let .style(reference) = style { + changeTone = reference.apiInputStyle flags |= (1 << 2) } case let .stylize(emojify, style): @@ -501,8 +752,8 @@ func _internal_composeAIMessage(account: Account, text: TextWithEntities, mode: flags |= (1 << 3) } - if case let .style(name) = style { - changeTone = name + if case let .style(reference) = style { + changeTone = reference.apiInputStyle flags |= (1 << 2) } case .proofread: @@ -511,7 +762,7 @@ func _internal_composeAIMessage(account: Account, text: TextWithEntities, mode: let inputText: Api.TextWithEntities = .textWithEntities(Api.TextWithEntities.Cons_textWithEntities(text: text.text, entities: apiEntitiesFromMessageTextEntities(text.entities, associatedPeers: SimpleDictionary()))) - return account.network.request(Api.functions.messages.composeMessageWithAI(flags: flags, text: inputText, translateToLang: translateToLang, changeTone: changeTone)) + return account.network.request(Api.functions.messages.composeMessageWithAI(flags: flags, text: inputText, translateToLang: translateToLang, tone: changeTone)) |> `catch` { error -> Signal in if error.errorDescription == "AICOMPOSE_FLOOD_PREMIUM" { return .fail(.nonPremiumFlood) @@ -549,3 +800,325 @@ func _internal_composeAIMessage(account: Account, text: TextWithEntities, mode: } } } + +public final class AIMessageStylePreview { + public let from: TextWithEntities + public let to: TextWithEntities + public let index: Int? + + public init(from: TextWithEntities, to: TextWithEntities, index: Int?) { + self.from = from + self.to = to + self.index = index + } +} + +extension AIMessageStylePreview { + convenience init(apiPreview: Api.AiComposeToneExample, index: Int) { + switch apiPreview { + case let .aiComposeToneExample(aiComposeToneExample): + self.init(from: TextWithEntities(apiValue: aiComposeToneExample.from), to: TextWithEntities(apiValue: aiComposeToneExample.to), index: index) + } + } +} + +public func _internal_requestAIMessageStylePreview(account: Account, reference: TelegramComposeAIMessageMode.CloudStyle.Reference, index: Int) -> Signal { + return account.network.request(Api.functions.aicompose.getToneExample(tone: reference.apiInputStyle, num: Int32(index))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> AIMessageStylePreview? in + guard let result else { + return nil + } + return AIMessageStylePreview( + apiPreview: result, + index: index + ) + } +} + +public enum CreateAITextStyleError { + case generic + case premiumRequired +} + +func _internal_createAITextStyle(account: Account, displayAuthor: Bool, emojiFileId: Int64, title: String, prompt: String) -> Signal { + var flags: Int32 = 0 + if displayAuthor { + flags |= (1 << 0) + } + return account.network.request(Api.functions.aicompose.createTone( + flags: flags, + emojiId: emojiFileId, + title: title, + prompt: prompt + )) + |> mapError { error -> CreateAITextStyleError in + if error.errorDescription == "TONES_SAVED_TOO_MANY" { + return .premiumRequired + } + return .generic + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> TelegramComposeAIMessageMode.CloudStyle in + let style = TelegramComposeAIMessageMode.CloudStyle(apiTone: result) + + let styles = _internal_cachedCloudAITextStyles(transaction: transaction) + var items = styles?.items ?? [] + items.insert(style, at: 0) + _internal_setCachedCloudAITextStyles(transaction: transaction, styles: CachedCloudAITextStyles(items: items, hash: 0)) + return style + } + |> castError(CreateAITextStyleError.self) + } +} + +public enum EditAITextStyleError { + case generic +} + +func _internal_editAITextStyle(account: Account, id: Int64, accessHash: Int64, displayAuthor: Bool, emojiFileId: Int64, title: String, prompt: String) -> Signal { + var flags: Int32 = 0 + flags |= (1 << 0) + flags |= (1 << 1) + flags |= (1 << 2) + flags |= (1 << 3) + return account.network.request(Api.functions.aicompose.updateTone( + flags: flags, + tone: .inputAiComposeToneID(Api.InputAiComposeTone.Cons_inputAiComposeToneID(id: id, accessHash: accessHash)), + displayAuthor: displayAuthor ? .boolTrue : .boolFalse, + emojiId: emojiFileId, + title: title, + prompt: prompt + )) + |> mapError { _ -> EditAITextStyleError in + return .generic + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> TelegramComposeAIMessageMode.CloudStyle in + let style = TelegramComposeAIMessageMode.CloudStyle(apiTone: result) + + let styles = _internal_cachedCloudAITextStyles(transaction: transaction) + var items = styles?.items ?? [] + if let index = items.firstIndex(where: { $0.id == .style(.custom(id)) }) { + items[index] = style + } + _internal_setCachedCloudAITextStyles(transaction: transaction, styles: CachedCloudAITextStyles(items: items, hash: 0)) + return style + } + |> castError(EditAITextStyleError.self) + } +} + +public struct TelegramAIComposeToneExample: Equatable { + public let from: TextWithEntities + public let to: TextWithEntities + public init(from: TextWithEntities, to: TextWithEntities) { + self.from = from + self.to = to + } +} + +private func _internal_mapToneExample(_ apiExample: Api.AiComposeToneExample) -> TelegramAIComposeToneExample { + switch apiExample { + case let .aiComposeToneExample(data): + return TelegramAIComposeToneExample(from: TextWithEntities(apiValue: data.from), to: TextWithEntities(apiValue: data.to)) + } +} + +func _internal_getAIComposeToneExample(network: Network, tone: TelegramComposeAIMessageMode.CloudStyle.Reference, num: Int32) -> Signal { + return network.request(Api.functions.aicompose.getToneExample(tone: tone.apiInputStyle, num: num)) + |> map { apiExample -> TelegramAIComposeToneExample? in + return _internal_mapToneExample(apiExample) + } + |> `catch` { _ -> Signal in + return .single(nil) + } +} + +func _internal_getAIComposeToneExample(network: Network, slug: String, num: Int32) -> Signal { + return network.request(Api.functions.aicompose.getToneExample(tone: .inputAiComposeToneSlug(Api.InputAiComposeTone.Cons_inputAiComposeToneSlug(slug: slug)), num: num)) + |> map { apiExample -> TelegramAIComposeToneExample? in + return _internal_mapToneExample(apiExample) + } + |> `catch` { _ -> Signal in + return .single(nil) + } +} + +public enum DeleteAITextStyleError { + case generic +} + +func _internal_deleteAITextStyle(account: Account, id: Int64, accessHash: Int64) -> Signal { + return account.network.request(Api.functions.aicompose.deleteTone(tone: .inputAiComposeToneID(Api.InputAiComposeTone.Cons_inputAiComposeToneID(id: id, accessHash: accessHash)))) + |> mapError { _ -> DeleteAITextStyleError in + return .generic + } + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> Void in + let styles = _internal_cachedCloudAITextStyles(transaction: transaction) + var items = styles?.items ?? [] + items.removeAll(where: { $0.id == .style(.custom(id)) }) + _internal_setCachedCloudAITextStyles(transaction: transaction, styles: CachedCloudAITextStyles(items: items, hash: 0)) + } + |> castError(DeleteAITextStyleError.self) + |> ignoreValues + } +} + +func _internal_unsaveAITextStyle(account: Account, id: Int64, accessHash: Int64) -> Signal { + return account.network.request(Api.functions.aicompose.saveTone(tone: .inputAiComposeToneID(Api.InputAiComposeTone.Cons_inputAiComposeToneID(id: id, accessHash: accessHash)), unsave: .boolTrue)) + |> mapError { _ -> DeleteAITextStyleError in + return .generic + } + |> mapToSignal { _ -> Signal in + return account.postbox.transaction { transaction -> Void in + let styles = _internal_cachedCloudAITextStyles(transaction: transaction) + var items = styles?.items ?? [] + items.removeAll(where: { $0.id == .style(.custom(id)) }) + _internal_setCachedCloudAITextStyles(transaction: transaction, styles: CachedCloudAITextStyles(items: items, hash: 0)) + } + |> castError(DeleteAITextStyleError.self) + |> ignoreValues + } +} + +final class CachedCloudAITextStyles: Codable { + let items: [TelegramComposeAIMessageMode.CloudStyle] + let hash: Int64 + + init(items: [TelegramComposeAIMessageMode.CloudStyle], hash: Int64) { + self.items = items + self.hash = hash + } +} + +func _internal_cachedCloudAITextStyles(postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> CachedCloudAITextStyles? in + return _internal_cachedCloudAITextStyles(transaction: transaction) + } +} + +func _internal_cachedCloudAITextStyles(transaction: Transaction) -> CachedCloudAITextStyles? { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: 0) + + let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedCloudAITextStyles, key: key))?.get(CachedCloudAITextStyles.self) + if let cached { + return cached + } else { + return nil + } +} + +func _internal_setCachedCloudAITextStyles(transaction: Transaction, styles: CachedCloudAITextStyles) { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: 0) + + if let entry = CodableEntry(styles) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedCloudAITextStyles, key: key), entry: entry) + } +} + +func _internal_requestAIMessageStyle(account: Account, slug: String) -> Signal<(style: TelegramComposeAIMessageMode.CloudStyle, initialPreview: AIMessageStylePreview?)?, NoError> { + return account.network.request(Api.functions.aicompose.getTone(tone: .inputAiComposeToneSlug(Api.InputAiComposeTone.Cons_inputAiComposeToneSlug(slug: slug)))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<(style: TelegramComposeAIMessageMode.CloudStyle, initialPreview: AIMessageStylePreview?)?, NoError> in + guard let result else { + return .single(nil) + } + return account.postbox.transaction { transaction -> (style: TelegramComposeAIMessageMode.CloudStyle, initialPreview: AIMessageStylePreview?)? in + switch result { + case let .tones(tones): + guard let tone = tones.tones.first else { + return nil + } + updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: AccumulatedPeers(users: tones.users)) + return (TelegramComposeAIMessageMode.CloudStyle(apiTone: tone), nil) + case .tonesNotModified: + return nil + } + } + } +} + +public enum InstallAIMessageStyleError { + case generic +} + +func _internal_installAIMessageStyle(account: Account, style: TelegramComposeAIMessageMode.CloudStyle.Custom) -> Signal { + return account.network.request(Api.functions.aicompose.saveTone(tone: .inputAiComposeToneID(Api.InputAiComposeTone.Cons_inputAiComposeToneID(id: style.id, accessHash: style.accessHash)), unsave: .boolFalse)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> castError(InstallAIMessageStyleError.self) + |> mapToSignal { result -> Signal in + if result != nil { + return account.postbox.transaction { transaction -> Void in + var items = _internal_cachedCloudAITextStyles(transaction: transaction)?.items ?? [] + if let index = items.firstIndex(where: { $0.id == .style(.custom(style.id)) }) { + items.remove(at: index) + } + items.insert(TelegramComposeAIMessageMode.CloudStyle(content: .custom(style)), at: 0) + _internal_setCachedCloudAITextStyles(transaction: transaction, styles: CachedCloudAITextStyles(items: items, hash: 0)) + } + |> ignoreValues + |> castError(InstallAIMessageStyleError.self) + } else { + return .fail(.generic) + } + } +} + +func managedSynchronizeCloudAITextStyles(postbox: Postbox, network: Network) -> Signal { + let poll = Signal { subscriber in + let signal: Signal = _internal_cachedCloudAITextStyles(postbox: postbox) + |> mapToSignal { current in + return (network.request(Api.functions.aicompose.getTones(hash: current?.hash ?? 0)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> Signal in + guard let result = result else { + return .complete() + } + + switch result { + case let .tones(tones): + _internal_setCachedCloudAITextStyles(transaction: transaction, styles: CachedCloudAITextStyles( + items: tones.tones.map(TelegramComposeAIMessageMode.CloudStyle.init(apiTone:)), + hash: tones.hash + )) + case .tonesNotModified: + break + } + + return .complete() + } + |> switchToLatest + }) + } + + return signal.start(completed: { + subscriber.putCompletion() + }) + } + + return ( + poll + |> then( + .complete() + |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()) + ) + ) + |> restart +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index 56455cee9c..cb765c543f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -20,7 +20,7 @@ public enum AppStoreTransactionPurpose { case stars(count: Int64, currency: String, amount: Int64, peerId: EnginePeer.Id?) case starsGift(peerId: EnginePeer.Id, count: Int64, currency: String, amount: Int64) case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, currency: String, amount: Int64, users: Int32) - case authCode(restore: Bool, phoneNumber: String, phoneCodeHash: String, currency: String, amount: Int64) + case authCode(restore: Bool, phoneNumber: String, phoneCodeHash: String, premiumDays: Int32, currency: String, amount: Int64) } private func apiInputStorePaymentPurpose(postbox: Postbox, purpose: AppStoreTransactionPurpose) -> Signal { @@ -155,12 +155,12 @@ private func apiInputStorePaymentPurpose(postbox: Postbox, purpose: AppStoreTran return .single(.inputStorePaymentStarsGiveaway(.init(flags: flags, stars: stars, boostPeer: apiBoostPeer, additionalPeers: additionalPeers, countriesIso2: countries, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount, users: users))) } |> switchToLatest - case let .authCode(restore, phoneNumber, phoneCodeHash, currency, amount): + case let .authCode(restore, phoneNumber, phoneCodeHash, premiumDays, currency, amount): var flags: Int32 = 0 if restore { flags |= (1 << 0) } - return .single(.inputStorePaymentAuthCode(.init(flags: flags, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, currency: currency, amount: amount))) + return .single(.inputStorePaymentAuthCode(.init(flags: flags, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, premiumDays: premiumDays, currency: currency, amount: amount))) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift index a1a5efa82f..2d7f0380db 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift @@ -239,20 +239,20 @@ func _internal_addChannelMember(account: Account, peerId: PeerId, memberId: Peer return cachedData } }) - var peers: [PeerId: Peer] = [:] + var peers: [EnginePeer.Id: EnginePeer] = [:] var presences: [PeerId: PeerPresence] = [:] - peers[memberPeer.id] = memberPeer + peers[memberPeer.id] = EnginePeer(memberPeer) if let presence = transaction.getPeerPresence(peerId: memberPeer.id) { presences[memberPeer.id] = presence } if case let .member(_, _, maybeAdminInfo, _, _, _) = updatedParticipant { if let adminInfo = maybeAdminInfo { if let peer = transaction.getPeer(adminInfo.promotedBy) { - peers[peer.id] = peer + peers[peer.id] = EnginePeer(peer) } } } - return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: memberPeer, peers: peers, presences: presences)) + return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(memberPeer), peers: peers, presences: presences)) } |> mapError { _ -> AddChannelMemberError in } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChangePeerNotificationSettings.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChangePeerNotificationSettings.swift index fe3110e357..058519ec1a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChangePeerNotificationSettings.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChangePeerNotificationSettings.swift @@ -62,7 +62,7 @@ func _internal_togglePeerMuted(account: Account, peerId: PeerId, threadId: Int64 } } -public func resolvedAreStoriesMuted(globalSettings: GlobalNotificationSettingsSet, peer: Peer, peerSettings: TelegramPeerNotificationSettings?, topSearchPeers: [PeerId]) -> Bool { +public func resolvedAreStoriesMuted(globalSettings: GlobalNotificationSettingsSet, peer: EnginePeer, peerSettings: TelegramPeerNotificationSettings?, topSearchPeers: [PeerId]) -> Bool { let defaultIsMuted: Bool switch globalSettings.privateChats.storySettings.mute { case .muted: @@ -114,7 +114,7 @@ func _internal_togglePeerStoriesMuted(account: Account, peerId: PeerId) -> Signa case .default: let globalNotificationSettings = transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications)?.get(GlobalNotificationSettings.self) ?? GlobalNotificationSettings.defaultSettings - if resolvedAreStoriesMuted(globalSettings: globalNotificationSettings.effective, peer: peer, peerSettings: previousSettings, topSearchPeers: topSearchPeers) { + if resolvedAreStoriesMuted(globalSettings: globalNotificationSettings.effective, peer: EnginePeer(peer), peerSettings: previousSettings, topSearchPeers: topSearchPeers) { storySettings.mute = .unmuted } else { storySettings.mute = .muted diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index f1481f4e1c..3d37a28416 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -268,7 +268,7 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net let participant = ChannelParticipant(apiParticipant: channelAdminLogEventActionParticipantInviteData.participant) if let peer = peers[participant.peerId] { - action = .participantInvite(RenderedChannelParticipant(participant: participant, peer: peer)) + action = .participantInvite(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer))) } case let .channelAdminLogEventActionParticipantToggleBan(channelAdminLogEventActionParticipantToggleBanData): let (prev, new) = (channelAdminLogEventActionParticipantToggleBanData.prevParticipant, channelAdminLogEventActionParticipantToggleBanData.newParticipant) @@ -276,7 +276,7 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net let newParticipant = ChannelParticipant(apiParticipant: new) if let prevPeer = peers[prevParticipant.peerId], let newPeer = peers[newParticipant.peerId] { - action = .participantToggleBan(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer)) + action = .participantToggleBan(prev: RenderedChannelParticipant(participant: prevParticipant, peer: EnginePeer(prevPeer)), new: RenderedChannelParticipant(participant: newParticipant, peer: EnginePeer(newPeer))) } case let .channelAdminLogEventActionParticipantToggleAdmin(channelAdminLogEventActionParticipantToggleAdminData): let (prev, new) = (channelAdminLogEventActionParticipantToggleAdminData.prevParticipant, channelAdminLogEventActionParticipantToggleAdminData.newParticipant) @@ -284,7 +284,7 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net let newParticipant = ChannelParticipant(apiParticipant: new) if let prevPeer = peers[prevParticipant.peerId], let newPeer = peers[newParticipant.peerId] { - action = .participantToggleAdmin(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer)) + action = .participantToggleAdmin(prev: RenderedChannelParticipant(participant: prevParticipant, peer: EnginePeer(prevPeer)), new: RenderedChannelParticipant(participant: newParticipant, peer: EnginePeer(newPeer))) } case let .channelAdminLogEventActionChangeStickerSet(channelAdminLogEventActionChangeStickerSetData): let (prevStickerset, newStickerset) = (channelAdminLogEventActionChangeStickerSetData.prevStickerset, channelAdminLogEventActionChangeStickerSetData.newStickerset) @@ -480,7 +480,7 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net let newParticipant = ChannelParticipant(apiParticipant: new) if let prevPeer = peers[prevParticipant.peerId], let newPeer = peers[newParticipant.peerId] { - action = .participantSubscriptionExtended(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer)) + action = .participantSubscriptionExtended(prev: RenderedChannelParticipant(participant: prevParticipant, peer: EnginePeer(prevPeer)), new: RenderedChannelParticipant(participant: newParticipant, peer: EnginePeer(newPeer))) } case let .channelAdminLogEventActionToggleAutotranslation(channelAdminLogEventActionToggleAutotranslationData): let newValue = channelAdminLogEventActionToggleAutotranslationData.newValue diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift index 0630e1db6f..228c50bb41 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift @@ -125,19 +125,19 @@ func _internal_updateChannelMemberBannedRights(account: Account, peerId: PeerId, return cachedData } }) - var peers: [PeerId: Peer] = [:] + var peers: [EnginePeer.Id: EnginePeer] = [:] var presences: [PeerId: PeerPresence] = [:] - peers[memberPeer.id] = memberPeer + peers[memberPeer.id] = EnginePeer(memberPeer) if let presence = transaction.getPeerPresence(peerId: memberPeer.id) { presences[memberPeer.id] = presence } if case let .member(_, _, _, maybeBanInfo, _, _) = updatedParticipant, let banInfo = maybeBanInfo { if let peer = transaction.getPeer(banInfo.restrictedBy) { - peers[peer.id] = peer + peers[peer.id] = EnginePeer(peer) } } - return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: memberPeer, peers: peers, presences: presences), isMember) + return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(memberPeer), peers: peers, presences: presences), isMember) } } } else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift index c90272d402..496c6f251a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift @@ -99,10 +99,10 @@ func _internal_channelMembers(postbox: Postbox, network: Network, accountPeerId: let (participants, chats, users) = (channelParticipantsData.participants, channelParticipantsData.chats, channelParticipantsData.users) let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers) - var peers: [PeerId: Peer] = [:] + var peers: [EnginePeer.Id: EnginePeer] = [:] for id in parsedPeers.allIds { if let peer = transaction.getPeer(id) { - peers[peer.id] = peer + peers[peer.id] = EnginePeer(peer) } } @@ -112,7 +112,7 @@ func _internal_channelMembers(postbox: Postbox, network: Network, accountPeerId: if let presence = transaction.getPeerPresence(peerId: participant.peerId) { renderedPresences[participant.peerId] = presence } - items.append(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: renderedPresences)) + items.append(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer), peers: peers, presences: renderedPresences)) } } case .channelParticipantsNotModified: diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift index dc1f7012a9..b7784ca041 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift @@ -167,17 +167,17 @@ func _internal_updateChatOwnership(account: Account, peerId: PeerId, memberId: P return cachedData } }) - var peers: [PeerId: Peer] = [:] + var peers: [EnginePeer.Id: EnginePeer] = [:] var presences: [PeerId: PeerPresence] = [:] - peers[accountUser.id] = accountUser + peers[accountUser.id] = EnginePeer(accountUser) if let presence = transaction.getPeerPresence(peerId: accountUser.id) { presences[accountUser.id] = presence } - peers[user.id] = user + peers[user.id] = EnginePeer(user) if let presence = transaction.getPeerPresence(peerId: user.id) { presences[user.id] = presence } - return [(currentCreator, RenderedChannelParticipant(participant: updatedPreviousCreator, peer: accountUser, peers: peers, presences: presences)), (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: user, peers: peers, presences: presences))] + return [(currentCreator, RenderedChannelParticipant(participant: updatedPreviousCreator, peer: EnginePeer(accountUser), peers: peers, presences: presences)), (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(user), peers: peers, presences: presences))] } |> mapError { _ -> ChatOwnershipTransferError in } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift index aa845ecc20..c9cfe46d38 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift @@ -7,18 +7,18 @@ import MtProtoKit public struct RenderedChannelParticipant: Equatable { public let participant: ChannelParticipant - public let peer: Peer - public let peers: [PeerId: Peer] + public let peer: EnginePeer + public let peers: [EnginePeer.Id: EnginePeer] public let presences: [PeerId: PeerPresence] - - public init(participant: ChannelParticipant, peer: Peer, peers: [PeerId: Peer] = [:], presences: [PeerId: PeerPresence] = [:]) { + + public init(participant: ChannelParticipant, peer: EnginePeer, peers: [EnginePeer.Id: EnginePeer] = [:], presences: [PeerId: PeerPresence] = [:]) { self.participant = participant self.peer = peer self.peers = peers self.presences = presences } - + public static func ==(lhs: RenderedChannelParticipant, rhs: RenderedChannelParticipant) -> Bool { - return lhs.participant == rhs.participant && lhs.peer.isEqual(rhs.peer) + return lhs.participant == rhs.participant && lhs.peer == rhs.peer } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift index dceedea39a..b6de8a22e1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/InactiveChannels.swift @@ -5,17 +5,17 @@ import Postbox import TelegramApi public struct InactiveChannel : Equatable { - public let peer: Peer + public let peer: EnginePeer public let lastActivityDate: Int32 public let participantsCount: Int32? - - init(peer: Peer, lastActivityDate: Int32, participantsCount: Int32?) { + + init(peer: EnginePeer, lastActivityDate: Int32, participantsCount: Int32?) { self.peer = peer self.lastActivityDate = lastActivityDate self.participantsCount = participantsCount } public static func ==(lhs: InactiveChannel, rhs: InactiveChannel) -> Bool { - return lhs.peer.isEqual(rhs.peer) && lhs.lastActivityDate == rhs.lastActivityDate && lhs.participantsCount == rhs.participantsCount + return lhs.peer == rhs.peer && lhs.lastActivityDate == rhs.lastActivityDate && lhs.participantsCount == rhs.participantsCount } } @@ -43,7 +43,7 @@ func _internal_inactiveChannelList(network: Network) -> Signal<[InactiveChannel] } var inactive: [InactiveChannel] = [] for (i, channel) in channels.enumerated() { - inactive.append(InactiveChannel(peer: channel, lastActivityDate: i < dates.count ? dates[i] : 0, participantsCount: participantsCounts[channel.id])) + inactive.append(InactiveChannel(peer: EnginePeer(channel), lastActivityDate: i < dates.count ? dates[i] : 0, participantsCount: participantsCounts[channel.id])) } return inactive } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift index 5bf4891b4f..c1bfd309f7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift @@ -56,12 +56,12 @@ func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> S return .fail(.generic) } return account.postbox.transaction { transaction -> RenderedChannelParticipant? in - var peers: [PeerId: Peer] = [:] + var peers: [EnginePeer.Id: EnginePeer] = [:] var presences: [PeerId: PeerPresence] = [:] guard let peer = transaction.getPeer(account.peerId) else { return nil } - peers[account.peerId] = peer + peers[account.peerId] = EnginePeer(peer) if let presence = transaction.getPeerPresence(peerId: account.peerId) { presences[account.peerId] = presence } @@ -74,12 +74,12 @@ func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> S if case let .member(_, _, maybeAdminInfo, _, _, _) = updatedParticipant { if let adminInfo = maybeAdminInfo { if let peer = transaction.getPeer(adminInfo.promotedBy) { - peers[peer.id] = peer + peers[peer.id] = EnginePeer(peer) } } } - return RenderedChannelParticipant(participant: updatedParticipant, peer: peer, peers: peers, presences: presences) + return RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(peer), peers: peers, presences: presences) } |> castError(JoinChannelError.self) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationExceptionsList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationExceptionsList.swift index 022d4a046e..23b6e63be8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationExceptionsList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/NotificationExceptionsList.swift @@ -5,14 +5,14 @@ import TelegramApi public final class NotificationExceptionsList: Equatable { - public let peers: [PeerId: Peer] + public let peers: [PeerId: EnginePeer] public let settings: [PeerId: TelegramPeerNotificationSettings] - - public init(peers: [PeerId: Peer], settings: [PeerId: TelegramPeerNotificationSettings]) { + + public init(peers: [PeerId: EnginePeer], settings: [PeerId: TelegramPeerNotificationSettings]) { self.peers = peers self.settings = settings } - + public static func ==(lhs: NotificationExceptionsList, rhs: NotificationExceptionsList) -> Bool { return lhs === rhs } @@ -41,10 +41,10 @@ func _internal_notificationExceptionsList(accountPeerId: PeerId, postbox: Postbo let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: accountPeerId,peers: parsedPeers) - var peers: [PeerId: Peer] = [:] + var peers: [PeerId: EnginePeer] = [:] for id in parsedPeers.allIds { if let peer = transaction.getPeer(id) { - peers[peer.id] = peer + peers[peer.id] = EnginePeer(peer) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift index bc0b5ab1ec..dd54a5fdfa 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift @@ -558,6 +558,26 @@ public extension EnginePeer { var profileBackgroundEmojiId: Int64? { return self._asPeer().profileBackgroundEmojiId } + + var isCopyProtectionEnabled: Bool { + return self._asPeer().isCopyProtectionEnabled + } + + var isMonoForum: Bool { + return self._asPeer().isMonoForum + } + + var associatedPeerId: Id? { + return self._asPeer().associatedPeerId + } + + var hasCustomNameColor: Bool { + return self._asPeer().hasCustomNameColor + } + + func hasSensitiveContent(platform: String) -> Bool { + return self._asPeer().hasSensitiveContent(platform: platform) + } } public extension EnginePeer { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift index f29b60c549..fc494c5ed8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift @@ -248,18 +248,18 @@ func _internal_updateChannelAdminRights(account: Account, peerId: PeerId, adminI return cachedData } }) - var peers: [PeerId: Peer] = [:] + var peers: [EnginePeer.Id: EnginePeer] = [:] var presences: [PeerId: PeerPresence] = [:] - peers[adminPeer.id] = adminPeer + peers[adminPeer.id] = EnginePeer(adminPeer) if let presence = transaction.getPeerPresence(peerId: adminPeer.id) { presences[adminPeer.id] = presence } if case let .member(_, _, maybeAdminInfo, _, _, _) = updatedParticipant, let adminInfo = maybeAdminInfo { if let peer = transaction.getPeer(adminInfo.promotedBy) { - peers[peer.id] = peer + peers[peer.id] = EnginePeer(peer) } } - return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: adminPeer, peers: peers, presences: presences)) + return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(adminPeer), peers: peers, presences: presences)) } |> mapError { _ -> UpdateChannelAdminRightsError in } } } else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift index 37bb72ce2b..93f86df0a9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift @@ -57,15 +57,15 @@ func _internal_updateChatRank(account: Account, peerId: PeerId, userId: PeerId, return nil } else { let updatedParticipant = currentParticipant?.withUpdated(rank: rank) ?? .member(id: userId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: rank, subscriptionUntilDate: nil) - var peers: [PeerId: Peer] = [:] + var peers: [EnginePeer.Id: EnginePeer] = [:] var presences: [PeerId: PeerPresence] = [:] - peers[user.id] = user + peers[user.id] = EnginePeer(user) if let presence = transaction.getPeerPresence(peerId: user.id) { presences[user.id] = presence } if case let .member(_, _, maybeAdminInfo, _, _, _) = updatedParticipant, let adminInfo = maybeAdminInfo { if let peer = transaction.getPeer(adminInfo.promotedBy) { - peers[peer.id] = peer + peers[peer.id] = EnginePeer(peer) } } let historyView = transaction.getMessagesHistoryViewState(input: .single(peerId: peerId, threadId: nil), ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set(), count: 50, clipHoles: true, anchor: .upperBound, namespaces: .just(Set([Namespaces.Message.Cloud]))) @@ -92,7 +92,7 @@ func _internal_updateChatRank(account: Account, peerId: PeerId, userId: PeerId, return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) }) } - return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: user, peers: peers, presences: presences)) + return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(user), peers: peers, presences: presences)) } } |> castError(UpdateChatRankError.self) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift index c4de6ebe66..fe9e694a9f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift @@ -151,7 +151,10 @@ func _internal_updateRecentPeersEnabled(postbox: Postbox, network: Network, enab } func _internal_managedRecentlyUsedInlineBots(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal { - let remotePeers = network.request(Api.functions.contacts.getTopPeers(flags: 1 << 2, offset: 0, limit: 16, hash: 0)) + var flags: Int32 = 0 + flags |= 1 << 2 + flags |= 1 << 17 + let remotePeers = network.request(Api.functions.contacts.getTopPeers(flags: flags, offset: 0, limit: 24, hash: 0)) |> retryRequestIfNotFrozen |> map { result -> (AccumulatedPeers, [(PeerId, Double)])? in switch result { @@ -208,17 +211,21 @@ func _internal_managedRecentlyUsedInlineBots(postbox: Postbox, network: Network, return updatedRemotePeers } +func _internal_addRecentlyUsedInlineBot(transaction: Transaction, peerId: PeerId) { + var maxRating = 1.0 + for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots) { + if let contents = entry.contents.get(RecentPeerItem.self) { + maxRating = max(maxRating, contents.rating) + } + } + if let entry = CodableEntry(RecentPeerItem(rating: maxRating)) { + transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots, item: OrderedItemListEntry(id: RecentPeerItemId(peerId).rawValue, contents: entry), removeTailIfCountExceeds: 20) + } +} + func _internal_addRecentlyUsedInlineBot(postbox: Postbox, peerId: PeerId) -> Signal { return postbox.transaction { transaction -> Void in - var maxRating = 1.0 - for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots) { - if let contents = entry.contents.get(RecentPeerItem.self) { - maxRating = max(maxRating, contents.rating) - } - } - if let entry = CodableEntry(RecentPeerItem(rating: maxRating)) { - transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots, item: OrderedItemListEntry(id: RecentPeerItemId(peerId).rawValue, contents: entry), removeTailIfCountExceeds: 20) - } + _internal_addRecentlyUsedInlineBot(transaction: transaction, peerId: peerId) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift index 54ede971c6..ba2969cd40 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift @@ -5,7 +5,7 @@ import TelegramApi import MtProtoKit -func _internal_reportPeer(account: Account, peerId: PeerId) -> Signal { +func _internal_reportPeer(account: Account, peerId: PeerId, sourceMessageId: MessageId? = nil) -> Signal { return account.postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId) { if let peer = peer as? TelegramSecretChat { @@ -37,7 +37,7 @@ func _internal_reportPeer(account: Account, peerId: PeerId) -> Signal map(Optional.init) |> `catch` { _ -> Signal in @@ -115,9 +115,9 @@ private extension ReportReason { } } -func _internal_reportPeer(account: Account, peerId: PeerId, reason: ReportReason, message: String) -> Signal { +func _internal_reportPeer(account: Account, peerId: PeerId, reason: ReportReason, message: String, sourceMessageId: MessageId? = nil) -> Signal { return account.postbox.transaction { transaction -> Signal in - if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { + if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer, sourceMessageId: sourceMessageId, transaction: transaction) { return account.network.request(Api.functions.account.reportPeer(peer: inputPeer, reason: reason.apiReason, message: message)) |> `catch` { _ -> Signal in return .single(.boolFalse) @@ -131,9 +131,9 @@ func _internal_reportPeer(account: Account, peerId: PeerId, reason: ReportReason } |> switchToLatest } -func _internal_reportPeerPhoto(account: Account, peerId: PeerId, reason: ReportReason, message: String) -> Signal { +func _internal_reportPeerPhoto(account: Account, peerId: PeerId, reason: ReportReason, message: String, sourceMessageId: MessageId? = nil) -> Signal { return account.postbox.transaction { transaction -> Signal in - if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { + if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer, sourceMessageId: sourceMessageId, transaction: transaction) { return account.network.request(Api.functions.account.reportProfilePhoto(peer: inputPeer, photoId: .inputPhotoEmpty, reason: reason.apiReason, message: message)) |> `catch` { _ -> Signal in return .single(.boolFalse) @@ -193,7 +193,7 @@ func _internal_reportPeerReaction(account: Account, authorId: PeerId, messageId: guard let peer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { return nil } - guard let author = transaction.getPeer(authorId).flatMap(apiInputPeer) else { + guard let authorPeer = transaction.getPeer(authorId), let author = apiInputPeer(authorPeer, sourceMessageId: messageId, transaction: transaction) else { return nil } return (peer, author) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SavedMusic.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SavedMusic.swift index c1d6b785d7..0bededb960 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SavedMusic.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SavedMusic.swift @@ -8,7 +8,7 @@ public enum AddSavedMusicError { case generic } -func revalidatedMusic(account: Account, file: FileMediaReference, signal: @escaping (CloudDocumentMediaResource) -> Signal) -> Signal { +public func revalidatedMusic(account: Account, file: FileMediaReference, signal: @escaping (CloudDocumentMediaResource) -> Signal) -> Signal { guard let resource = file.media.resource as? CloudDocumentMediaResource else { return .fail(MTRpcError(errorCode: 500, errorDescription: "Internal")) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchGroupMembers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchGroupMembers.swift index 5fbef54318..574bfb90db 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchGroupMembers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchGroupMembers.swift @@ -80,7 +80,7 @@ func _internal_searchGroupMembers(postbox: Postbox, network: Network, accountPee let existingIds = Set(local.map { $0.id }) let filtered: [Peer] if let participants = participants { - filtered = participants.map({ $0.peer }).filter({ peer in + filtered = participants.map({ $0.peer._asPeer() }).filter({ peer in if existingIds.contains(peer.id) { return false } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift index fbbb62a2c8..e82a0a009c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/SearchPeers.swift @@ -5,16 +5,16 @@ import TelegramApi import MtProtoKit public struct FoundPeer: Equatable { - public let peer: Peer + public let peer: EnginePeer public let subscribers: Int32? - - public init(peer: Peer, subscribers: Int32?) { + + public init(peer: EnginePeer, subscribers: Int32?) { self.peer = peer self.subscribers = subscribers } - + public static func ==(lhs: FoundPeer, rhs: FoundPeer) -> Bool { - return lhs.peer.isEqual(rhs.peer) && lhs.subscribers == rhs.subscribers + return lhs.peer == rhs.peer && lhs.subscribers == rhs.subscribers } } @@ -67,9 +67,9 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo continue } if let user = peer as? TelegramUser { - renderedMyPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount)) + renderedMyPeers.append(FoundPeer(peer: EnginePeer(peer), subscribers: user.subscriberCount)) } else { - renderedMyPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + renderedMyPeers.append(FoundPeer(peer: EnginePeer(peer), subscribers: subscribers[peerId])) } } } @@ -82,9 +82,9 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo continue } if let user = peer as? TelegramUser { - renderedPeers.append(FoundPeer(peer: peer, subscribers: user.subscriberCount)) + renderedPeers.append(FoundPeer(peer: EnginePeer(peer), subscribers: user.subscriberCount)) } else { - renderedPeers.append(FoundPeer(peer: peer, subscribers: subscribers[peerId])) + renderedPeers.append(FoundPeer(peer: EnginePeer(peer), subscribers: subscribers[peerId])) } } } @@ -94,14 +94,14 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo break case .channels: renderedMyPeers = renderedMyPeers.filter { item in - if let channel = item.peer as? TelegramChannel, case .broadcast = channel.info { + if case let .channel(channel) = item.peer, case .broadcast = channel.info { return true } else { return false } } renderedPeers = renderedPeers.filter { item in - if let channel = item.peer as? TelegramChannel, case .broadcast = channel.info { + if case let .channel(channel) = item.peer, case .broadcast = channel.info { return true } else { return false @@ -109,18 +109,18 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo } case .groups: renderedMyPeers = renderedMyPeers.filter { item in - if let channel = item.peer as? TelegramChannel, case .group = channel.info { + if case let .channel(channel) = item.peer, case .group = channel.info { return true - } else if item.peer is TelegramGroup { + } else if case .legacyGroup = item.peer { return true } else { return false } } renderedPeers = renderedPeers.filter { item in - if let channel = item.peer as? TelegramChannel, case .group = channel.info { + if case let .channel(channel) = item.peer, case .group = channel.info { return true - } else if item.peer is TelegramGroup { + } else if case .legacyGroup = item.peer { return true } else { return false @@ -128,14 +128,14 @@ public func _internal_searchPeers(accountPeerId: PeerId, postbox: Postbox, netwo } case .privateChats: renderedMyPeers = renderedMyPeers.filter { item in - if item.peer is TelegramUser { + if case .user = item.peer { return true } else { return false } } renderedPeers = renderedPeers.filter { item in - if item.peer is TelegramUser { + if case .user = item.peer { return true } else { return false diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 8a1236f933..600175ea17 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -251,8 +251,9 @@ public extension TelegramEngine { } } - public func updatedRemotePeer(peer: PeerReference) -> Signal { + public func updatedRemotePeer(peer: PeerReference) -> Signal { return _internal_updatedRemotePeer(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, peer: peer) + |> map(EnginePeer.init) } public func chatOnlineMembers(peerId: PeerId) -> Signal { @@ -335,16 +336,16 @@ public extension TelegramEngine { return _internal_updateChannelSlowModeInteractively(postbox: self.account.postbox, network: self.account.network, accountStateManager: self.account.stateManager, peerId: peerId, timeout: timeout) } - public func reportPeer(peerId: PeerId) -> Signal { - return _internal_reportPeer(account: self.account, peerId: peerId) + public func reportPeer(peerId: PeerId, sourceMessageId: MessageId? = nil) -> Signal { + return _internal_reportPeer(account: self.account, peerId: peerId, sourceMessageId: sourceMessageId) } - public func reportPeer(peerId: PeerId, reason: ReportReason, message: String) -> Signal { - return _internal_reportPeer(account: self.account, peerId: peerId, reason: reason, message: message) + public func reportPeer(peerId: PeerId, reason: ReportReason, message: String, sourceMessageId: MessageId? = nil) -> Signal { + return _internal_reportPeer(account: self.account, peerId: peerId, reason: reason, message: message, sourceMessageId: sourceMessageId) } - public func reportPeerPhoto(peerId: PeerId, reason: ReportReason, message: String) -> Signal { - return _internal_reportPeerPhoto(account: self.account, peerId: peerId, reason: reason, message: message) + public func reportPeerPhoto(peerId: PeerId, reason: ReportReason, message: String, sourceMessageId: MessageId? = nil) -> Signal { + return _internal_reportPeerPhoto(account: self.account, peerId: peerId, reason: reason, message: message, sourceMessageId: sourceMessageId) } public func reportPeerMessages(messageIds: [MessageId], reason: ReportReason, message: String) -> Signal { @@ -701,16 +702,18 @@ public extension TelegramEngine { return _internal_removeRecentlyUsedApp(account: self.account, peerId: peerId) } - public func uploadedPeerPhoto(resource: MediaResource) -> Signal { - return _internal_uploadedPeerPhoto(postbox: self.account.postbox, network: self.account.network, resource: resource) + public func uploadedPeerPhoto(resource: EngineMediaResource) -> Signal { + return _internal_uploadedPeerPhoto(postbox: self.account.postbox, network: self.account.network, resource: resource._asResource()) } - public func uploadedPeerVideo(resource: MediaResource) -> Signal { - return _internal_uploadedPeerVideo(postbox: self.account.postbox, network: self.account.network, messageMediaPreuploadManager: self.account.messageMediaPreuploadManager, resource: resource) + public func uploadedPeerVideo(resource: EngineMediaResource) -> Signal { + return _internal_uploadedPeerVideo(postbox: self.account.postbox, network: self.account.network, messageMediaPreuploadManager: self.account.messageMediaPreuploadManager, resource: resource._asResource()) } - public func updatePeerPhoto(peerId: PeerId, photo: Signal?, video: Signal? = nil, videoStartTimestamp: Double? = nil, markup: UploadPeerPhotoMarkup? = nil, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { - return _internal_updatePeerPhoto(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, accountPeerId: self.account.peerId, peerId: peerId, photo: photo, video: video, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: mapResourceToAvatarSizes) + public func updatePeerPhoto(peerId: PeerId, photo: Signal?, video: Signal? = nil, videoStartTimestamp: Double? = nil, markup: UploadPeerPhotoMarkup? = nil, mapResourceToAvatarSizes: @escaping (EngineMediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal { + return _internal_updatePeerPhoto(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, accountPeerId: self.account.peerId, peerId: peerId, photo: photo, video: video, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { rawResource, representations in + return mapResourceToAvatarSizes(EngineMediaResource(rawResource), representations) + }) } public func requestUpdateChatListFilter(id: Int32, filter: ChatListFilter?) -> Signal { @@ -820,8 +823,8 @@ public extension TelegramEngine { } } - public func fetchAndUpdateCachedPeerData(peerId: PeerId) -> Signal { - return _internal_fetchAndUpdateCachedPeerData(accountPeerId: self.account.peerId, peerId: peerId, network: self.account.network, postbox: self.account.postbox) + public func fetchAndUpdateCachedPeerData(peerId: PeerId, sourceMessageId: EngineMessage.Id? = nil) -> Signal { + return _internal_fetchAndUpdateCachedPeerData(accountPeerId: self.account.peerId, peerId: peerId, sourceMessageId: sourceMessageId, network: self.account.network, postbox: self.account.postbox) } public func toggleItemPinned(location: TogglePeerChatPinnedLocation, itemId: PinnedItemId) -> Signal { @@ -1230,14 +1233,11 @@ public extension TelegramEngine { } public func ensurePeerIsLocallyAvailable(peer: EnginePeer) -> Signal { - return _internal_storedMessageFromSearchPeer(postbox: self.account.postbox, peer: peer._asPeer()) - |> map { result -> EnginePeer in - return EnginePeer(result) - } + return _internal_storedMessageFromSearchPeer(postbox: self.account.postbox, peer: peer) } public func ensurePeersAreLocallyAvailable(peers: [EnginePeer]) -> Signal { - return _internal_storedMessageFromSearchPeers(account: self.account, peers: peers.map { $0._asPeer() }) + return _internal_storedMessageFromSearchPeers(account: self.account, peers: peers) } public func mostRecentSecretChat(id: EnginePeer.Id) -> Signal { @@ -1591,7 +1591,7 @@ public extension TelegramEngine { } public func subscribeIsPremiumRequiredForMessaging(id: EnginePeer.Id) -> Signal { - if id.namespace != Namespaces.Peer.CloudUser { + if id == self.account.peerId || id.namespace != Namespaces.Peer.CloudUser { return .single(false) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 5f1a6d6d8a..41dede4909 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -140,7 +140,7 @@ func fetchAndUpdateSupplementalCachedPeerData(peerId rawPeerId: PeerId, accountP } } -func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerId, network: Network, postbox: Postbox) -> Signal { +func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPeerId: PeerId, sourceMessageId: EngineMessage.Id? = nil, network: Network, postbox: Postbox) -> Signal { return postbox.combinedView(keys: [.basicPeer(rawPeerId)]) |> mapToSignal { views -> Signal in if accountPeerId == rawPeerId { @@ -178,7 +178,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee if rawPeerId == accountPeerId { return (.inputUserSelf, rawPeer, rawPeerId) } else { - return (apiInputUser(peer), peer, peer.id) + return (apiInputUser(peer, sourceMessageId: sourceMessageId, transaction: transaction), peer, peer.id) } } |> mapToSignal { inputUser, maybePeer, peerId -> Signal in @@ -302,7 +302,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let unofficialSecurityRisk = (userFullFlags2 & (1 << 26)) != 0 var flags: CachedUserFlags = previous.flags - if premiumRequired { + if premiumRequired && peerId != accountPeerId { flags.insert(.premiumRequired) } else { flags.remove(.premiumRequired) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeers.swift index 1a300fe33e..a724c1ca7f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/BlockedPeers.swift @@ -4,9 +4,9 @@ import SwiftSignalKit import TelegramApi import MtProtoKit -func _internal_requestUpdatePeerIsBlocked(account: Account, peerId: PeerId, isBlocked: Bool) -> Signal { +func _internal_requestUpdatePeerIsBlocked(account: Account, peerId: PeerId, isBlocked: Bool, sourceMessageId: MessageId? = nil) -> Signal { return account.postbox.transaction { transaction -> Signal in - if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { + if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer, sourceMessageId: sourceMessageId, transaction: transaction) { let signal: Signal if isBlocked { signal = account.network.request(Api.functions.contacts.block(flags: 0, id: inputPeer)) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift index 05061edcee..1a4c8ce215 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift @@ -9,8 +9,8 @@ public extension TelegramEngine { self.account = account } - public func requestUpdatePeerIsBlocked(peerId: PeerId, isBlocked: Bool) -> Signal { - return _internal_requestUpdatePeerIsBlocked(account: self.account, peerId: peerId, isBlocked: isBlocked) + public func requestUpdatePeerIsBlocked(peerId: PeerId, isBlocked: Bool, sourceMessageId: MessageId? = nil) -> Signal { + return _internal_requestUpdatePeerIsBlocked(account: self.account, peerId: peerId, isBlocked: isBlocked, sourceMessageId: sourceMessageId) } public func requestUpdatePeerIsBlockedFromStories(peerId: PeerId, isBlocked: Bool) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift index d0758efd41..18a0f80ff0 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift @@ -2,6 +2,7 @@ import Foundation import SwiftSignalKit import Postbox import TelegramApi +import RangeSet public enum MediaResourceUserContentType: UInt8, Equatable { case other = 0 @@ -253,20 +254,24 @@ public extension TelegramEngine { return _internal_collectStorageUsageStats(account: self.account) } - public func renderStorageUsageStatsMessages(stats: StorageUsageStats, categories: [StorageUsageStats.CategoryKey], existingMessages: [EngineMessage.Id: Message]) -> Signal<[EngineMessage.Id: Message], NoError> { - return _internal_renderStorageUsageStatsMessages(account: self.account, stats: stats, categories: categories, existingMessages: existingMessages) + public func renderStorageUsageStatsMessages(stats: StorageUsageStats, categories: [StorageUsageStats.CategoryKey], existingMessages: [EngineMessage.Id: EngineMessage]) -> Signal<[EngineMessage.Id: EngineMessage], NoError> { + let rawExisting = existingMessages.mapValues { $0._asMessage() } + return _internal_renderStorageUsageStatsMessages(account: self.account, stats: stats, categories: categories, existingMessages: rawExisting) + |> map { rawResult -> [EngineMessage.Id: EngineMessage] in + return rawResult.mapValues(EngineMessage.init) + } } - - public func clearStorage(peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey], includeMessages: [Message], excludeMessages: [Message]) -> Signal { - return _internal_clearStorage(account: self.account, peerId: peerId, categories: categories, includeMessages: includeMessages, excludeMessages: excludeMessages) + + public func clearStorage(peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey], includeMessages: [EngineMessage], excludeMessages: [EngineMessage]) -> Signal { + return _internal_clearStorage(account: self.account, peerId: peerId, categories: categories, includeMessages: includeMessages.map { $0._asMessage() }, excludeMessages: excludeMessages.map { $0._asMessage() }) } - - public func clearStorage(peerIds: Set, includeMessages: [Message], excludeMessages: [Message]) -> Signal { - _internal_clearStorage(account: self.account, peerIds: peerIds, includeMessages: includeMessages, excludeMessages: excludeMessages) + + public func clearStorage(peerIds: Set, includeMessages: [EngineMessage], excludeMessages: [EngineMessage]) -> Signal { + _internal_clearStorage(account: self.account, peerIds: peerIds, includeMessages: includeMessages.map { $0._asMessage() }, excludeMessages: excludeMessages.map { $0._asMessage() }) } - - public func clearStorage(messages: [Message]) -> Signal { - _internal_clearStorage(account: self.account, messages: messages) + + public func clearStorage(messages: [EngineMessage]) -> Signal { + _internal_clearStorage(account: self.account, messages: messages.map { $0._asMessage() }) } public func clearCachedMediaResources(mediaResourceIds: Set) -> Signal { @@ -415,5 +420,93 @@ public extension TelegramEngine { public func applicationIcons() -> Signal { return _internal_applicationIcons(account: account) } + + public func fetch( + reference: MediaResourceReference, + userLocation: MediaResourceUserLocation, + userContentType: MediaResourceUserContentType + ) -> Signal { + return fetchedMediaResource( + mediaBox: self.account.postbox.mediaBox, + userLocation: userLocation, + userContentType: userContentType, + reference: reference + ) + } + + public func status( + resource: EngineMediaResource, + approximateSynchronousValue: Bool = false + ) -> Signal { + return self.account.postbox.mediaBox.resourceStatus(resource._asResource(), approximateSynchronousValue: approximateSynchronousValue) + |> map { EngineMediaResource.FetchStatus($0) } + } + + public func status( + id: EngineMediaResource.Id, + resourceSize: Int64 + ) -> Signal { + return self.account.postbox.mediaBox.resourceStatus(MediaResourceId(id.stringRepresentation), resourceSize: resourceSize) + |> map { EngineMediaResource.FetchStatus($0) } + } + + public func data( + resource: EngineMediaResource, + pathExtension: String? = nil, + waitUntilFetchStatus: Bool = false, + incremental: Bool = false, + attemptSynchronously: Bool = false + ) -> Signal { + let option: ResourceDataRequestOption = incremental + ? .incremental(waitUntilFetchStatus: waitUntilFetchStatus) + : .complete(waitUntilFetchStatus: waitUntilFetchStatus) + return self.account.postbox.mediaBox.resourceData( + resource._asResource(), + pathExtension: pathExtension, + option: option, + attemptSynchronously: attemptSynchronously + ) + |> map { EngineMediaResource.ResourceData($0) } + } + + public func shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id) -> String { + return self.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(MediaResourceId(id.stringRepresentation)) + } + + public func completedResourcePath(id: EngineMediaResource.Id, pathExtension: String? = nil) -> String? { + return self.account.postbox.mediaBox.completedResourcePath(id: MediaResourceId(id.stringRepresentation), pathExtension: pathExtension) + } + + public func storeResourceData(id: EngineMediaResource.Id, data: Data, synchronous: Bool = false) { + self.account.postbox.mediaBox.storeResourceData(MediaResourceId(id.stringRepresentation), data: data, synchronous: synchronous) + } + + public func cancelInteractiveResourceFetch(id: EngineMediaResource.Id) { + self.account.postbox.mediaBox.cancelInteractiveResourceFetch(resourceId: MediaResourceId(id.stringRepresentation)) + } + + public func moveResourceData(id: EngineMediaResource.Id, toTempPath: String) { + self.account.postbox.mediaBox.moveResourceData(MediaResourceId(id.stringRepresentation), toTempPath: toTempPath) + } + + public func moveResourceData(from: EngineMediaResource.Id, to: EngineMediaResource.Id, synchronous: Bool = false) { + self.account.postbox.mediaBox.moveResourceData(from: MediaResourceId(from.stringRepresentation), to: MediaResourceId(to.stringRepresentation), synchronous: synchronous) + } + + public func copyResourceData(id: EngineMediaResource.Id, fromTempPath: String) { + self.account.postbox.mediaBox.copyResourceData(MediaResourceId(id.stringRepresentation), fromTempPath: fromTempPath) + } + + public func copyResourceData(from: EngineMediaResource.Id, to: EngineMediaResource.Id, synchronous: Bool = false) { + self.account.postbox.mediaBox.copyResourceData(from: MediaResourceId(from.stringRepresentation), to: MediaResourceId(to.stringRepresentation), synchronous: synchronous) + } + + public func resourceRangesStatus(resource: EngineMediaResource) -> Signal, NoError> { + return self.account.postbox.mediaBox.resourceRangesStatus(resource._asResource()) + } + + public func removeCachedResources(ids: [EngineMediaResource.Id], force: Bool = false, notify: Bool = false) -> Signal { + return self.account.postbox.mediaBox.removeCachedResources(ids.map { MediaResourceId($0.stringRepresentation) }, force: force, notify: notify) + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift index fbc091f82e..0ee5c27c2d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift @@ -87,24 +87,24 @@ func decryptedSecureIdFile(context: SecureIdAccessContext, encryptedData: Data, return unpaddedFileData } -public func uploadSecureIdFile(context: SecureIdAccessContext, postbox: Postbox, network: Network, resource: MediaResource) -> Signal { - return postbox.mediaBox.resourceData(resource) +public func uploadSecureIdFile(context: SecureIdAccessContext, engine: TelegramEngine, resource: EngineMediaResource) -> Signal { + return engine.account.postbox.mediaBox.resourceData(resource._asResource()) |> mapError { _ -> UploadSecureIdFileError in } |> mapToSignal { next -> Signal in if !next.complete { return .complete() } - + guard let data = try? Data(contentsOf: URL(fileURLWithPath: next.path)) else { return .fail(.generic) } - + guard let encryptedData = encryptedSecureIdFile(context: context, data: data) else { return .fail(.generic) } - - return multipartUpload(network: network, postbox: postbox, source: .data(encryptedData.data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) + + return multipartUpload(network: engine.account.network, postbox: engine.account.postbox, source: .data(encryptedData.data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> mapError { _ -> UploadSecureIdFileError in return .generic } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift index ca0da2cf99..9d1ae5baae 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift @@ -6,7 +6,7 @@ import MtProtoKit public enum UploadStickerStatus { case progress(Float) - case complete(CloudDocumentMediaResource, String) + case complete(EngineMediaResource, String) } public enum UploadStickerError { @@ -94,7 +94,7 @@ func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResour if let thumbnail, let previewRepresentation = file.previewRepresentations.first(where: { $0.dimensions == PixelDimensions(width: 320, height: 320) }) { account.postbox.mediaBox.copyResourceData(from: thumbnail.id, to: previewRepresentation.resource.id, synchronous: true) } - return .single(.complete(uploadedResource, file.mimeType)) + return .single(.complete(EngineMediaResource(uploadedResource), file.mimeType)) } default: break @@ -201,7 +201,7 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri } for sticker in stickers { if let resource = sticker.resource.resource as? CloudDocumentMediaResource { - uploadStickers.append(.single(.complete(resource, sticker.mimeType))) + uploadStickers.append(.single(.complete(EngineMediaResource(resource), sticker.mimeType))) } else { uploadStickers.append(_internal_uploadSticker(account: account, peer: peer, resource: sticker.resource.resource, thumbnail: sticker.thumbnailResource?.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, duration: sticker.duration, mimeType: sticker.mimeType) |> mapError { _ -> CreateStickerSetError in @@ -213,8 +213,8 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri |> mapToSignal { uploadedStickers -> Signal in var resources: [CloudDocumentMediaResource] = [] for sticker in uploadedStickers { - if case let .complete(resource, _) = sticker { - resources.append(resource) + if case let .complete(resource, _) = sticker, let rawResource = resource._asResource() as? CloudDocumentMediaResource { + resources.append(rawResource) } } if resources.count == stickers.count { @@ -368,7 +368,7 @@ private func revalidatedSticker(account: Account, sticker: FileMediaReference func _internal_addStickerToStickerSet(account: Account, packReference: StickerPackReference, sticker: ImportSticker) -> Signal { let uploadSticker: Signal if let resource = sticker.resource.resource as? CloudDocumentMediaResource { - uploadSticker = .single(.complete(resource, sticker.mimeType)) + uploadSticker = .single(.complete(EngineMediaResource(resource), sticker.mimeType)) } else { uploadSticker = account.postbox.loadedPeerWithId(account.peerId) |> castError(AddStickerToSetError.self) @@ -381,15 +381,15 @@ func _internal_addStickerToStickerSet(account: Account, packReference: StickerPa } return uploadSticker |> mapToSignal { uploadedSticker in - guard case let .complete(resource, _) = uploadedSticker else { + guard case let .complete(resource, _) = uploadedSticker, let rawResource = resource._asResource() as? CloudDocumentMediaResource else { return .complete() } - + var flags: Int32 = 0 if sticker.keywords.count > 0 { flags |= (1 << 1) } - let inputSticker: Api.InputStickerSetItem = .inputStickerSetItem(.init(flags: flags, document: .inputDocument(.init(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data()))), emoji: sticker.emojis.joined(), maskCoords: nil, keywords: sticker.keywords)) + let inputSticker: Api.InputStickerSetItem = .inputStickerSetItem(.init(flags: flags, document: .inputDocument(.init(id: rawResource.fileId, accessHash: rawResource.accessHash, fileReference: Buffer(data: rawResource.fileReference ?? Data()))), emoji: sticker.emojis.joined(), maskCoords: nil, keywords: sticker.keywords)) return account.network.request(Api.functions.stickers.addStickerToSet(stickerset: packReference.apiInputStickerSet, sticker: inputSticker)) |> `catch` { error -> Signal in if error.errorDescription == "FILE_REFERENCE_EXPIRED" { @@ -489,7 +489,7 @@ public enum ReplaceStickerError { func _internal_replaceSticker(account: Account, previousSticker: FileMediaReference, sticker: ImportSticker) -> Signal { let uploadSticker: Signal if let resource = sticker.resource.resource as? CloudDocumentMediaResource { - uploadSticker = .single(.complete(resource, sticker.mimeType)) + uploadSticker = .single(.complete(EngineMediaResource(resource), sticker.mimeType)) } else { uploadSticker = account.postbox.loadedPeerWithId(account.peerId) |> castError(ReplaceStickerError.self) @@ -502,14 +502,14 @@ func _internal_replaceSticker(account: Account, previousSticker: FileMediaRefere } return uploadSticker |> mapToSignal { uploadedSticker in - guard case let .complete(resource, _) = uploadedSticker else { + guard case let .complete(resource, _) = uploadedSticker, let rawResource = resource._asResource() as? CloudDocumentMediaResource else { return .complete() } var flags: Int32 = 0 if sticker.keywords.count > 0 { flags |= (1 << 1) } - let inputSticker: Api.InputStickerSetItem = .inputStickerSetItem(.init(flags: flags, document: .inputDocument(.init(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data()))), emoji: sticker.emojis.joined(), maskCoords: nil, keywords: sticker.keywords)) + let inputSticker: Api.InputStickerSetItem = .inputStickerSetItem(.init(flags: flags, document: .inputDocument(.init(id: rawResource.fileId, accessHash: rawResource.accessHash, fileReference: Buffer(data: rawResource.fileReference ?? Data()))), emoji: sticker.emojis.joined(), maskCoords: nil, keywords: sticker.keywords)) return revalidatedSticker(account: account, sticker: previousSticker, signal: { previousResource in return account.network.request(Api.functions.stickers.replaceSticker(sticker: .inputDocument(.init(id: previousResource.fileId, accessHash: previousResource.accessHash, fileReference: Buffer(data: previousResource.fileReference))), newSticker: inputSticker)) }) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift index a4bb852591..6f80a4270f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift @@ -58,6 +58,665 @@ public struct SearchStickersScope: OptionSet { public static let remote = SearchStickersScope(rawValue: 1 << 1) } +private struct StickerSearchContextInitialData { + let localItems: [FoundStickerItem] + let cached: CachedStickerQueryResult? + let isPremium: Bool + let searchStickersConfiguration: SearchStickersConfiguration + let cacheKey: String +} + +private func normalizedStickerSearchEmoticon(_ emoticon: [String]) -> [String] { + if emoticon == ["\u{2764}"] { + return ["\u{2764}\u{FE0F}"] + } else { + return emoticon + } +} + +private func stickerSearchCacheKey(query: String?, emoticon: [String]) -> String { + if let query, !query.isEmpty { + return query + } else { + return emoticon.sorted().joined() + } +} + +private func stickerSearchCombinedItems(localItems: [FoundStickerItem], files: [TelegramMediaFile], isPremium: Bool, searchStickersConfiguration: SearchStickersConfiguration) -> [FoundStickerItem] { + var premiumItems: [FoundStickerItem] = [] + var otherItems: [FoundStickerItem] = [] + for item in localItems { + if item.file.isPremiumSticker { + premiumItems.append(item) + } else { + otherItems.append(item) + } + } + + var foundItems: [FoundStickerItem] = [] + var foundAnimatedItems: [FoundStickerItem] = [] + var foundPremiumItems: [FoundStickerItem] = [] + + var currentItemIds = Set(localItems.map { $0.file.fileId }) + for file in files { + if currentItemIds.contains(file.fileId) { + continue + } + currentItemIds.insert(file.fileId) + + let item = FoundStickerItem(file: file, stringRepresentations: []) + if file.isPremiumSticker { + foundPremiumItems.append(item) + } else if file.isAnimatedSticker || file.isVideoSticker { + foundAnimatedItems.append(item) + } else { + foundItems.append(item) + } + } + + let allPremiumItems = premiumItems + foundPremiumItems + let allOtherItems = otherItems + foundAnimatedItems + foundItems + + var result: [FoundStickerItem] = [] + if isPremium { + let batchCount = Int(searchStickersConfiguration.normalStickersPerPremiumCount) + if batchCount == 0 { + result.append(contentsOf: allPremiumItems) + result.append(contentsOf: allOtherItems) + } else if allPremiumItems.isEmpty { + result.append(contentsOf: allOtherItems) + } else { + var i = 0 + for premiumItem in allPremiumItems { + if i < allOtherItems.count { + for j in i ..< min(i + batchCount, allOtherItems.count) { + result.append(allOtherItems[j]) + } + i += batchCount + } + result.append(premiumItem) + } + if i < allOtherItems.count { + for j in i ..< allOtherItems.count { + result.append(allOtherItems[j]) + } + } + } + } else { + result.append(contentsOf: allOtherItems) + result.append(contentsOf: allPremiumItems.prefix(max(0, Int(searchStickersConfiguration.premiumStickersCount)))) + } + return result +} + +private func stickerSearchApiFiles(_ stickers: [Api.Document]) -> [TelegramMediaFile] { + var files: [TelegramMediaFile] = [] + for sticker in stickers { + if let file = telegramMediaFileFromApiDocument(sticker, altDocuments: []), let _ = file.id { + files.append(file) + } + } + return files +} + +private func stickerSearchContextInitialData(account: Account, query: String, emoticon: [String], scope: SearchStickersScope) -> Signal { + let cacheKey = stickerSearchCacheKey(query: query, emoticon: emoticon) + + return account.postbox.transaction { transaction -> StickerSearchContextInitialData in + let isPremium = transaction.getPeer(account.peerId)?.isPremium ?? false + + var result: [FoundStickerItem] = [] + if scope.contains(.installed) { + for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudSavedStickers) { + if let item = entry.contents.get(SavedStickerItem.self) { + for representation in item.stringRepresentations { + for queryItem in emoticon { + if representation.hasPrefix(queryItem) { + result.append(FoundStickerItem(file: item.file._parse(), stringRepresentations: item.stringRepresentations)) + break + } + } + } + } + } + + var currentItems = Set(result.map { $0.file.fileId }) + var recentItems: [TelegramMediaFile] = [] + var recentAnimatedItems: [TelegramMediaFile] = [] + var recentItemsIds = Set() + var matchingRecentItemsIds = Set() + + for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentStickers) { + if let item = entry.contents.get(RecentMediaItem.self) { + let file = item.media + if file.isPremiumSticker && !isPremium { + continue + } + + if !currentItems.contains(file.fileId) { + currentItems.insert(file.fileId) + + if let displayText = file.stickerDisplayText { + for queryItem in emoticon { + if displayText.hasPrefix(queryItem) { + matchingRecentItemsIds.insert(file.fileId) + break + } + } + recentItemsIds.insert(file.fileId) + if file.isAnimatedSticker || file.isVideoSticker { + recentAnimatedItems.append(file._parse()) + } else { + recentItems.append(file._parse()) + } + break + } + } + } + } + + let searchQueries: [ItemCollectionSearchQuery] = emoticon.map { queryItem -> ItemCollectionSearchQuery in + return .exact(ValueBoxKey(queryItem)) + } + + var installedItems: [FoundStickerItem] = [] + var installedAnimatedItems: [FoundStickerItem] = [] + var installedPremiumItems: [FoundStickerItem] = [] + + for searchQuery in searchQueries { + for item in transaction.searchItemCollection(namespace: Namespaces.ItemCollection.CloudStickerPacks, query: searchQuery) { + if let item = item as? StickerPackItem { + if !currentItems.contains(item.file.fileId) { + currentItems.insert(item.file.fileId) + + let stringRepresentations = item.getStringRepresentationsOfIndexKeys() + if !recentItemsIds.contains(item.file.fileId) { + if item.file.isPremiumSticker { + installedPremiumItems.append(FoundStickerItem(file: item.file._parse(), stringRepresentations: stringRepresentations)) + } else if item.file.isAnimatedSticker || item.file.isVideoSticker { + installedAnimatedItems.append(FoundStickerItem(file: item.file._parse(), stringRepresentations: stringRepresentations)) + } else { + installedItems.append(FoundStickerItem(file: item.file._parse(), stringRepresentations: stringRepresentations)) + } + } else { + matchingRecentItemsIds.insert(item.file.fileId) + if item.file.isAnimatedSticker || item.file.isVideoSticker { + recentAnimatedItems.append(item.file._parse()) + } else { + recentItems.append(item.file._parse()) + } + } + } + } + } + } + + for file in recentAnimatedItems { + if file.isPremiumSticker && !isPremium { + continue + } + if matchingRecentItemsIds.contains(file.fileId) { + result.append(FoundStickerItem(file: file, stringRepresentations: emoticon)) + } + } + + for file in recentItems { + if file.isPremiumSticker && !isPremium { + continue + } + if matchingRecentItemsIds.contains(file.fileId) { + result.append(FoundStickerItem(file: file, stringRepresentations: emoticon)) + } + } + + result.append(contentsOf: installedPremiumItems) + result.append(contentsOf: installedAnimatedItems) + result.append(contentsOf: installedItems) + } + + var cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(cacheKey)))?.get(CachedStickerQueryResult.self) + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + let searchStickersConfiguration = SearchStickersConfiguration.with(appConfiguration: appConfiguration) + + if let currentCached = cached, currentTime > currentCached.timestamp + searchStickersConfiguration.cacheTimeout { + cached = nil + } + + return StickerSearchContextInitialData( + localItems: result, + cached: cached, + isPremium: isPremium, + searchStickersConfiguration: searchStickersConfiguration, + cacheKey: cacheKey + ) + } +} + +private func stickerSearchContextPage(account: Account, query: String, emoticon: [String], inputLanguageCode: String, scope: SearchStickersScope = [.installed, .remote], offset: Int32) -> Signal<(items: [FoundStickerItem], isFinalResult: Bool, nextOffset: Int32?), NoError> { + if scope.isEmpty { + return .single(([], true, nil)) + } + + let emoticon = normalizedStickerSearchEmoticon(emoticon) + if offset > 0 { + if !scope.contains(.remote) { + return .single(([], true, nil)) + } + + let flags: Int32 = 0 + return account.network.request(Api.functions.messages.searchStickers(flags: flags, q: query, emoticon: emoticon.joined(separator: ""), langCode: [inputLanguageCode], offset: offset, limit: 128, hash: 0)) + |> `catch` { _ -> Signal in + return .single(.foundStickersNotModified(.init(flags: 0, nextOffset: nil))) + } + |> map { result -> (items: [FoundStickerItem], isFinalResult: Bool, nextOffset: Int32?) in + switch result { + case let .foundStickers(foundStickersData): + let files = stickerSearchApiFiles(foundStickersData.stickers) + return (files.map { FoundStickerItem(file: $0, stringRepresentations: []) }, true, foundStickersData.nextOffset) + case let .foundStickersNotModified(data): + return ([], true, data.nextOffset) + } + } + } + + return stickerSearchContextInitialData(account: account, query: query, emoticon: emoticon, scope: scope) + |> mapToSignal { initialData -> Signal<(items: [FoundStickerItem], isFinalResult: Bool, nextOffset: Int32?), NoError> in + if !scope.contains(.remote) { + return .single((initialData.localItems, true, nil)) + } + + let tempResult: [FoundStickerItem] + if let cached = initialData.cached { + tempResult = stickerSearchCombinedItems(localItems: initialData.localItems, files: cached.items, isPremium: initialData.isPremium, searchStickersConfiguration: initialData.searchStickersConfiguration) + } else { + tempResult = initialData.localItems + } + + let flags: Int32 = 0 + let remote = account.network.request(Api.functions.messages.searchStickers(flags: flags, q: query, emoticon: emoticon.joined(separator: ""), langCode: [inputLanguageCode], offset: 0, limit: 128, hash: initialData.cached?.hash ?? 0)) + |> `catch` { _ -> Signal in + return .single(.foundStickersNotModified(.init(flags: 0, nextOffset: nil))) + } + |> mapToSignal { result -> Signal<(items: [FoundStickerItem], isFinalResult: Bool, nextOffset: Int32?), NoError> in + return account.postbox.transaction { transaction -> (items: [FoundStickerItem], isFinalResult: Bool, nextOffset: Int32?) in + switch result { + case let .foundStickers(foundStickersData): + let files = stickerSearchApiFiles(foundStickersData.stickers) + let resultItems = stickerSearchCombinedItems(localItems: initialData.localItems, files: files, isPremium: initialData.isPremium, searchStickersConfiguration: initialData.searchStickersConfiguration) + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if foundStickersData.hash != 0, let entry = CodableEntry(CachedStickerQueryResult(items: files, hash: foundStickersData.hash, timestamp: currentTime)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedStickerQueryResults, key: CachedStickerQueryResult.cacheKey(initialData.cacheKey)), entry: entry) + } + + return (resultItems, true, foundStickersData.nextOffset) + case let .foundStickersNotModified(data): + return (tempResult, true, data.nextOffset) + } + } + } + + return .single((tempResult, false, nil)) + |> then( + remote + |> delay(0.2, queue: Queue.concurrentDefaultQueue()) + ) + } +} + +public final class StickerSearchContext { + public struct State: Equatable { + public let items: [FoundStickerItem] + public let canLoadMore: Bool + public let isLoadingMore: Bool + + public init(items: [FoundStickerItem], canLoadMore: Bool, isLoadingMore: Bool) { + self.items = items + self.canLoadMore = canLoadMore + self.isLoadingMore = isLoadingMore + } + } + + private let queue: Queue = .mainQueue() + private let account: Account + private let query: String? + private let emoticon: [String] + private let inputLanguageCode: String + private let scope: SearchStickersScope + + private let disposable = MetaDisposable() + + private var items: [FoundStickerItem] = [] + private var nextOffset: Int32? + private var canLoadMore = false + private var isLoadingMore = false + + private let stateValue = Promise() + public var state: Signal { + return self.stateValue.get() + } + + init(account: Account, query: String?, emoticon: [String], inputLanguageCode: String, scope: SearchStickersScope) { + self.account = account + self.query = query + self.emoticon = normalizedStickerSearchEmoticon(emoticon) + self.inputLanguageCode = inputLanguageCode + self.scope = scope + + self.loadInitial() + } + + deinit { + self.disposable.dispose() + } + + public func loadMore() { + guard let query = self.query, !query.isEmpty else { + return + } + guard !self.isLoadingMore, self.canLoadMore, let nextOffset = self.nextOffset else { + return + } + + self.isLoadingMore = true + self.pushState() + + self.disposable.set((stickerSearchContextPage(account: self.account, query: query, emoticon: self.emoticon, inputLanguageCode: self.inputLanguageCode, scope: self.scope, offset: nextOffset) + |> deliverOn(self.queue)).start(next: { [weak self] result in + guard let self else { + return + } + var currentItemIds = Set(self.items.map { $0.file.fileId }) + for item in result.items { + if currentItemIds.contains(item.file.fileId) { + continue + } + currentItemIds.insert(item.file.fileId) + self.items.append(item) + } + self.nextOffset = result.nextOffset + self.canLoadMore = result.nextOffset != nil + self.isLoadingMore = false + self.pushState() + })) + } + + private func loadInitial() { + self.items = [] + self.nextOffset = nil + self.canLoadMore = false + self.isLoadingMore = true + self.pushState() + + let signal: Signal<(items: [FoundStickerItem], isFinalResult: Bool, nextOffset: Int32?), NoError> + if let query = self.query, !query.isEmpty { + signal = stickerSearchContextPage(account: self.account, query: query, emoticon: self.emoticon, inputLanguageCode: self.inputLanguageCode, scope: self.scope, offset: 0) + } else { + signal = _internal_searchStickers(account: self.account, query: self.query, emoticon: self.emoticon, inputLanguageCode: self.inputLanguageCode, scope: self.scope) + |> map { result -> (items: [FoundStickerItem], isFinalResult: Bool, nextOffset: Int32?) in + return (result.items, result.isFinalResult, nil) + } + } + + self.disposable.set((signal + |> deliverOn(self.queue)).start(next: { [weak self] result in + guard let self else { + return + } + + self.items = result.items + self.nextOffset = result.nextOffset + self.canLoadMore = result.nextOffset != nil + self.isLoadingMore = !result.isFinalResult + self.pushState() + })) + } + + private func pushState() { + self.stateValue.set(.single(State(items: self.items, canLoadMore: self.canLoadMore, isLoadingMore: self.isLoadingMore))) + } +} + +private struct EmojiSearchContextInitialData { + let localItems: [TelegramMediaFile] + let cached: CachedStickerQueryResult? + let cacheKey: String +} + +private func emojiSearchAppendUniqueItems(currentItems: [TelegramMediaFile], files: [TelegramMediaFile]) -> [TelegramMediaFile] { + var result = currentItems + var currentItemIds = Set(currentItems.map { $0.fileId }) + for file in files { + if currentItemIds.contains(file.fileId) { + continue + } + currentItemIds.insert(file.fileId) + result.append(file) + } + return result +} + +private func emojiSearchContextInitialData(account: Account, query: String, emoticon: [String], scope: SearchStickersScope) -> Signal { + let cacheKey = stickerSearchCacheKey(query: query, emoticon: emoticon) + let querySet = Set(emoticon) + + return account.postbox.transaction { transaction -> EmojiSearchContextInitialData in + var result: [TelegramMediaFile] = [] + if scope.contains(.installed) { + var currentItems = Set() + for info in transaction.getItemCollectionsInfos(namespace: Namespaces.ItemCollection.CloudEmojiPacks) { + if let info = info.1 as? StickerPackCollectionInfo { + let items = transaction.getItemCollectionItems(collectionId: info.id) + for item in items { + if let item = item as? StickerPackItem { + let file = item.file + if currentItems.contains(file.fileId) { + continue + } + + let stringRepresentations = item.getStringRepresentationsOfIndexKeys() + for stringRepresentation in stringRepresentations { + if querySet.contains(stringRepresentation) { + currentItems.insert(file.fileId) + result.append(file._parse()) + break + } + } + } + } + } + } + } + + var cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedEmojiQueryResults, key: CachedStickerQueryResult.cacheKey(cacheKey)))?.get(CachedStickerQueryResult.self) + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + let searchStickersConfiguration = SearchStickersConfiguration.with(appConfiguration: appConfiguration) + + if let currentCached = cached, currentTime > currentCached.timestamp + searchStickersConfiguration.cacheTimeout { + cached = nil + } + + return EmojiSearchContextInitialData(localItems: result, cached: cached, cacheKey: cacheKey) + } +} + +private func emojiSearchContextPage(account: Account, query: String, emoticon: [String], inputLanguageCode: String, scope: SearchStickersScope, offset: Int32) -> Signal<(items: [TelegramMediaFile], isFinalResult: Bool, nextOffset: Int32?), NoError> { + if offset != 0 { + guard scope.contains(.remote) else { + return .single(([], true, nil)) + } + + let flags: Int32 = 1 << 0 + return account.network.request(Api.functions.messages.searchStickers(flags: flags, q: query, emoticon: emoticon.joined(separator: ""), langCode: [inputLanguageCode], offset: offset, limit: 128, hash: 0)) + |> `catch` { _ -> Signal in + return .single(.foundStickersNotModified(.init(flags: 0, nextOffset: nil))) + } + |> map { result -> (items: [TelegramMediaFile], isFinalResult: Bool, nextOffset: Int32?) in + switch result { + case let .foundStickers(foundStickersData): + return (stickerSearchApiFiles(foundStickersData.stickers), true, foundStickersData.nextOffset) + case let .foundStickersNotModified(data): + return ([], true, data.nextOffset) + } + } + } + + return emojiSearchContextInitialData(account: account, query: query, emoticon: emoticon, scope: scope) + |> mapToSignal { initialData -> Signal<(items: [TelegramMediaFile], isFinalResult: Bool, nextOffset: Int32?), NoError> in + if !scope.contains(.remote) { + return .single((initialData.localItems, true, nil)) + } + + let tempResult: [TelegramMediaFile] + if let cached = initialData.cached { + tempResult = emojiSearchAppendUniqueItems(currentItems: initialData.localItems, files: cached.items) + } else { + tempResult = initialData.localItems + } + + let flags: Int32 = 1 << 0 + let remote = account.network.request(Api.functions.messages.searchStickers(flags: flags, q: query, emoticon: emoticon.joined(separator: ""), langCode: [inputLanguageCode], offset: 0, limit: 128, hash: initialData.cached?.hash ?? 0)) + |> `catch` { _ -> Signal in + return .single(.foundStickersNotModified(.init(flags: 0, nextOffset: nil))) + } + |> mapToSignal { result -> Signal<(items: [TelegramMediaFile], isFinalResult: Bool, nextOffset: Int32?), NoError> in + return account.postbox.transaction { transaction -> (items: [TelegramMediaFile], isFinalResult: Bool, nextOffset: Int32?) in + switch result { + case let .foundStickers(foundStickersData): + let files = stickerSearchApiFiles(foundStickersData.stickers) + let resultItems = emojiSearchAppendUniqueItems(currentItems: initialData.localItems, files: files) + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if let entry = CodableEntry(CachedStickerQueryResult(items: files, hash: foundStickersData.hash, timestamp: currentTime)) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedEmojiQueryResults, key: CachedStickerQueryResult.cacheKey(initialData.cacheKey)), entry: entry) + } + + return (resultItems, true, foundStickersData.nextOffset) + case let .foundStickersNotModified(data): + return (tempResult, true, data.nextOffset) + } + } + } + + return .single((tempResult, false, nil)) + |> then( + remote + |> delay(0.2, queue: Queue.concurrentDefaultQueue()) + ) + } +} + +public final class EmojiSearchContext { + public struct State: Equatable { + public let items: [TelegramMediaFile] + public let canLoadMore: Bool + public let isLoadingMore: Bool + + public init(items: [TelegramMediaFile], canLoadMore: Bool, isLoadingMore: Bool) { + self.items = items + self.canLoadMore = canLoadMore + self.isLoadingMore = isLoadingMore + } + } + + private let queue: Queue = .mainQueue() + private let account: Account + private let query: String? + private let emoticon: [String] + private let inputLanguageCode: String + private let scope: SearchStickersScope + + private let disposable = MetaDisposable() + + private var items: [TelegramMediaFile] = [] + private var nextOffset: Int32? + private var canLoadMore = false + private var isLoadingMore = false + + private let stateValue = Promise() + public var state: Signal { + return self.stateValue.get() + } + + init(account: Account, query: String?, emoticon: [String], inputLanguageCode: String, scope: SearchStickersScope) { + self.account = account + self.query = query + self.emoticon = normalizedStickerSearchEmoticon(emoticon) + self.inputLanguageCode = inputLanguageCode + self.scope = scope + + self.loadInitial() + } + + deinit { + self.disposable.dispose() + } + + public func loadMore() { + guard let query = self.query, !query.isEmpty else { + return + } + guard !self.isLoadingMore, self.canLoadMore, let nextOffset = self.nextOffset else { + return + } + + self.isLoadingMore = true + self.pushState() + + self.disposable.set((emojiSearchContextPage(account: self.account, query: query, emoticon: self.emoticon, inputLanguageCode: self.inputLanguageCode, scope: self.scope, offset: nextOffset) + |> deliverOn(self.queue)).start(next: { [weak self] result in + guard let self else { + return + } + + self.items = emojiSearchAppendUniqueItems(currentItems: self.items, files: result.items) + self.nextOffset = result.nextOffset + self.canLoadMore = result.nextOffset != nil + self.isLoadingMore = false + self.pushState() + })) + } + + private func loadInitial() { + self.items = [] + self.nextOffset = nil + self.canLoadMore = false + self.isLoadingMore = true + self.pushState() + + let signal: Signal<(items: [TelegramMediaFile], isFinalResult: Bool, nextOffset: Int32?), NoError> + if let query = self.query, !query.isEmpty { + signal = emojiSearchContextPage(account: self.account, query: query, emoticon: self.emoticon, inputLanguageCode: self.inputLanguageCode, scope: self.scope, offset: 0) + } else { + signal = _internal_searchEmoji(account: self.account, query: self.query, emoticon: self.emoticon, inputLanguageCode: self.inputLanguageCode, scope: self.scope) + |> map { result -> (items: [TelegramMediaFile], isFinalResult: Bool, nextOffset: Int32?) in + return (result.items.map(\.file), result.isFinalResult, nil) + } + } + + self.disposable.set((signal + |> deliverOn(self.queue)).start(next: { [weak self] result in + guard let self else { + return + } + + self.items = result.items + self.nextOffset = result.nextOffset + self.canLoadMore = result.nextOffset != nil + self.isLoadingMore = !result.isFinalResult + self.pushState() + })) + } + + private func pushState() { + self.stateValue.set(.single(State(items: self.items, canLoadMore: self.canLoadMore, isLoadingMore: self.isLoadingMore))) + } +} + func _internal_randomGreetingSticker(account: Account) -> Signal { let key: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudGreetingStickers) return account.postbox.combinedView(keys: [key]) @@ -85,11 +744,7 @@ func _internal_searchStickers(account: Account, query: String?, emoticon: [Strin if scope.isEmpty { return .single(([], true)) } - var emoticon = emoticon - if emoticon == ["\u{2764}"] { - emoticon = ["\u{2764}\u{FE0F}"] - } - + let emoticon = normalizedStickerSearchEmoticon(emoticon) let cacheKey: String if let query, !query.isEmpty { cacheKey = query @@ -493,9 +1148,7 @@ func _internal_searchStickers(account: Account, category: EmojiSearchCategories. } var query = category.identifiers - if query == ["\u{2764}"] { - query = ["\u{2764}\u{FE0F}"] - } + query = normalizedStickerSearchEmoticon(query) return account.postbox.transaction { transaction -> ([FoundStickerItem], CachedStickerQueryResult?, Bool, SearchStickersConfiguration) in let isPremium = transaction.getPeer(account.peerId)?.isPremium ?? false @@ -565,7 +1218,7 @@ func _internal_searchStickers(account: Account, category: EmojiSearchCategories. var searchQueries: [ItemCollectionSearchQuery] = query.map { queryItem -> ItemCollectionSearchQuery in return .exact(ValueBoxKey(queryItem)) } - if query == ["\u{2764}"] { + if query == ["\u{2764}\u{FE0F}"] { searchQueries = [.any([ValueBoxKey("\u{2764}"), ValueBoxKey("\u{2764}\u{FE0F}")])] } @@ -877,11 +1530,8 @@ func _internal_searchEmoji(account: Account, query: String?, emoticon: [String], if scope.isEmpty { return .single(([], true)) } - var emoticon = emoticon - if emoticon == ["\u{2764}"] { - emoticon = ["\u{2764}\u{FE0F}"] - } - + + let emoticon = normalizedStickerSearchEmoticon(emoticon) let cacheKey: String if let query, !query.isEmpty { cacheKey = query @@ -1235,14 +1885,3 @@ func _internal_searchGifs(account: Account, query: String, nextOffset: String = } } } - -extension TelegramMediaFile { - var stickerString: String? { - for attr in attributes { - if case let .Sticker(displayText, _, _) = attr { - return displayText - } - } - return nil - } -} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPackInteractiveOperations.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPackInteractiveOperations.swift index 359a1a6a58..f622cbc6f5 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPackInteractiveOperations.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPackInteractiveOperations.swift @@ -2,26 +2,28 @@ import Foundation import Postbox import SwiftSignalKit +public struct AddStickerPackResult { +} -func _internal_addStickerPackInteractively(postbox: Postbox, info: StickerPackCollectionInfo, items: [StickerPackItem], positionInList: Int? = nil) -> Signal { - return postbox.transaction { transaction -> Void in +func _internal_addStickerPackInteractively(postbox: Postbox, info: StickerPackCollectionInfo, items: [StickerPackItem], positionInList: Int? = nil, noDelay: Bool) -> Signal { + return postbox.transaction { transaction -> AddStickerPackResult in let namespace: SynchronizeInstalledStickerPacksOperationNamespace? switch info.id.namespace { - case Namespaces.ItemCollection.CloudStickerPacks: - namespace = .stickers - case Namespaces.ItemCollection.CloudMaskPacks: - namespace = .masks - case Namespaces.ItemCollection.CloudEmojiPacks: - namespace = .emoji - default: - namespace = nil + case Namespaces.ItemCollection.CloudStickerPacks: + namespace = .stickers + case Namespaces.ItemCollection.CloudMaskPacks: + namespace = .masks + case Namespaces.ItemCollection.CloudEmojiPacks: + namespace = .emoji + default: + namespace = nil } - if let namespace = namespace { + if let namespace { var mappedInfo = info if items.isEmpty { mappedInfo = StickerPackCollectionInfo(id: info.id, flags: info.flags, accessHash: info.accessHash, title: info.title, shortName: info.shortName, thumbnail: info.thumbnail, thumbnailFileId: info.thumbnailFileId, immediateThumbnailData: info.immediateThumbnailData, hash: Int32(bitPattern: arc4random()), count: info.count) } - addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespace, content: .add([mappedInfo.id]), noDelay: items.isEmpty) + addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: namespace, content: .add([mappedInfo.id]), noDelay: noDelay) var updatedInfos = transaction.getItemCollectionsInfos(namespace: mappedInfo.id.namespace).map { $0.1 as! StickerPackCollectionInfo } if let index = updatedInfos.firstIndex(where: { $0.id == mappedInfo.id }) { let currentInfo = updatedInfos[index] @@ -42,6 +44,10 @@ func _internal_addStickerPackInteractively(postbox: Postbox, info: StickerPackCo transaction.replaceItemCollectionItems(collectionId: mappedInfo.id, items: indexedItems) } transaction.replaceItemCollectionInfos(namespace: mappedInfo.id.namespace, itemCollectionInfos: updatedInfos.map { ($0.id, $0) }) + + return AddStickerPackResult() + } else { + return AddStickerPackResult() } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index f107a788b8..332ca02758 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -34,6 +34,10 @@ public extension TelegramEngine { return _internal_searchStickers(account: self.account, query: query, emoticon: emoticon, inputLanguageCode: inputLanguageCode, scope: scope) } + public func stickerSearchContext(query: String?, emoticon: [String], inputLanguageCode: String = "", scope: SearchStickersScope = [.installed, .remote]) -> StickerSearchContext { + return StickerSearchContext(account: self.account, query: query, emoticon: emoticon, inputLanguageCode: inputLanguageCode, scope: scope) + } + public func searchStickers(category: EmojiSearchCategories.Group, scope: SearchStickersScope = [.installed, .remote]) -> Signal<(items: [FoundStickerItem], isFinalResult: Bool), NoError> { return _internal_searchStickers(account: self.account, category: category, scope: scope) } @@ -58,8 +62,8 @@ public extension TelegramEngine { return _internal_searchGifs(account: self.account, query: query, nextOffset: nextOffset) } - public func addStickerPackInteractively(info: StickerPackCollectionInfo, items: [StickerPackItem], positionInList: Int? = nil) -> Signal { - return _internal_addStickerPackInteractively(postbox: self.account.postbox, info: info, items: items, positionInList: positionInList) + public func addStickerPackInteractively(info: StickerPackCollectionInfo, items: [StickerPackItem], positionInList: Int? = nil, noDelay: Bool = false) -> Signal { + return _internal_addStickerPackInteractively(postbox: self.account.postbox, info: info, items: items, positionInList: positionInList, noDelay: noDelay) } public func removeStickerPackInteractively(id: ItemCollectionId, option: RemoveStickerPackOption) -> Signal<(Int, [ItemCollectionItem])?, NoError> { @@ -82,8 +86,8 @@ public extension TelegramEngine { return _internal_stickerPacksAttachedToMedia(account: self.account, media: media) } - public func uploadSticker(peer: Peer, resource: MediaResource, thumbnail: MediaResource?, alt: String, dimensions: PixelDimensions, duration: Double?, mimeType: String) -> Signal { - return _internal_uploadSticker(account: self.account, peer: peer, resource: resource, thumbnail: thumbnail, alt: alt, dimensions: dimensions, duration: duration, mimeType: mimeType) + public func uploadSticker(peer: EnginePeer, resource: EngineMediaResource, thumbnail: EngineMediaResource?, alt: String, dimensions: PixelDimensions, duration: Double?, mimeType: String) -> Signal { + return _internal_uploadSticker(account: self.account, peer: peer._asPeer(), resource: resource._asResource(), thumbnail: thumbnail?._asResource(), alt: alt, dimensions: dimensions, duration: duration, mimeType: mimeType) } public func createStickerSet(title: String, shortName: String, stickers: [ImportSticker], thumbnail: ImportSticker?, type: CreateStickerSetType, software: String?) -> Signal { @@ -294,6 +298,10 @@ public extension TelegramEngine { } } + public func emojiSearchContext(query: String?, emoticon: [String], inputLanguageCode: String = "", scope: SearchStickersScope = [.installed, .remote]) -> EmojiSearchContext { + return EmojiSearchContext(account: self.account, query: query, emoticon: emoticon, inputLanguageCode: inputLanguageCode, scope: scope) + } + public func searchEmoji(category: EmojiSearchCategories.Group) -> Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError> { return _internal_searchEmoji(account: self.account, query: nil, emoticon: category.identifiers, inputLanguageCode: "") |> map { items, isFinalResult -> (items: [TelegramMediaFile], isFinalResult: Bool) in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Utils/EnginePostboxCoding.swift b/submodules/TelegramCore/Sources/TelegramEngine/Utils/EnginePostboxCoding.swift new file mode 100644 index 0000000000..2cd61b18cd --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Utils/EnginePostboxCoding.swift @@ -0,0 +1,9 @@ +import Postbox + +public typealias EngineMemoryBuffer = MemoryBuffer +public typealias EnginePostboxDecoder = PostboxDecoder +public typealias EnginePostboxEncoder = PostboxEncoder +public typealias EngineAdaptedPostboxDecoder = AdaptedPostboxDecoder +public typealias EngineItemCollectionId = ItemCollectionId +public typealias EngineFetchResourceSourceType = FetchResourceSourceType +public typealias EngineFetchResourceError = FetchResourceError diff --git a/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift b/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift index e58021f404..d1684d3f0b 100644 --- a/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift +++ b/submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift @@ -6,16 +6,37 @@ import Postbox private final class LinkHelperClass: NSObject { } -public func canSendMessagesToPeer(_ peer: Peer, ignoreDefault: Bool = false) -> Bool { - if let peer = peer as? TelegramUser, peer.addressName == "replies" { - return false - } else if peer is TelegramUser || peer is TelegramGroup { - return !peer.isDeleted - } else if let peer = peer as? TelegramSecretChat { - return peer.embeddedState == .active - } else if let peer = peer as? TelegramChannel { - return peer.hasPermission(.sendSomething, ignoreDefault: ignoreDefault) - } else { +public func canSendMessagesToPeer(_ peer: EnginePeer, ignoreDefault: Bool = false) -> Bool { + if case let .user(user) = peer, user.addressName == "replies" { return false } + switch peer { + case .user, .legacyGroup: + return !peer.isDeleted + case let .secretChat(secretChat): + return secretChat.embeddedState == .active + case let .channel(channel): + return channel.hasPermission(.sendSomething, ignoreDefault: ignoreDefault) + } +} + +public func canSendReactionsToPeer(_ peer: EnginePeer, ignoreDefault: Bool = false) -> Bool { + switch peer { + case .user: + return !peer.isDeleted + case let .legacyGroup(group): + switch group.role { + case .creator, .admin: + return !peer.isDeleted + case .member: + if let defaultBannedRights = group.defaultBannedRights, defaultBannedRights.flags.contains(.banSendReactions) && !ignoreDefault { + return false + } + return !peer.isDeleted + } + case let .secretChat(secretChat): + return secretChat.embeddedState == .active + case let .channel(channel): + return channel.hasBannedPermission(.banSendReactions, ignoreDefault: ignoreDefault) == nil + } } diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index 393234c0a0..c072041592 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -500,7 +500,17 @@ public extension Message { } return nil } + + var guestChatAttribute: GuestChatMessageAttribute? { + for attribute in self.attributes { + if let attribute = attribute as? GuestChatMessageAttribute { + return attribute + } + } + return nil + } } + public extension Message { var reactionsAttribute: ReactionsMessageAttribute? { for attribute in self.attributes { diff --git a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift index 49eb1535ee..df30e8c937 100644 --- a/submodules/TelegramCore/Sources/Utils/PeerUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/PeerUtils.swift @@ -491,6 +491,10 @@ public func peerViewMonoforumMainPeer(_ view: PeerView) -> Peer? { } public extension RenderedPeer { + convenience init(peer: EnginePeer) { + self.init(peer: peer._asPeer()) + } + convenience init(message: Message) { var peers = SimpleDictionary() let peerId = message.id.peerId diff --git a/submodules/TelegramCore/Sources/Utils/StoredMessageFromSearchPeer.swift b/submodules/TelegramCore/Sources/Utils/StoredMessageFromSearchPeer.swift index 27e0300312..08a093eecd 100644 --- a/submodules/TelegramCore/Sources/Utils/StoredMessageFromSearchPeer.swift +++ b/submodules/TelegramCore/Sources/Utils/StoredMessageFromSearchPeer.swift @@ -2,16 +2,16 @@ import Foundation import Postbox import SwiftSignalKit -public func _internal_storedMessageFromSearchPeer(postbox: Postbox, peer: Peer) -> Signal { - return postbox.transaction { transaction -> Peer in +public func _internal_storedMessageFromSearchPeer(postbox: Postbox, peer: EnginePeer) -> Signal { + return postbox.transaction { transaction -> EnginePeer in if transaction.getPeer(peer.id) == nil { - updatePeersCustom(transaction: transaction, peers: [peer], update: { _, updatedPeer in + updatePeersCustom(transaction: transaction, peers: [peer._asPeer()], update: { _, updatedPeer in return updatedPeer }) } if let group = transaction.getPeer(peer.id) as? TelegramGroup, let migrationReference = group.migrationReference { if let migrationPeer = transaction.getPeer(migrationReference.peerId) { - return migrationPeer + return EnginePeer(migrationPeer) } else { return peer } @@ -20,11 +20,11 @@ public func _internal_storedMessageFromSearchPeer(postbox: Postbox, peer: Peer) } } -func _internal_storedMessageFromSearchPeers(account: Account, peers: [Peer]) -> Signal { +func _internal_storedMessageFromSearchPeers(account: Account, peers: [EnginePeer]) -> Signal { return account.postbox.transaction { transaction -> Void in for peer in peers { if transaction.getPeer(peer.id) == nil { - updatePeersCustom(transaction: transaction, peers: [peer], update: { _, updatedPeer in + updatePeersCustom(transaction: transaction, peers: [peer._asPeer()], update: { _, updatedPeer in return updatedPeer }) } 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..4f6c9b29b1 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, accentDisabledButtonColor: rootControllerTheme.chat.inputPanel.panelControlDisabledColor, accentForegroundColor: rootControllerTheme.list.itemCheckColors.foregroundColor, style: style, glassStyle: glassStyle) } } diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift index 0fc5c68106..c67533ea1e 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkTintedPresentationTheme.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import TelegramCore import TelegramUIPreferences -import Postbox private let defaultDarkTintedAccentColor = UIColor(rgb: 0x2ea6ff) public let defaultDarkTintedPresentationTheme = makeDefaultDarkTintedPresentationTheme(preview: false) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 51173eea52..ff32a5575d 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -132,6 +132,13 @@ public enum PresentationResourceKey: Int32 { case chatListGiftIcon case chatListLocationIcon case chatListPollIcon + case chatListTodoIcon + case chatListGameIcon + case chatListCallIncomingIcon + case chatListCallOutgoingIcon + case chatListCallVideoIncomingIcon + case chatListCallVideoOutgoingIcon + case chatListVoiceMessageIcon case chatListGeneralTopicIcon case chatListGeneralTopicTemplateIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 3bb0f88571..1c9c9a99da 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1286,7 +1286,7 @@ public struct PresentationResourcesChat { UIGraphicsPopContext() } - }) + })?.withRenderingMode(.alwaysTemplate) }) } @@ -1312,7 +1312,7 @@ public struct PresentationResourcesChat { UIGraphicsPopContext() } - }) + })?.withRenderingMode(.alwaysTemplate) }) } @@ -1338,7 +1338,7 @@ public struct PresentationResourcesChat { UIGraphicsPopContext() } - }) + })?.withRenderingMode(.alwaysTemplate) }) } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift index 9a09cfb296..0994b46a48 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift @@ -207,38 +207,37 @@ public struct PresentationResourcesChatList { public static func badgeBackgroundMention(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? { return theme.image(PresentationResourceParameterKey.chatListBadgeBackgroundMention(diameter), { theme in - //return generateScaledImage(image: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeActiveBackgroundColor), size: CGSize(width: diameter, height: diameter)) - return generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeActiveBackgroundColor) + return generateScaledImage(image: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeActiveBackgroundColor), size: CGSize(width: diameter, height: diameter), opaque: false) }) } public static func badgeBackgroundInactiveMention(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? { return theme.image(PresentationResourceParameterKey.chatListBadgeBackgroundInactiveMention(diameter), { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor) + return generateScaledImage(image: generateTintedImage(image: UIImage(bundleImageName: "Chat List/MentionBadgeIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor), size: CGSize(width: diameter, height: diameter), opaque: false) }) } public static func badgeBackgroundReactions(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? { return theme.image(PresentationResourceParameterKey.badgeBackgroundReactions(diameter), { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat List/ReactionsBadgeIcon"), color: theme.chatList.reactionBadgeActiveBackgroundColor) + return generateScaledImage(image: generateTintedImage(image: UIImage(bundleImageName: "Chat List/ReactionsBadgeIcon"), color: theme.chatList.reactionBadgeActiveBackgroundColor), size: CGSize(width: diameter, height: diameter), opaque: false) }) } public static func badgeBackgroundInactiveReactions(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? { return theme.image(PresentationResourceParameterKey.badgeBackgroundInactiveReactions(diameter), { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat List/ReactionsBadgeIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor) + return generateScaledImage(image: generateTintedImage(image: UIImage(bundleImageName: "Chat List/ReactionsBadgeIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor), size: CGSize(width: diameter, height: diameter), opaque: false) }) } public static func badgeBackgroundPollVotes(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? { return theme.image(PresentationResourceParameterKey.badgeBackgroundPollVotes(diameter), { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat List/PollVotesBadgeIcon"), color: theme.chatList.unreadBadgeActiveBackgroundColor) + return generateScaledImage(image: generateTintedImage(image: UIImage(bundleImageName: "Chat List/PollVotesBadgeIcon"), color: theme.chatList.unreadBadgeActiveBackgroundColor), size: CGSize(width: diameter, height: diameter), opaque: false) }) } public static func badgeBackgroundInactivePollVotes(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? { return theme.image(PresentationResourceParameterKey.badgeBackgroundInactivePollVotes(diameter), { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat List/PollVotesBadgeIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor) + return generateScaledImage(image: generateTintedImage(image: UIImage(bundleImageName: "Chat List/PollVotesBadgeIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor), size: CGSize(width: diameter, height: diameter), opaque: false) }) } @@ -314,6 +313,48 @@ public struct PresentationResourcesChatList { }) } + public static func todoIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListTodoIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/TodoIcon"), color: theme.chatList.muteIconColor) + }) + } + + public static func gameIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListGameIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/GameIcon"), color: theme.chatList.muteIconColor) + }) + } + + public static func callIncomingIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListCallIncomingIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/CallIncomingIcon"), color: theme.chatList.muteIconColor) + }) + } + + public static func callOutgoingIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListCallOutgoingIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/CallOutgoingIcon"), color: theme.chatList.muteIconColor) + }) + } + + public static func callVideoIncomingIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListCallVideoIncomingIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/CallVideoIncomingIcon"), color: theme.chatList.muteIconColor) + }) + } + + public static func callVideoOutgoingIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListCallVideoOutgoingIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/CallVideoOutgoingIcon"), color: theme.chatList.muteIconColor) + }) + } + + public static func voiceMessageIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListVoiceMessageIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/VoiceMessageIcon"), color: theme.chatList.muteIconColor) + }) + } + public static func verifiedIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatListVerifiedIcon.rawValue, { theme in if let backgroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconForeground") { 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/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index ddf1e274ac..2cd47b82f8 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() }) } @@ -118,6 +132,10 @@ public struct PresentationResourcesSettings { public static let business = renderSettingsIcon(name: "Item List/Icons/Business", backgroundColors: [UIColor(rgb: 0xA95CE3), UIColor(rgb: 0xF16B80)]) public static let myProfile = renderSettingsIcon(name: "Item List/Icons/Profile", backgroundColors: [colorRed]) + public static let birthday = renderSettingsIcon(name: "Item List/Icons/Cake", backgroundColors: [colorBlue]) + public static let aiTools = renderSettingsIcon(name: "Item List/Icons/AITools", backgroundColors: [colorPurple]) + public static let yourColor = renderSettingsIcon(name: "Item List/Icons/Brush", backgroundColors: [colorLightBlue]) + public static let storageUsage = renderSettingsIcon(name: "Item List/Icons/Pie", backgroundColors: [colorOrange]) public static let dataUsage = renderSettingsIcon(name: "Item List/Icons/Stats", backgroundColors: [colorPurple]) @@ -156,11 +174,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 +187,41 @@ 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)) } - }) - - 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() + 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.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)) - } + 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 = renderSettingsIcon(name: "Ads/TonAbout", backgroundColors: [UIColor(rgb: 0x32ade6)]) + 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 +232,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 +277,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/BUILD b/submodules/TelegramUI/BUILD index c53cfdcae3..a33742767d 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -219,7 +219,7 @@ swift_library( "//submodules/ItemListVenueItem:ItemListVenueItem", "//submodules/SemanticStatusNode:SemanticStatusNode", "//submodules/AccountUtils:AccountUtils", - "//submodules/Svg:Svg", + "//submodules/Svg/LegacyImpl", "//submodules/ManagedAnimationNode:ManagedAnimationNode", "//submodules/TooltipUI:TooltipUI", "//submodules/ListMessageItem:ListMessageItem", @@ -283,6 +283,8 @@ swift_library( "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTAnimationCacheImpl:DCTAnimationCacheImpl", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/TelegramUI/Components/ChatInputPanelContainer:ChatInputPanelContainer", "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/TelegramUI/Components/EmojiSuggestionsComponent:EmojiSuggestionsComponent", @@ -520,6 +522,7 @@ swift_library( "//submodules/TelegramUI/Components/ChatParticipantRightsScreen", "//submodules/TelegramUI/Components/PeerInfo/PeerCopyProtectionInfoScreen", "//submodules/TelegramUI/Components/Chat/ChatRankInfoScreen", + "//submodules/TelegramUI/Components/PollStatsScreen", "//submodules/TelegramUI/Components/RankChatPreviewItem", "//submodules/TelegramUI/Components/TextProcessingScreen", "//submodules/TelegramUI/Components/CreateBotScreen", diff --git a/submodules/TelegramUI/Components/AdPanelHeaderPanelComponent/Sources/AdPanelHeaderPanelComponent.swift b/submodules/TelegramUI/Components/AdPanelHeaderPanelComponent/Sources/AdPanelHeaderPanelComponent.swift index 69991d5d3b..2463a7a93b 100644 --- a/submodules/TelegramUI/Components/AdPanelHeaderPanelComponent/Sources/AdPanelHeaderPanelComponent.swift +++ b/submodules/TelegramUI/Components/AdPanelHeaderPanelComponent/Sources/AdPanelHeaderPanelComponent.swift @@ -7,7 +7,6 @@ import ComponentDisplayAdapters import AccountContext import TelegramCore import SwiftSignalKit -import Postbox import PresentationDataUtils import ContextUI import AsyncDisplayKit diff --git a/submodules/TelegramUI/Components/AdPanelHeaderPanelComponent/Sources/ChatAdPanelNode.swift b/submodules/TelegramUI/Components/AdPanelHeaderPanelComponent/Sources/ChatAdPanelNode.swift index b6e7d92aae..a35099f90c 100644 --- a/submodules/TelegramUI/Components/AdPanelHeaderPanelComponent/Sources/ChatAdPanelNode.swift +++ b/submodules/TelegramUI/Components/AdPanelHeaderPanelComponent/Sources/ChatAdPanelNode.swift @@ -366,7 +366,7 @@ final class ChatAdPanelNode: ASDisplayNode { if fileReference.media.isAnimatedSticker { let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512) updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))) - updatedFetchMediaSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource)) + updatedFetchMediaSignal = context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media)) } else if fileReference.media.isVideo || fileReference.media.isAnimated { updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, blurred: false) } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/BUILD b/submodules/TelegramUI/Components/AdminUserActionsSheet/BUILD index ab1b9a45f7..c2e9d97ddd 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/BUILD +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/BUILD @@ -12,12 +12,12 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", "//submodules/Components/ViewControllerComponent", "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/ResizableSheetComponent", "//submodules/Components/MultilineTextComponent", "//submodules/Components/BundleIconComponent", "//submodules/TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift index dc1f55617c..f89ecc8d9f 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift @@ -227,7 +227,7 @@ final class AdminUserActionsPeerComponent: Component { let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.title, font: Font.semibold(component.baseFontSize), textColor: component.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: component.title, font: Font.medium(component.baseFontSize), textColor: component.theme.list.itemPrimaryTextColor)) )), environment: {}, containerSize: CGSize(width: maxTextSize, height: 100.0) diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift index 54d828be17..2f7a4dc717 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift @@ -1,24 +1,21 @@ import Foundation import UIKit import Display -import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import ViewControllerComponent -import ComponentDisplayAdapters import TelegramPresentationData import AccountContext import TelegramCore import MultilineTextComponent import ButtonComponent import PresentationDataUtils -import Markdown -import UndoUI -import AvatarNode -import TelegramStringFormatting import ListSectionComponent import ListActionItemComponent import PlainButtonComponent +import ResizableSheetComponent +import GlassBarButtonComponent +import BundleIconComponent struct MediaRight: OptionSet, Hashable { var rawValue: Int @@ -32,6 +29,7 @@ struct MediaRight: OptionSet, Hashable { static let videoMessages = MediaRight(rawValue: 1 << 6) static let links = MediaRight(rawValue: 1 << 7) static let polls = MediaRight(rawValue: 1 << 8) + static let reactions = MediaRight(rawValue: 1 << 9) } extension MediaRight { @@ -78,7 +76,8 @@ private func rightsFromBannedRights(_ rights: TelegramChatBannedRightsFlags) -> .voiceMessages, .videoMessages, .links, - .polls + .polls, + .reactions ] if rights.contains(.banSendText) { @@ -171,6 +170,9 @@ private func rightFlagsFromRights(participantRights: ParticipantRight, mediaRigh if !mediaRights.contains(.polls) { result.insert(.banSendPolls) } + if !mediaRights.contains(.reactions) { + result.insert(.banSendReactions) + } return result } @@ -184,9 +186,779 @@ private let allMediaRightItems: [MediaRight] = [ .voiceMessages, .videoMessages, .links, - .polls + .polls, + .reactions ] +private enum AdminUserActionOptionSection { + case report + case deleteAll + case ban +} + +private enum AdminUserDeleteAllOption { + case messages + case reactions +} + +private enum AdminUserActionConfigItem: Hashable, CaseIterable { + case sendMessages + case sendMedia + case addUsers + case pinMessages + case changeInfo +} + +private struct AdminUserActionsSheetState: Equatable { + var isOptionReportExpanded: Bool + var optionReportSelectedPeers: Set + var isOptionDeleteAllExpanded: Bool + var optionDeleteAllSelectedPeers: Set + var optionDeleteAllReactionsSelectedPeers: Set + var isOptionBanExpanded: Bool + var optionBanSelectedPeers: Set + var isConfigurationExpanded: Bool + var isMediaSectionExpanded: Bool + var allowedParticipantRights: ParticipantRight + var allowedMediaRights: MediaRight + var participantRights: ParticipantRight + var mediaRights: MediaRight +} + +private func availableAdminUserActionOptionSections( + accountPeerId: EnginePeer.Id, + chatPeer: EnginePeer, + peers: [RenderedChannelParticipant], + mode: AdminUserActionsSheet.Mode +) -> [AdminUserActionOptionSection] { + var result: [AdminUserActionOptionSection] = [.report] + + switch mode { + case .monoforum: + result.append(.ban) + case .chat, .chatReaction: + if case let .channel(channel) = chatPeer { + if channel.hasPermission(.deleteAllMessages) { + result.append(.deleteAll) + + if channel.hasPermission(.banMembers) { + var canBanEveryone = true + for peer in peers { + if peer.peer.id == accountPeerId { + canBanEveryone = false + continue + } + + switch peer.participant { + case .creator: + canBanEveryone = false + case let .member(_, _, adminInfo, _, _, _): + if let adminInfo { + if channel.flags.contains(.isCreator) { + } else if adminInfo.promotedBy == accountPeerId { + } else { + canBanEveryone = false + } + } + } + } + + if canBanEveryone { + result.append(.ban) + } + } + } + } + case .liveStream: + result.append(.deleteAll) + result.append(.ban) + } + + return result +} + +private func adminUserActionsTitle( + strings: PresentationStrings, + mode: AdminUserActionsSheet.Mode, + peers: [RenderedChannelParticipant], + selectedDeleteAllPeers: Set +) -> String { + switch mode { + case .monoforum: + if let peer = peers.first { + return strings.Monoforum_DeleteTopic_Title(peer.peer.compactDisplayTitle).string + } else { + return strings.Common_Delete + } + case let .chat(messageCount, deleteAllMessageCount, _): + if let deleteAllMessageCount, selectedDeleteAllPeers == Set(peers.map { $0.peer.id }) { + return strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount)) + } else { + return strings.Chat_AdminActionSheet_DeleteTitle(Int32(messageCount)) + } + case let .liveStream(messageCount, deleteAllMessageCount, _): + if let deleteAllMessageCount, selectedDeleteAllPeers == Set(peers.map { $0.peer.id }) { + return strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount)) + } else { + return strings.Chat_AdminActionSheet_DeleteTitle(Int32(messageCount)) + } + case .chatReaction: + return strings.Chat_AdminActionSheet_DeleteReactionTitle + } +} + +private final class AdminUserActionsContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let chatPeer: EnginePeer + let peers: [RenderedChannelParticipant] + let mode: AdminUserActionsSheet.Mode + let theme: PresentationTheme + let strings: PresentationStrings + let presentationData: PresentationData + let sheetState: AdminUserActionsSheetState + let disableOptionsSectionAnimation: Bool + let toggleOptionSelection: (AdminUserActionOptionSection) -> Void + let toggleOptionExpansion: (AdminUserActionOptionSection) -> Void + let togglePeerSelection: (AdminUserActionOptionSection, EnginePeer) -> Void + let toggleDeleteAllOptionPeerSelection: (AdminUserDeleteAllOption, EnginePeer) -> Void + let toggleConfiguration: () -> Void + let toggleConfigItem: (AdminUserActionConfigItem) -> Void + let toggleMediaSectionExpansion: () -> Void + let toggleMediaRight: (MediaRight) -> Void + + init( + context: AccountContext, + chatPeer: EnginePeer, + peers: [RenderedChannelParticipant], + mode: AdminUserActionsSheet.Mode, + theme: PresentationTheme, + strings: PresentationStrings, + presentationData: PresentationData, + sheetState: AdminUserActionsSheetState, + disableOptionsSectionAnimation: Bool, + toggleOptionSelection: @escaping (AdminUserActionOptionSection) -> Void, + toggleOptionExpansion: @escaping (AdminUserActionOptionSection) -> Void, + togglePeerSelection: @escaping (AdminUserActionOptionSection, EnginePeer) -> Void, + toggleDeleteAllOptionPeerSelection: @escaping (AdminUserDeleteAllOption, EnginePeer) -> Void, + toggleConfiguration: @escaping () -> Void, + toggleConfigItem: @escaping (AdminUserActionConfigItem) -> Void, + toggleMediaSectionExpansion: @escaping () -> Void, + toggleMediaRight: @escaping (MediaRight) -> Void + ) { + self.context = context + self.chatPeer = chatPeer + self.peers = peers + self.mode = mode + self.theme = theme + self.strings = strings + self.presentationData = presentationData + self.sheetState = sheetState + self.disableOptionsSectionAnimation = disableOptionsSectionAnimation + self.toggleOptionSelection = toggleOptionSelection + self.toggleOptionExpansion = toggleOptionExpansion + self.togglePeerSelection = togglePeerSelection + self.toggleDeleteAllOptionPeerSelection = toggleDeleteAllOptionPeerSelection + self.toggleConfiguration = toggleConfiguration + self.toggleConfigItem = toggleConfigItem + self.toggleMediaSectionExpansion = toggleMediaSectionExpansion + self.toggleMediaRight = toggleMediaRight + } + + static func ==(lhs: AdminUserActionsContentComponent, rhs: AdminUserActionsContentComponent) -> Bool { + return false + } + + final class View: UIView { + private let optionsSection = ComponentView() + private let optionsFooter = ComponentView() + private let configSection = ComponentView() + + func update(component: AdminUserActionsContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let sideInset: CGFloat = 16.0 + var contentHeight: CGFloat = 76.0 + 15.0 + + let availableOptions = availableAdminUserActionOptionSections( + accountPeerId: component.context.account.peerId, + chatPeer: component.chatPeer, + peers: component.peers, + mode: component.mode + ) + + let optionsItem: (AdminUserActionOptionSection) -> AnyComponentWithIdentity = { section in + let sectionId: AnyHashable + let selectedPeers: Set + var additionalSelectedPeers = Set() + let isExpanded: Bool + let title: String + + switch section { + case .report: + sectionId = "report" + selectedPeers = component.sheetState.optionReportSelectedPeers + isExpanded = component.sheetState.isOptionReportExpanded + title = component.strings.Chat_AdminActionSheet_ReportSpam + case .deleteAll: + sectionId = "delete-all" + selectedPeers = component.sheetState.optionDeleteAllSelectedPeers + additionalSelectedPeers = component.sheetState.optionDeleteAllReactionsSelectedPeers + isExpanded = component.sheetState.isOptionDeleteAllExpanded + if component.peers.count == 1 { + title = component.strings.Chat_AdminActionSheet_DeleteAllSingle(component.peers[0].peer.compactDisplayTitle).string + } else { + title = component.strings.Chat_AdminActionSheet_DeleteAllMultiple + } + case .ban: + sectionId = "ban" + selectedPeers = component.sheetState.optionBanSelectedPeers + isExpanded = component.sheetState.isOptionBanExpanded + + let banTitle: String + let restrictTitle: String + if component.peers.count == 1 { + banTitle = component.strings.Chat_AdminActionSheet_BanSingle(component.peers[0].peer.compactDisplayTitle).string + restrictTitle = component.strings.Chat_AdminActionSheet_RestrictSingle(component.peers[0].peer.compactDisplayTitle).string + } else { + banTitle = component.strings.Chat_AdminActionSheet_BanMultiple + restrictTitle = component.strings.Chat_AdminActionSheet_RestrictMultiple + } + title = component.sheetState.isConfigurationExpanded ? restrictTitle : banTitle + } + + var titleItems: [AnyComponentWithIdentity] = [ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: title, + font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize), + textColor: component.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ] + + var accessory: ListActionItemComponent.Accessory? + var isExpandable = false + if component.peers.count > 1 { + let selectedCount = selectedPeers.union(additionalSelectedPeers).count + accessory = .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent( + content: AnyComponent(OptionSectionExpandIndicatorComponent( + theme: component.theme, + count: selectedCount == 0 ? component.peers.count : selectedCount, + isExpanded: isExpanded + )), + effectAlignment: .center, + action: { + component.toggleOptionExpansion(section) + }, + animateScale: false + ))), + insets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 2.0), + isInteractive: true + )) + } else if case .deleteAll = section { + var count = 0 + if !selectedPeers.isEmpty { + count += 1 + } + if !additionalSelectedPeers.isEmpty { + count += 1 + } + titleItems.append( + AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent( + theme: component.theme, + title: "\(count)/2", + isExpanded: isExpanded + ))) + ) + isExpandable = true + } + + return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListActionItemComponent( + theme: component.theme, + style: .glass, + title: AnyComponent(HStack(titleItems, spacing: 7.0)), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: !selectedPeers.isEmpty || !additionalSelectedPeers.isEmpty, + toggle: { + component.toggleOptionSelection(section) + } + )), + icon: .none, + accessory: accessory, + action: { _ in + if isExpandable { + component.toggleOptionExpansion(section) + } else { + component.toggleOptionSelection(section) + } + }, + highlighting: .disabled + ))) + } + + let expandedPeersItem: (AdminUserActionOptionSection) -> AnyComponentWithIdentity = { section in + let sectionId: AnyHashable + let selectedPeers: Set + var additionalSelectedPeers = Set() + switch section { + case .report: + sectionId = "report-peers" + selectedPeers = component.sheetState.optionReportSelectedPeers + case .deleteAll: + sectionId = "delete-all-peers" + selectedPeers = component.sheetState.optionDeleteAllSelectedPeers + additionalSelectedPeers = component.sheetState.optionDeleteAllReactionsSelectedPeers + case .ban: + sectionId = "ban-peers" + selectedPeers = component.sheetState.optionBanSelectedPeers + } + + var subItems: [AnyComponentWithIdentity] = [] + if component.peers.count > 1 { + for peer in component.peers { + subItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(AdminUserActionsPeerComponent( + context: component.context, + theme: component.theme, + strings: component.strings, + baseFontSize: component.presentationData.listsFontSize.baseDisplaySize, + sideInset: 0.0, + title: peer.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), + peer: peer.peer, + selectionState: .editing(isSelected: selectedPeers.contains(peer.peer.id) || additionalSelectedPeers.contains(peer.peer.id)), + action: { peer in + component.togglePeerSelection(section, peer) + } + )))) + } + } else { + subItems.append( + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: component.theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.strings.Chat_AdminActionSheet_DeleteAllMessages, + font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize), + textColor: component.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: !selectedPeers.isEmpty, + toggle: { + component.toggleDeleteAllOptionPeerSelection(.messages, component.peers[0].peer) + } + )), + icon: .none, + accessory: nil, + action: { _ in + component.toggleDeleteAllOptionPeerSelection(.messages, component.peers[0].peer) + }, + highlighting: .disabled + ))) + ) + subItems.append( + AnyComponentWithIdentity(id: 1, component: AnyComponent(ListActionItemComponent( + theme: component.theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.strings.Chat_AdminActionSheet_DeleteAllReactions, + font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize), + textColor: component.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: !additionalSelectedPeers.isEmpty, + toggle: { + component.toggleDeleteAllOptionPeerSelection(.reactions, component.peers[0].peer) + } + )), + icon: .none, + accessory: nil, + action: { _ in + component.toggleDeleteAllOptionPeerSelection(.reactions, component.peers[0].peer) + }, + highlighting: .disabled + ))) + ) + } + + return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListSubSectionComponent( + theme: component.theme, + leftInset: 43.0, + items: subItems + ))) + } + + var optionsSectionItems: [AnyComponentWithIdentity] = [] + for option in availableOptions { + let isExpanded: Bool + switch option { + case .report: + isExpanded = component.sheetState.isOptionReportExpanded + case .deleteAll: + isExpanded = component.sheetState.isOptionDeleteAllExpanded + case .ban: + isExpanded = component.sheetState.isOptionBanExpanded + } + + optionsSectionItems.append(optionsItem(option)) + if isExpanded { + optionsSectionItems.append(expandedPeersItem(option)) + } + } + + let optionsSectionTransition: ComponentTransition = component.disableOptionsSectionAnimation ? transition.withAnimation(.none) : transition + let optionsSectionSize = self.optionsSection.update( + transition: optionsSectionTransition, + component: AnyComponent(ListSectionComponent( + theme: component.theme, + style: .glass, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.strings.Chat_AdminActionSheet_RestrictSectionHeader, + font: Font.regular(component.presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: component.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: optionsSectionItems, + isModal: true + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) + ) + let optionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: optionsSectionSize) + self.optionsSection.parentState = state + if let optionsSectionView = self.optionsSection.view { + if optionsSectionView.superview == nil { + self.addSubview(optionsSectionView) + } + transition.setFrame(view: optionsSectionView, frame: optionsSectionFrame) + } + contentHeight += optionsSectionSize.height + + let partiallyRestrictTitle: String + let fullyBanTitle: String + if component.peers.count == 1 { + partiallyRestrictTitle = component.strings.Chat_AdminActionSheet_RestrictFooterSingle + fullyBanTitle = component.strings.Chat_AdminActionSheet_BanFooterSingle + } else { + partiallyRestrictTitle = component.strings.Chat_AdminActionSheet_RestrictFooterMultiple + fullyBanTitle = component.strings.Chat_AdminActionSheet_BanFooterMultiple + } + + let optionsFooterSize = self.optionsFooter.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(OptionsSectionFooterComponent( + theme: component.theme, + text: component.sheetState.isConfigurationExpanded ? fullyBanTitle : partiallyRestrictTitle, + fontSize: component.presentationData.listsFontSize.itemListBaseHeaderFontSize, + isExpanded: component.sheetState.isConfigurationExpanded + )), + effectAlignment: .left, + contentInsets: UIEdgeInsets(), + action: { + component.toggleConfiguration() + }, + animateAlpha: true, + animateScale: false, + animateContents: true + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + + var configSectionItems: [AnyComponentWithIdentity] = [] + + if case let .channel(channel) = component.chatPeer, channel.isMonoForum { + } else if case .liveStream = component.mode { + } else { + var allConfigItems: [(AdminUserActionConfigItem, Bool)] = [] + if !component.sheetState.allowedMediaRights.isEmpty || !component.sheetState.allowedParticipantRights.isEmpty { + for configItem in AdminUserActionConfigItem.allCases { + let isEnabled: Bool + switch configItem { + case .sendMessages: + isEnabled = component.sheetState.allowedParticipantRights.contains(.sendMessages) + case .sendMedia: + isEnabled = !component.sheetState.allowedMediaRights.isEmpty + case .addUsers: + isEnabled = component.sheetState.allowedParticipantRights.contains(.addMembers) + case .pinMessages: + isEnabled = component.sheetState.allowedParticipantRights.contains(.pinMessages) + case .changeInfo: + isEnabled = component.sheetState.allowedParticipantRights.contains(.changeInfo) + } + allConfigItems.append((configItem, isEnabled)) + } + } + + for (configItem, isEnabled) in allConfigItems { + let itemTitle: AnyComponent + let itemValue: Bool + switch configItem { + case .sendMessages: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.strings.Channel_BanUser_PermissionSendMessages, + font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize), + textColor: component.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = component.sheetState.participantRights.contains(.sendMessages) + case .sendMedia: + if isEnabled { + itemTitle = AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.strings.Channel_BanUser_PermissionSendMedia, + font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize), + textColor: component.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent( + theme: component.theme, + title: "\(component.sheetState.mediaRights.count)/\(component.sheetState.allowedMediaRights.count)", + isExpanded: component.sheetState.isMediaSectionExpanded + ))) + ], spacing: 7.0)) + } else { + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.strings.Channel_BanUser_PermissionSendMedia, + font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize), + textColor: component.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + } + itemValue = !component.sheetState.mediaRights.isEmpty + case .addUsers: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.strings.Channel_BanUser_PermissionAddMembers, + font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize), + textColor: component.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = component.sheetState.participantRights.contains(.addMembers) + case .pinMessages: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.strings.Channel_EditAdmin_PermissionPinMessages, + font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize), + textColor: component.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = component.sheetState.participantRights.contains(.pinMessages) + case .changeInfo: + itemTitle = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.strings.Channel_BanUser_PermissionChangeGroupInfo, + font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize), + textColor: component.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )) + itemValue = component.sheetState.participantRights.contains(.changeInfo) + } + + configSectionItems.append(AnyComponentWithIdentity(id: configItem, component: AnyComponent(ListActionItemComponent( + theme: component.theme, + style: .glass, + title: itemTitle, + accessory: .toggle(ListActionItemComponent.Toggle( + style: isEnabled ? .icons : .lock, + isOn: itemValue, + isInteractive: isEnabled, + action: isEnabled ? { _ in + component.toggleConfigItem(configItem) + } : nil + )), + action: ((isEnabled && configItem == .sendMedia) || !isEnabled) ? { _ in + if !isEnabled { + environment.controller()?.present(textAlertController( + context: component.context, + title: nil, + text: component.strings.GroupPermission_PermissionDisabledByDefault, + actions: [ + TextAlertAction(type: .defaultAction, title: component.strings.Common_OK, action: { + }) + ] + ), in: .window(.root)) + } else { + component.toggleMediaSectionExpansion() + } + } : nil, + highlighting: .disabled + )))) + + if isEnabled, case .sendMedia = configItem, component.sheetState.isMediaSectionExpanded { + var mediaItems: [AnyComponentWithIdentity] = [] + mediaRightsLoop: for possibleMediaItem in allMediaRightItems { + if !component.sheetState.allowedMediaRights.contains(possibleMediaItem) { + continue + } + + let mediaItemTitle: String + switch possibleMediaItem { + case .photos: + mediaItemTitle = component.strings.Channel_BanUser_PermissionSendPhoto + case .videos: + mediaItemTitle = component.strings.Channel_BanUser_PermissionSendVideo + case .stickersAndGifs: + mediaItemTitle = component.strings.Channel_BanUser_PermissionSendStickersAndGifs + case .music: + mediaItemTitle = component.strings.Channel_BanUser_PermissionSendMusic + case .files: + mediaItemTitle = component.strings.Channel_BanUser_PermissionSendFile + case .voiceMessages: + mediaItemTitle = component.strings.Channel_BanUser_PermissionSendVoiceMessage + case .videoMessages: + mediaItemTitle = component.strings.Channel_BanUser_PermissionSendVideoMessage + case .links: + mediaItemTitle = component.strings.Channel_BanUser_PermissionEmbedLinks + case .polls: + mediaItemTitle = component.strings.Channel_BanUser_PermissionSendPolls + case .reactions: + mediaItemTitle = component.strings.Channel_BanUser_PermissionSendReactions + default: + continue mediaRightsLoop + } + + mediaItems.append(AnyComponentWithIdentity(id: possibleMediaItem, component: AnyComponent(ListActionItemComponent( + theme: component.theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: mediaItemTitle, + font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize), + textColor: component.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: component.sheetState.mediaRights.contains(possibleMediaItem), + toggle: { + component.toggleMediaRight(possibleMediaItem) + } + )), + icon: .none, + accessory: .none, + action: { _ in + component.toggleMediaRight(possibleMediaItem) + }, + highlighting: .disabled + )))) + } + configSectionItems.append(AnyComponentWithIdentity(id: "media-sub", component: AnyComponent(ListSubSectionComponent( + theme: component.theme, + leftInset: 0.0, + items: mediaItems + )))) + } + } + } + + let configSectionSize = self.configSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: component.theme, + style: .glass, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.peers.count == 1 ? component.strings.Chat_AdminActionSheet_PermissionsSectionHeader : component.strings.Chat_AdminActionSheet_PermissionsSectionHeaderMultiple, + font: Font.regular(component.presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: component.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: configSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) + ) + let configSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 30.0), size: configSectionSize) + self.configSection.parentState = state + if let configSectionView = self.configSection.view { + if configSectionView.superview == nil { + configSectionView.clipsToBounds = true + configSectionView.layer.cornerRadius = 11.0 + self.addSubview(configSectionView) + } + let effectiveConfigSectionFrame: CGRect + if component.sheetState.isConfigurationExpanded { + effectiveConfigSectionFrame = configSectionFrame + } else { + effectiveConfigSectionFrame = CGRect(origin: CGPoint(x: configSectionFrame.minX, y: configSectionFrame.minY - 30.0), size: CGSize(width: configSectionFrame.width, height: 0.0)) + } + transition.setFrame(view: configSectionView, frame: effectiveConfigSectionFrame) + transition.setAlpha(view: configSectionView, alpha: component.sheetState.isConfigurationExpanded ? 1.0 : 0.0) + } + + if availableOptions.contains(.ban) && !configSectionItems.isEmpty { + let optionsFooterFrame: CGRect + if component.sheetState.isConfigurationExpanded { + contentHeight += 30.0 + contentHeight += configSectionSize.height + contentHeight += 7.0 + optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) + contentHeight += optionsFooterSize.height + } else { + contentHeight += 7.0 + optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) + contentHeight += optionsFooterSize.height + } + self.optionsFooter.parentState = state + if let optionsFooterView = self.optionsFooter.view { + if optionsFooterView.superview == nil { + self.addSubview(optionsFooterView) + } + transition.setFrame(view: optionsFooterView, frame: optionsFooterFrame) + transition.setAlpha(view: optionsFooterView, alpha: 1.0) + } + } else { + self.optionsFooter.parentState = state + if let optionsFooterView = self.optionsFooter.view { + if optionsFooterView.superview == nil { + self.addSubview(optionsFooterView) + } + let optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) + transition.setFrame(view: optionsFooterView, frame: optionsFooterFrame) + transition.setAlpha(view: optionsFooterView, alpha: 0.0) + } + } + + contentHeight += 36.0 + 52.0 + 30.0 + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + private final class AdminUserActionsSheetComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -211,62 +983,21 @@ private final class AdminUserActionsSheetComponent: Component { return true } - private struct ItemLayout: Equatable { - var containerSize: CGSize - var containerInset: CGFloat - var bottomInset: CGFloat - var topInset: CGFloat - - init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { - self.containerSize = containerSize - self.containerInset = containerInset - self.bottomInset = bottomInset - self.topInset = topInset - } - } - - private final class ScrollView: UIScrollView { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return super.hitTest(point, with: event) - } - } - - final class View: UIView, UIScrollViewDelegate { - private let dimView: UIView - private let backgroundLayer: SimpleLayer - private let navigationBarContainer: SparseContainerView - private let navigationBackgroundView: BlurredBackgroundView - private let navigationBarSeparator: SimpleLayer - private let scrollView: ScrollView - private let scrollContentClippingView: SparseContainerView - private let scrollContentView: UIView - - private let leftButton = ComponentView() - - private let title = ComponentView() - private let actionButton = ComponentView() - - private let optionsSection = ComponentView() - private let optionsFooter = ComponentView() - private let configSection = ComponentView() - - private let bottomOverscrollLimit: CGFloat - - private var ignoreScrolling: Bool = false + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>() + private let animateOut = ActionSlot>() private var component: AdminUserActionsSheetComponent? private weak var state: EmptyComponentState? private var environment: ViewControllerComponentContainer.Environment? private var isUpdating: Bool = false - - private var itemLayout: ItemLayout? - - private var topOffsetDistance: CGFloat? + private var isDismissing: Bool = false private var isOptionReportExpanded: Bool = false private var optionReportSelectedPeers = Set() private var isOptionDeleteAllExpanded: Bool = false private var optionDeleteAllSelectedPeers = Set() + private var optionDeleteAllReactionsSelectedPeers = Set() private var isOptionBanExpanded: Bool = false private var optionBanSelectedPeers = Set() @@ -281,110 +1012,13 @@ private final class AdminUserActionsSheetComponent: Component { private var previousWasConfigurationExpanded: Bool = false override init(frame: CGRect) { - self.bottomOverscrollLimit = 200.0 - - self.dimView = UIView() - - self.backgroundLayer = SimpleLayer() - self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - self.backgroundLayer.cornerRadius = 10.0 - - self.navigationBarContainer = SparseContainerView() - - self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) - self.navigationBarSeparator = SimpleLayer() - - self.scrollView = ScrollView() - - self.scrollContentClippingView = SparseContainerView() - self.scrollContentClippingView.clipsToBounds = true - - self.scrollContentView = UIView() - super.init(frame: frame) - - self.addSubview(self.dimView) - self.layer.addSublayer(self.backgroundLayer) - - 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 = false - self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.alwaysBounceHorizontal = false - self.scrollView.alwaysBounceVertical = true - self.scrollView.scrollsToTop = false - self.scrollView.delegate = self - self.scrollView.clipsToBounds = true - - self.addSubview(self.scrollContentClippingView) - self.scrollContentClippingView.addSubview(self.scrollView) - - self.scrollView.addSubview(self.scrollContentView) - - self.addSubview(self.navigationBarContainer) - - self.navigationBarContainer.addSubview(self.navigationBackgroundView) - self.navigationBarContainer.layer.addSublayer(self.navigationBarSeparator) - - self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func scrollViewDidScroll(_ scrollView: UIScrollView) { - if !self.ignoreScrolling { - self.updateScrolling(transition: .immediate) - } - } - - func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - /*guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else { - return - } - - var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset - topOffset = max(0.0, topOffset) - - if topOffset < topOffsetDistance { - targetContentOffset.pointee.y = scrollView.contentOffset.y - scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true) - }*/ - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.bounds.contains(point) { - return nil - } - if !self.backgroundLayer.frame.contains(point) { - return self.dimView - } - - if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { - return result - } - - let result = super.hitTest(point, with: event) - return result - } - - @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - guard let environment = self.environment, let controller = environment.controller() else { - return - } - controller.dismiss() - } - } - private func calculateMonoforumResult() -> AdminUserActionsSheet.MonoforumResult { return AdminUserActionsSheet.MonoforumResult( ban: !self.optionBanSelectedPeers.isEmpty, @@ -395,6 +1029,7 @@ private final class AdminUserActionsSheetComponent: Component { private func calculateChatResult() -> AdminUserActionsSheet.ChatResult { var reportSpamPeers: [EnginePeer.Id] = [] var deleteAllFromPeers: [EnginePeer.Id] = [] + var deleteAllReactionsFromPeers: [EnginePeer.Id] = [] var banPeers: [EnginePeer.Id] = [] var updateBannedRights: [EnginePeer.Id: TelegramChatBannedRights] = [:] @@ -405,14 +1040,16 @@ private final class AdminUserActionsSheetComponent: Component { deleteAllFromPeers.append(id) } + for id in self.optionDeleteAllReactionsSelectedPeers.sorted() { + deleteAllReactionsFromPeers.append(id) + } + if !self.isConfigurationExpanded { for id in self.optionBanSelectedPeers.sorted() { banPeers.append(id) } } else { - var banFlags: TelegramChatBannedRightsFlags = [] - banFlags = rightFlagsFromRights(participantRights: self.participantRights, mediaRights: self.mediaRights) - + let banFlags = rightFlagsFromRights(participantRights: self.participantRights, mediaRights: self.mediaRights) let bannedRights = TelegramChatBannedRights(flags: banFlags, untilDate: Int32.max) for id in self.optionBanSelectedPeers.sorted() { updateBannedRights[id] = bannedRights @@ -422,12 +1059,13 @@ private final class AdminUserActionsSheetComponent: Component { return AdminUserActionsSheet.ChatResult( reportSpamPeers: reportSpamPeers, deleteAllFromPeers: deleteAllFromPeers, + deleteAllReactionsFromPeers: deleteAllReactionsFromPeers, banPeers: banPeers, updateBannedRights: updateBannedRights ) } - private func calculateLiveStreamResult() -> AdminUserActionsSheet.LiveStreamResult { + private func calculateLiveStreamResult() -> AdminUserActionsSheet.LiveStreamResult { return AdminUserActionsSheet.LiveStreamResult( reportSpam: !self.optionReportSelectedPeers.isEmpty, deleteAll: !self.optionDeleteAllSelectedPeers.isEmpty, @@ -435,85 +1073,16 @@ private final class AdminUserActionsSheetComponent: Component { ) } - private func updateScrolling(transition: ComponentTransition) { - guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { - return - } - var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset - - let navigationAlpha: CGFloat = 1.0 - max(0.0, min(1.0, (topOffset + 20.0) / 20.0)) - transition.setAlpha(view: self.navigationBackgroundView, alpha: navigationAlpha) - transition.setAlpha(layer: self.navigationBarSeparator, alpha: navigationAlpha) - - topOffset = max(0.0, topOffset) - transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) - - transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) - - let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) - self.topOffsetDistance = topOffsetDistance - var topOffsetFraction = topOffset / topOffsetDistance - topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) - - let transitionFactor: CGFloat = 1.0 - topOffsetFraction - if self.isUpdating { - DispatchQueue.main.async { [weak controller] in - guard let controller else { - return - } - controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) - } - } else { - controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition) - } - } - - func animateIn() { - self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - if let actionButtonView = self.actionButton.view { - actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } - } - - func animateOut(completion: @escaping () -> Void) { - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - - self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in - completion() - }) - self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - if let actionButtonView = self.actionButton.view { - actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - } - - if let environment = self.environment, let controller = environment.controller() { - controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) - } - } - func update(component: AdminUserActionsSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } - let environment = environment[ViewControllerComponentContainer.Environment.self].value - let themeUpdated = self.environment?.theme !== environment.theme - - let resetScrolling = self.scrollView.bounds.width != availableSize.width - - let sideInset: CGFloat = 16.0 + environment.safeInsets.left - if self.component == nil { let _ = (component.context.account.postbox.peerView(id: component.chatPeer.id) |> take(1)).start(next: { [weak self] peerView in - guard let self else{ + guard let self else { return } @@ -582,188 +1151,90 @@ private final class AdminUserActionsSheetComponent: Component { self.component = component self.state = state - self.environment = environment - if themeUpdated { - self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor - - self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor - } + let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environmentValue + let controller = environmentValue.controller + let theme = environmentValue.theme.withModalBlocksBackground() let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) - - var contentHeight: CGFloat = 0.0 - contentHeight += 54.0 - contentHeight += 16.0 - - let leftButtonSize = self.leftButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)), - action: { [weak self] in - guard let self, let controller = self.environment?.controller() else { - return - } - controller.dismiss() + let dismiss: (Bool) -> Void = { [weak self] animated in + guard let self, !self.isDismissing else { + return + } + self.isDismissing = true + + let performDismiss: () -> Void = { + if let controller = controller() as? AdminUserActionsSheet { + controller.completePendingDismiss() + controller.dismiss(animated: false) } - ).minSize(CGSize(width: 44.0, height: 56.0))), - environment: {}, - containerSize: CGSize(width: 120.0, height: 100.0) + } + + if animated { + self.animateOut.invoke(Action { _ in + performDismiss() + }) + } else { + performDismiss() + } + } + + let disableOptionsSectionAnimation = self.previousWasConfigurationExpanded != self.isConfigurationExpanded + self.previousWasConfigurationExpanded = self.isConfigurationExpanded + + let currentState = AdminUserActionsSheetState( + isOptionReportExpanded: self.isOptionReportExpanded, + optionReportSelectedPeers: self.optionReportSelectedPeers, + isOptionDeleteAllExpanded: self.isOptionDeleteAllExpanded, + optionDeleteAllSelectedPeers: self.optionDeleteAllSelectedPeers, + optionDeleteAllReactionsSelectedPeers: self.optionDeleteAllReactionsSelectedPeers, + isOptionBanExpanded: self.isOptionBanExpanded, + optionBanSelectedPeers: self.optionBanSelectedPeers, + isConfigurationExpanded: self.isConfigurationExpanded, + isMediaSectionExpanded: self.isMediaSectionExpanded, + allowedParticipantRights: self.allowedParticipantRights, + allowedMediaRights: self.allowedMediaRights, + participantRights: self.participantRights, + mediaRights: self.mediaRights ) - let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: 0.0), size: leftButtonSize) - if let leftButtonView = self.leftButton.view { - if leftButtonView.superview == nil { - self.navigationBarContainer.addSubview(leftButtonView) + + let performMainAction: () -> Void = { [weak self] in + guard let self, let component = self.component else { + return + } + let monoforumResult = self.calculateMonoforumResult() + let chatResult = self.calculateChatResult() + let liveStreamResult = self.calculateLiveStreamResult() + + dismiss(true) + + switch component.mode { + case let .monoforum(completion): + completion(monoforumResult) + case let .chat(_, _, completion): + completion(chatResult) + case let .liveStream(_, _, completion): + completion(liveStreamResult) + case let .chatReaction(completion): + completion(chatResult) } - transition.setFrame(view: leftButtonView, frame: leftButtonFrame) } - let containerInset: CGFloat = environment.statusBarHeight + 10.0 - - let clippingY: CGFloat - - enum OptionsSection { - case report - case deleteAll - case ban - } - - var availableOptions: [OptionsSection] = [] - availableOptions.append(.report) - - switch component.mode { - case .monoforum: - availableOptions.append(.ban) - case .chat: - if case let .channel(channel) = component.chatPeer { - if channel.hasPermission(.deleteAllMessages) { - availableOptions.append(.deleteAll) - - if channel.hasPermission(.banMembers) { - var canBanEveryone = true - for peer in component.peers { - if peer.peer.id == component.context.account.peerId { - canBanEveryone = false - continue - } - - switch peer.participant { - case .creator: - canBanEveryone = false - case let .member(_, _, adminInfo, banInfo, _, _): - let _ = banInfo - if let adminInfo { - if channel.flags.contains(.isCreator) { - } else if adminInfo.promotedBy == component.context.account.peerId { - } else { - canBanEveryone = false - } - } - } - } - - if canBanEveryone { - availableOptions.append(.ban) - } - } - } - } - case .liveStream: - availableOptions.append(.deleteAll) - availableOptions.append(.ban) - } - - let optionsItem: (OptionsSection) -> AnyComponentWithIdentity = { section in - let sectionId: AnyHashable - let selectedPeers: Set - let isExpanded: Bool - var title: String - - switch section { - case .report: - sectionId = "report" - selectedPeers = self.optionReportSelectedPeers - isExpanded = self.isOptionReportExpanded - - title = environment.strings.Chat_AdminActionSheet_ReportSpam - case .deleteAll: - sectionId = "delete-all" - selectedPeers = self.optionDeleteAllSelectedPeers - isExpanded = self.isOptionDeleteAllExpanded - - if component.peers.count == 1 { - title = environment.strings.Chat_AdminActionSheet_DeleteAllSingle(EnginePeer(component.peers[0].peer).compactDisplayTitle).string - } else { - title = environment.strings.Chat_AdminActionSheet_DeleteAllMultiple - } - case .ban: - sectionId = "ban" - selectedPeers = self.optionBanSelectedPeers - isExpanded = self.isOptionBanExpanded - - let banTitle: String - let restrictTitle: String - if component.peers.count == 1 { - banTitle = environment.strings.Chat_AdminActionSheet_BanSingle(EnginePeer(component.peers[0].peer).compactDisplayTitle).string - restrictTitle = environment.strings.Chat_AdminActionSheet_RestrictSingle(EnginePeer(component.peers[0].peer).compactDisplayTitle).string - } else { - banTitle = environment.strings.Chat_AdminActionSheet_BanMultiple - restrictTitle = environment.strings.Chat_AdminActionSheet_RestrictMultiple - } - title = self.isConfigurationExpanded ? restrictTitle : banTitle - } - - var accessory: ListActionItemComponent.Accessory? - if component.peers.count > 1 { - accessory = .custom(ListActionItemComponent.CustomAccessory( - component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent( - content: AnyComponent(OptionSectionExpandIndicatorComponent( - theme: environment.theme, - count: selectedPeers.isEmpty ? component.peers.count : selectedPeers.count, - isExpanded: isExpanded - )), - effectAlignment: .center, - action: { [weak self] in - guard let self else { - return - } - - switch section { - case .report: - self.isOptionReportExpanded = !self.isOptionReportExpanded - case .deleteAll: - self.isOptionDeleteAllExpanded = !self.isOptionDeleteAllExpanded - case .ban: - self.isOptionBanExpanded = !self.isOptionBanExpanded - } - - self.state?.updated(transition: .spring(duration: 0.35)) - }, - animateScale: false - ))), - insets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 2.0), - isInteractive: true - )) - } - - return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: title, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - ))), - ], alignment: .left, spacing: 2.0)), - leftIcon: .check(ListActionItemComponent.LeftIcon.Check( - isSelected: !selectedPeers.isEmpty, - toggle: { [weak self] in + let sheetSize = self.sheet.update( + transition: transition, + component: AnyComponent(ResizableSheetComponent( + content: AnyComponent(AdminUserActionsContentComponent( + context: component.context, + chatPeer: component.chatPeer, + peers: component.peers, + mode: component.mode, + theme: theme, + strings: environmentValue.strings, + presentationData: presentationData, + sheetState: currentState, + disableOptionsSectionAnimation: disableOptionsSectionAnimation, + toggleOptionSelection: { [weak self] section in guard let self, let component = self.component else { return } @@ -773,7 +1244,17 @@ private final class AdminUserActionsSheetComponent: Component { case .report: selectedPeers = self.optionReportSelectedPeers case .deleteAll: - selectedPeers = self.optionDeleteAllSelectedPeers + let allPeerIds = Set(component.peers.map { $0.peer.id }) + if self.optionDeleteAllSelectedPeers.isEmpty && self.optionDeleteAllReactionsSelectedPeers.isEmpty { + self.optionDeleteAllSelectedPeers = allPeerIds + self.optionDeleteAllReactionsSelectedPeers = allPeerIds + } else { + self.optionDeleteAllSelectedPeers.removeAll() + self.optionDeleteAllReactionsSelectedPeers.removeAll() + } + + self.state?.updated(transition: .spring(duration: 0.35)) + return case .ban: selectedPeers = self.optionBanSelectedPeers } @@ -799,75 +1280,24 @@ private final class AdminUserActionsSheetComponent: Component { } self.state?.updated(transition: .spring(duration: 0.35)) - } - )), - icon: .none, - accessory: accessory, - action: { [weak self] _ in - guard let self else { - return - } - - var selectedPeers: Set - switch section { - case .report: - selectedPeers = self.optionReportSelectedPeers - case .deleteAll: - selectedPeers = self.optionDeleteAllSelectedPeers - case .ban: - selectedPeers = self.optionBanSelectedPeers - } - - if selectedPeers.isEmpty { - for peer in component.peers { - selectedPeers.insert(peer.peer.id) + }, + toggleOptionExpansion: { [weak self] section in + guard let self else { + return } - } else { - selectedPeers.removeAll() - } - - switch section { - case .report: - self.optionReportSelectedPeers = selectedPeers - case .deleteAll: - self.optionDeleteAllSelectedPeers = selectedPeers - case .ban: - self.optionBanSelectedPeers = selectedPeers - } - - self.state?.updated(transition: .spring(duration: 0.35)) - }, - highlighting: .disabled - ))) - } - - let expandedPeersItem: (OptionsSection) -> AnyComponentWithIdentity = { section in - let sectionId: AnyHashable - let selectedPeers: Set - switch section { - case .report: - sectionId = "report-peers" - selectedPeers = self.optionReportSelectedPeers - case .deleteAll: - sectionId = "delete-all-peers" - selectedPeers = self.optionDeleteAllSelectedPeers - case .ban: - sectionId = "ban-peers" - selectedPeers = self.optionBanSelectedPeers - } - - var peerItems: [AnyComponentWithIdentity] = [] - for peer in component.peers { - peerItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(AdminUserActionsPeerComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - baseFontSize: presentationData.listsFontSize.baseDisplaySize, - sideInset: 0.0, - title: EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: .firstLast), - peer: EnginePeer(peer.peer), - selectionState: .editing(isSelected: selectedPeers.contains(peer.peer.id)), - action: { [weak self] peer in + + switch section { + case .report: + self.isOptionReportExpanded = !self.isOptionReportExpanded + case .deleteAll: + self.isOptionDeleteAllExpanded = !self.isOptionDeleteAllExpanded + case .ban: + self.isOptionBanExpanded = !self.isOptionBanExpanded + } + + self.state?.updated(transition: .spring(duration: 0.35)) + }, + togglePeerSelection: { [weak self] section, peer in guard let self else { return } @@ -877,7 +1307,16 @@ private final class AdminUserActionsSheetComponent: Component { case .report: selectedPeers = self.optionReportSelectedPeers case .deleteAll: - selectedPeers = self.optionDeleteAllSelectedPeers + if self.optionDeleteAllSelectedPeers.contains(peer.id) || self.optionDeleteAllReactionsSelectedPeers.contains(peer.id) { + self.optionDeleteAllSelectedPeers.remove(peer.id) + self.optionDeleteAllReactionsSelectedPeers.remove(peer.id) + } else { + self.optionDeleteAllSelectedPeers.insert(peer.id) + self.optionDeleteAllReactionsSelectedPeers.insert(peer.id) + } + + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) + return case .ban: selectedPeers = self.optionBanSelectedPeers } @@ -898,578 +1337,187 @@ private final class AdminUserActionsSheetComponent: Component { } self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) - } - )))) - } - return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListSubSectionComponent( - theme: environment.theme, - leftInset: 62.0, - items: peerItems - ))) - } - - var titleString: String - switch component.mode { - case .monoforum: - if let peer = component.peers.first { - titleString = environment.strings.Monoforum_DeleteTopic_Title(EnginePeer(peer.peer).compactDisplayTitle).string - } else { - titleString = environment.strings.Common_Delete - } - case let .chat(messageCount, deleteAllMessageCount, _): - titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(messageCount)) - if let deleteAllMessageCount { - if self.optionDeleteAllSelectedPeers == Set(component.peers.map(\.peer.id)) { - titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount)) - } - } - case let .liveStream(messageCount, deleteAllMessageCount, _): - titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(messageCount)) - if let deleteAllMessageCount { - if self.optionDeleteAllSelectedPeers == Set(component.peers.map(\.peer.id)) { - titleString = environment.strings.Chat_AdminActionSheet_DeleteTitle(Int32(deleteAllMessageCount)) - } - } - } - - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) - ) - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((54.0 - titleSize.height) * 0.5)), size: titleSize) - if let titleView = title.view { - if titleView.superview == nil { - self.navigationBarContainer.addSubview(titleView) - } - //transition.setPosition(view: titleView, position: titleFrame.center) - titleView.center = titleFrame.center - titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) - } - - let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0)) - transition.setFrame(view: self.navigationBackgroundView, frame: navigationBackgroundFrame) - self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) - transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - - var optionsSectionItems: [AnyComponentWithIdentity] = [] - - for option in availableOptions { - let isOptionExpanded: Bool - switch option { - case .report: - isOptionExpanded = self.isOptionReportExpanded - case .deleteAll: - isOptionExpanded = self.isOptionDeleteAllExpanded - case .ban: - isOptionExpanded = self.isOptionBanExpanded - } - - optionsSectionItems.append(optionsItem(option)) - if isOptionExpanded { - optionsSectionItems.append(expandedPeersItem(option)) - } - } - - var optionsSectionTransition = transition - if self.previousWasConfigurationExpanded != self.isConfigurationExpanded { - self.previousWasConfigurationExpanded = self.isConfigurationExpanded - optionsSectionTransition = optionsSectionTransition.withAnimation(.none) - } - let optionsSectionSize = self.optionsSection.update( - transition: optionsSectionTransition, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - style: .glass, - header: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Chat_AdminActionSheet_RestrictSectionHeader, - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor - )), - maximumNumberOfLines: 0 - )), - footer: nil, - items: optionsSectionItems, - isModal: true - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) - ) - - let optionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: optionsSectionSize) - if let optionsSectionView = self.optionsSection.view { - if optionsSectionView.superview == nil { - self.scrollContentView.addSubview(optionsSectionView) - self.optionsSection.parentState = state - } - transition.setFrame(view: optionsSectionView, frame: optionsSectionFrame) - } - contentHeight += optionsSectionSize.height - - let partiallyRestrictTitle: String - let fullyBanTitle: String - if component.peers.count == 1 { - partiallyRestrictTitle = environment.strings.Chat_AdminActionSheet_RestrictFooterSingle - fullyBanTitle = environment.strings.Chat_AdminActionSheet_BanFooterSingle - } else { - partiallyRestrictTitle = environment.strings.Chat_AdminActionSheet_RestrictFooterMultiple - fullyBanTitle = environment.strings.Chat_AdminActionSheet_BanFooterMultiple - } - - let optionsFooterSize = self.optionsFooter.update( - transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(OptionsSectionFooterComponent( - theme: environment.theme, - text: self.isConfigurationExpanded ? fullyBanTitle : partiallyRestrictTitle, - fontSize: presentationData.listsFontSize.itemListBaseHeaderFontSize, - isExpanded: self.isConfigurationExpanded - )), - effectAlignment: .left, - contentInsets: UIEdgeInsets(), - action: { [weak self] in - guard let self, let component = self.component else { - return - } - self.isConfigurationExpanded = !self.isConfigurationExpanded - if self.isConfigurationExpanded && self.optionBanSelectedPeers.isEmpty { - for peer in component.peers { - self.optionBanSelectedPeers.insert(peer.peer.id) + }, + toggleDeleteAllOptionPeerSelection: { [weak self] option, peer in + guard let self else { + return } - } - self.state?.updated(transition: .spring(duration: 0.35)) - }, - animateAlpha: true, - animateScale: false, - animateContents: true - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) - ) - - var configSectionItems: [AnyComponentWithIdentity] = [] - - enum ConfigItem: Hashable, CaseIterable { - case sendMessages - case sendMedia - case addUsers - case pinMessages - case changeInfo - } - - if case let .channel(channel) = component.chatPeer, channel.isMonoForum { - } else if case .liveStream = component.mode { - } else { - var allConfigItems: [(ConfigItem, Bool)] = [] - if !self.allowedMediaRights.isEmpty || !self.allowedParticipantRights.isEmpty { - for configItem in ConfigItem.allCases { - let isEnabled: Bool - switch configItem { - case .sendMessages: - isEnabled = self.allowedParticipantRights.contains(.sendMessages) - case .sendMedia: - isEnabled = !self.allowedMediaRights.isEmpty - case .addUsers: - isEnabled = self.allowedParticipantRights.contains(.addMembers) - case .pinMessages: - isEnabled = self.allowedParticipantRights.contains(.pinMessages) - case .changeInfo: - isEnabled = self.allowedParticipantRights.contains(.changeInfo) - } - allConfigItems.append((configItem, isEnabled)) - } - } - - loop: for (configItem, isEnabled) in allConfigItems { - let itemTitle: AnyComponent - let itemValue: Bool - switch configItem { - case .sendMessages: - itemTitle = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Channel_BanUser_PermissionSendMessages, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - )) - itemValue = self.participantRights.contains(.sendMessages) - case .sendMedia: - if isEnabled { - itemTitle = AnyComponent(HStack([ - AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Channel_BanUser_PermissionSendMedia, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - ))), - AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent( - theme: environment.theme, - title: "\(self.mediaRights.count)/\(self.allowedMediaRights.count)", - isExpanded: self.isMediaSectionExpanded - ))) - ], spacing: 7.0)) - } else { - itemTitle = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Channel_BanUser_PermissionSendMedia, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - )) - } - - itemValue = !self.mediaRights.isEmpty - case .addUsers: - itemTitle = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Channel_BanUser_PermissionAddMembers, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - )) - itemValue = self.participantRights.contains(.addMembers) - case .pinMessages: - itemTitle = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Channel_EditAdmin_PermissionPinMessages, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - )) - itemValue = self.participantRights.contains(.pinMessages) - case .changeInfo: - itemTitle = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Channel_BanUser_PermissionChangeGroupInfo, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - )) - itemValue = self.participantRights.contains(.changeInfo) - } - - configSectionItems.append(AnyComponentWithIdentity(id: configItem, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: itemTitle, - accessory: .toggle(ListActionItemComponent.Toggle( - style: isEnabled ? .icons : .lock, - isOn: itemValue, - isInteractive: isEnabled, - action: isEnabled ? { [weak self] _ in - guard let self else { - return + + switch option { + case .messages: + if self.optionDeleteAllSelectedPeers.contains(peer.id) { + self.optionDeleteAllSelectedPeers.remove(peer.id) + } else { + self.optionDeleteAllSelectedPeers.insert(peer.id) } - - switch configItem { - case .sendMessages: - if self.participantRights.contains(.sendMessages) { - self.participantRights.remove(.sendMessages) - } else { - self.participantRights.insert(.sendMessages) - } - case .sendMedia: - if self.mediaRights.isEmpty { - self.mediaRights = self.allowedMediaRights - } else { - self.mediaRights = [] - } - case .addUsers: - if self.participantRights.contains(.addMembers) { - self.participantRights.remove(.addMembers) - } else { - self.participantRights.insert(.addMembers) - } - case .pinMessages: - if self.participantRights.contains(.pinMessages) { - self.participantRights.remove(.pinMessages) - } else { - self.participantRights.insert(.pinMessages) - } - case .changeInfo: - if self.participantRights.contains(.changeInfo) { - self.participantRights.remove(.changeInfo) - } else { - self.participantRights.insert(.changeInfo) - } + case .reactions: + if self.optionDeleteAllReactionsSelectedPeers.contains(peer.id) { + self.optionDeleteAllReactionsSelectedPeers.remove(peer.id) + } else { + self.optionDeleteAllReactionsSelectedPeers.insert(peer.id) } - self.state?.updated(transition: .spring(duration: 0.35)) - } : nil - )), - action: ((isEnabled && configItem == .sendMedia) || !isEnabled) ? { [weak self] _ in + } + + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut))) + }, + toggleConfiguration: { [weak self] in guard let self, let component = self.component else { return } - if !isEnabled { - self.environment?.controller()?.present(textAlertController(context: component.context, title: nil, text: environment.strings.GroupPermission_PermissionDisabledByDefault, actions: [ - TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: { - }) - ]), in: .window(.root)) + self.isConfigurationExpanded = !self.isConfigurationExpanded + if self.isConfigurationExpanded && self.optionBanSelectedPeers.isEmpty { + for peer in component.peers { + self.optionBanSelectedPeers.insert(peer.peer.id) + } + } + self.state?.updated(transition: .spring(duration: 0.35)) + }, + toggleConfigItem: { [weak self] configItem in + guard let self else { + return + } + + switch configItem { + case .sendMessages: + if self.participantRights.contains(.sendMessages) { + self.participantRights.remove(.sendMessages) + } else { + self.participantRights.insert(.sendMessages) + } + case .sendMedia: + if self.mediaRights.isEmpty { + self.mediaRights = self.allowedMediaRights + } else { + self.mediaRights = [] + } + case .addUsers: + if self.participantRights.contains(.addMembers) { + self.participantRights.remove(.addMembers) + } else { + self.participantRights.insert(.addMembers) + } + case .pinMessages: + if self.participantRights.contains(.pinMessages) { + self.participantRights.remove(.pinMessages) + } else { + self.participantRights.insert(.pinMessages) + } + case .changeInfo: + if self.participantRights.contains(.changeInfo) { + self.participantRights.remove(.changeInfo) + } else { + self.participantRights.insert(.changeInfo) + } + } + self.state?.updated(transition: .spring(duration: 0.35)) + }, + toggleMediaSectionExpansion: { [weak self] in + guard let self else { + return + } + self.isMediaSectionExpanded = !self.isMediaSectionExpanded + self.state?.updated(transition: .spring(duration: 0.35)) + }, + toggleMediaRight: { [weak self] mediaRight in + guard let self else { + return + } + + if self.mediaRights.contains(mediaRight) { + self.mediaRights.remove(mediaRight) } else { - self.isMediaSectionExpanded = !self.isMediaSectionExpanded - self.state?.updated(transition: .spring(duration: 0.35)) - } - } : nil, - highlighting: .disabled - )))) - - if isEnabled, case .sendMedia = configItem, self.isMediaSectionExpanded { - var mediaItems: [AnyComponentWithIdentity] = [] - mediaRightsLoop: for possibleMediaItem in allMediaRightItems { - if !self.allowedMediaRights.contains(possibleMediaItem) { - continue + self.mediaRights.insert(mediaRight) } - let mediaItemTitle: String - switch possibleMediaItem { - case .photos: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendPhoto - case .videos: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendVideo - case .stickersAndGifs: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendStickersAndGifs - case .music: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendMusic - case .files: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendFile - case .voiceMessages: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendVoiceMessage - case .videoMessages: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendVideoMessage - case .links: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionEmbedLinks - case .polls: - mediaItemTitle = environment.strings.Channel_BanUser_PermissionSendPolls - default: - continue mediaRightsLoop - } - - mediaItems.append(AnyComponentWithIdentity(id: possibleMediaItem, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: mediaItemTitle, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - ))), - ], alignment: .left, spacing: 2.0)), - leftIcon: .check(ListActionItemComponent.LeftIcon.Check( - isSelected: self.mediaRights.contains(possibleMediaItem), - toggle: { [weak self] in - guard let self else { - return - } - - if self.mediaRights.contains(possibleMediaItem) { - self.mediaRights.remove(possibleMediaItem) - } else { - self.mediaRights.insert(possibleMediaItem) - } - - self.state?.updated(transition: .spring(duration: 0.35)) - } - )), - icon: .none, - accessory: .none, - action: { [weak self] _ in - guard let self else { - return - } - - if self.mediaRights.contains(possibleMediaItem) { - self.mediaRights.remove(possibleMediaItem) - } else { - self.mediaRights.insert(possibleMediaItem) - } - - self.state?.updated(transition: .spring(duration: 0.35)) - }, - highlighting: .disabled - )))) + self.state?.updated(transition: .spring(duration: 0.35)) } - configSectionItems.append(AnyComponentWithIdentity(id: "media-sub", component: AnyComponent(ListSubSectionComponent( - theme: environment.theme, - leftInset: 0.0, - items: mediaItems - )))) - } - } - } - - let configSectionSize = self.configSection.update( - transition: transition, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - header: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.peers.count == 1 ? environment.strings.Chat_AdminActionSheet_PermissionsSectionHeader : environment.strings.Chat_AdminActionSheet_PermissionsSectionHeaderMultiple, - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor - )), - maximumNumberOfLines: 0 )), - footer: nil, - items: configSectionItems - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) - ) - let configSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 30.0), size: configSectionSize) - if let configSectionView = self.configSection.view { - if configSectionView.superview == nil { - configSectionView.clipsToBounds = true - configSectionView.layer.cornerRadius = 11.0 - self.scrollContentView.addSubview(configSectionView) - self.configSection.parentState = state - } - let effectiveConfigSectionFrame: CGRect - if self.isConfigurationExpanded { - effectiveConfigSectionFrame = configSectionFrame - } else { - effectiveConfigSectionFrame = CGRect(origin: CGPoint(x: configSectionFrame.minX, y: configSectionFrame.minY - 30.0), size: CGSize(width: configSectionFrame.width, height: 0.0)) - } - transition.setFrame(view: configSectionView, frame: effectiveConfigSectionFrame) - transition.setAlpha(view: configSectionView, alpha: self.isConfigurationExpanded ? 1.0 : 0.0) - } - - if availableOptions.contains(.ban) && !configSectionItems.isEmpty { - let optionsFooterFrame: CGRect - if self.isConfigurationExpanded { - contentHeight += 30.0 - contentHeight += configSectionSize.height - contentHeight += 7.0 - optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) - contentHeight += optionsFooterSize.height - } else { - contentHeight += 7.0 - optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) - contentHeight += optionsFooterSize.height - } - if let optionsFooterView = self.optionsFooter.view { - if optionsFooterView.superview == nil { - self.scrollContentView.addSubview(optionsFooterView) - } - transition.setFrame(view: optionsFooterView, frame: optionsFooterFrame) - transition.setAlpha(view: optionsFooterView, alpha: 1.0) - } - } else { - if let optionsFooterView = self.optionsFooter.view { - if optionsFooterView.superview == nil { - self.scrollContentView.addSubview(optionsFooterView) - } - let optionsFooterFrame = CGRect(origin: CGPoint(x: sideInset + 16.0, y: contentHeight), size: optionsFooterSize) - transition.setFrame(view: optionsFooterView, frame: optionsFooterFrame) - transition.setAlpha(view: optionsFooterView, alpha: 0.0) - } - } - - contentHeight += 30.0 - - let actionButtonSize = self.actionButton.update( - transition: transition, - component: AnyComponent(ButtonComponent( - background: ButtonComponent.Background( - style: .glass, - color: environment.theme.list.itemCheckColors.fillColor, - foreground: environment.theme.list.itemCheckColors.foregroundColor, - pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), - cornerRadius: 54.0 * 0.5 + titleItem: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: adminUserActionsTitle( + strings: environmentValue.strings, + mode: component.mode, + peers: component.peers, + selectedDeleteAllPeers: self.optionDeleteAllSelectedPeers + ), + font: Font.semibold(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) ), - content: AnyComponentWithIdentity( - id: AnyHashable(0), - component: AnyComponent(ButtonTextContentComponent( - text: environment.strings.Chat_AdminActionSheet_ActionButton, - badge: 0, - textColor: environment.theme.list.itemCheckColors.foregroundColor, - badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, - badgeForeground: environment.theme.list.itemCheckColors.fillColor - )) - ), - isEnabled: true, - displaysProgress: false, - action: { [weak self] in - guard let self, let component = self.component else { - return + bottomItem: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 54.0 * 0.5 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(ButtonTextContentComponent( + text: environmentValue.strings.Chat_AdminActionSheet_ActionButton, + badge: 0, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: true, + displaysProgress: false, + action: { + performMainAction() } - self.environment?.controller()?.dismiss() - - switch component.mode { - case let .monoforum(completion): - completion(self.calculateMonoforumResult()) - case let .chat(_, _, completion): - completion(self.calculateChatResult()) - case let .liveStream(_, _, completion): - completion(self.calculateLiveStreamResult()) - } - } + )), + backgroundColor: .color(theme.list.modalBlocksBackgroundColor), + animateOut: self.animateOut )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 54.0) + environment: { + environmentValue + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environmentValue.statusBarHeight, + safeInsets: environmentValue.safeInsets, + inputHeight: 0.0, + metrics: environmentValue.metrics, + deviceMetrics: environmentValue.deviceMetrics, + isDisplaying: environmentValue.isVisible, + isCentered: environmentValue.metrics.widthClass == .regular, + screenSize: availableSize, + regularMetricsSize: nil, + dismiss: { animated in + dismiss(animated) + } + ) + }, + forceUpdate: true, + containerSize: availableSize ) - let bottomPanelHeight = 8.0 + environment.safeInsets.bottom + actionButtonSize.height - let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) - if let actionButtonView = actionButton.view { - if actionButtonView.superview == nil { - self.addSubview(actionButtonView) + self.sheet.parentState = state + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) } - transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize)) } - contentHeight += bottomPanelHeight - - clippingY = actionButtonFrame.minY - 24.0 - - let topInset: CGFloat = max(0.0, availableSize.height - containerInset - contentHeight) - - let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) - - self.scrollContentClippingView.layer.cornerRadius = 10.0 - - self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) - - transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) - - transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) - transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) - - let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset)) - transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) - transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) - - self.ignoreScrolling = true - let previousBounds = self.scrollView.bounds - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) - let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) - if contentSize != self.scrollView.contentSize { - self.scrollView.contentSize = contentSize - } - if resetScrolling { - self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) - } else { - if !previousBounds.isEmpty, !transition.animation.isImmediate { - let bounds = self.scrollView.bounds - if bounds.maxY != previousBounds.maxY { - let offsetY = previousBounds.maxY - bounds.maxY - transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) - } - } - } - self.ignoreScrolling = false - self.updateScrolling(transition: transition) - return availableSize } } @@ -1488,17 +1536,20 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer { case chat(messageCount: Int, deleteAllMessageCount: Int?, completion: (ChatResult) -> Void) case liveStream(messageCount: Int, deleteAllMessageCount: Int?, completion: (LiveStreamResult) -> Void) case monoforum(completion: (MonoforumResult) -> Void) + case chatReaction(completion: (ChatResult) -> Void) } public final class ChatResult { public let reportSpamPeers: [EnginePeer.Id] public let deleteAllFromPeers: [EnginePeer.Id] + public let deleteAllReactionsFromPeers: [EnginePeer.Id] public let banPeers: [EnginePeer.Id] public let updateBannedRights: [EnginePeer.Id: TelegramChatBannedRights] - init(reportSpamPeers: [EnginePeer.Id], deleteAllFromPeers: [EnginePeer.Id], banPeers: [EnginePeer.Id], updateBannedRights: [EnginePeer.Id: TelegramChatBannedRights]) { + init(reportSpamPeers: [EnginePeer.Id], deleteAllFromPeers: [EnginePeer.Id], deleteAllReactionsFromPeers: [EnginePeer.Id], banPeers: [EnginePeer.Id], updateBannedRights: [EnginePeer.Id: TelegramChatBannedRights]) { self.reportSpamPeers = reportSpamPeers self.deleteAllFromPeers = deleteAllFromPeers + self.deleteAllReactionsFromPeers = deleteAllReactionsFromPeers self.banPeers = banPeers self.updateBannedRights = updateBannedRights } @@ -1527,12 +1578,17 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer { } private let context: AccountContext - private var isDismissed: Bool = false + private var dismissCompletion: (() -> Void)? public init(context: AccountContext, chatPeer: EnginePeer, peers: [RenderedChannelParticipant], mode: Mode, customTheme: PresentationTheme? = nil) { self.context = context - super.init(context: context, component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, mode: mode), navigationBarAppearance: .none, theme: customTheme.flatMap({ .custom($0) }) ?? .default) + super.init( + context: context, + component: AdminUserActionsSheetComponent(context: context, chatPeer: chatPeer, peers: peers, mode: mode), + navigationBarAppearance: .none, + theme: customTheme.flatMap({ .custom($0) }) ?? .default + ) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal @@ -1548,24 +1604,30 @@ public class AdminUserActionsSheet: ViewControllerComponentContainer { override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.view.disablesInteractiveModalDismiss = true - - if let componentView = self.node.hostView.componentView as? AdminUserActionsSheetComponent.View { - componentView.animateIn() + } + + fileprivate func completePendingDismiss() { + let dismissCompletion = self.dismissCompletion + self.dismissCompletion = nil + dismissCompletion?() + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() } } override public func dismiss(completion: (() -> Void)? = nil) { if !self.isDismissed { self.isDismissed = true + self.dismissCompletion = completion - if let componentView = self.node.hostView.componentView as? AdminUserActionsSheetComponent.View { - componentView.animateOut(completion: { [weak self] in - completion?() - self?.dismiss(animated: false) - }) + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() } else { + self.completePendingDismiss() self.dismiss(animated: false) } } diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift index 5edc3135aa..a84335e979 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift @@ -1,23 +1,20 @@ import Foundation import UIKit import Display -import AsyncDisplayKit import ComponentFlow import SwiftSignalKit import ViewControllerComponent import ComponentDisplayAdapters +import ResizableSheetComponent import TelegramPresentationData import AccountContext import TelegramCore import MultilineTextComponent import ButtonComponent import PresentationDataUtils -import Markdown -import UndoUI import TelegramStringFormatting import ListSectionComponent import ListActionItemComponent -import PlainButtonComponent import GlassBarButtonComponent import BundleIconComponent @@ -163,15 +160,351 @@ private enum ActionType: Hashable { } } -private final class RecentActionsSettingsSheetComponent: Component { +private struct RecentActionsSettingsSheetState: Equatable { + var expandedSections: Set + var selectedMembersActions: Set + var selectedSettingsActions: Set + var selectedMessagesActions: Set + var selectedAdmins: Set +} + +private final class RecentActionsSettingsContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment - + + let context: AccountContext + let peer: EnginePeer + let adminPeers: [EnginePeer] + let theme: PresentationTheme + let sheetState: RecentActionsSettingsSheetState + let toggleActionTypeSectionSelection: (ActionTypeSection) -> Void + let toggleActionTypeSectionExpansion: (ActionTypeSection) -> Void + let toggleActionType: (ActionType) -> Void + let toggleAdmin: (EnginePeer) -> Void + let toggleAllAdmins: () -> Void + + init( + context: AccountContext, + peer: EnginePeer, + adminPeers: [EnginePeer], + theme: PresentationTheme, + sheetState: RecentActionsSettingsSheetState, + toggleActionTypeSectionSelection: @escaping (ActionTypeSection) -> Void, + toggleActionTypeSectionExpansion: @escaping (ActionTypeSection) -> Void, + toggleActionType: @escaping (ActionType) -> Void, + toggleAdmin: @escaping (EnginePeer) -> Void, + toggleAllAdmins: @escaping () -> Void + ) { + self.context = context + self.peer = peer + self.adminPeers = adminPeers + self.theme = theme + self.sheetState = sheetState + self.toggleActionTypeSectionSelection = toggleActionTypeSectionSelection + self.toggleActionTypeSectionExpansion = toggleActionTypeSectionExpansion + self.toggleActionType = toggleActionType + self.toggleAdmin = toggleAdmin + self.toggleAllAdmins = toggleAllAdmins + } + + static func ==(lhs: RecentActionsSettingsContentComponent, rhs: RecentActionsSettingsContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.adminPeers != rhs.adminPeers { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.sheetState != rhs.sheetState { + return false + } + return true + } + + final class View: UIView { + private let optionsSection = ComponentView() + private let adminsSection = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: RecentActionsSettingsContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + let theme = component.theme + let sheetState = component.sheetState + let sideInset: CGFloat = 16.0 + + var isGroup = true + if case let .channel(channel) = component.peer, case .broadcast = channel.info { + isGroup = false + } + + var contentHeight: CGFloat = 76.0 + 15.0 + + let actionTypeSectionItem: (ActionTypeSection) -> AnyComponentWithIdentity = { actionTypeSection in + let totalCount: Int + let selectedCount: Int + let title: String + let isExpanded = sheetState.expandedSections.contains(actionTypeSection) + + switch actionTypeSection { + case .members: + totalCount = MembersActionType.allCases.count + selectedCount = sheetState.selectedMembersActions.count + title = isGroup ? environment.strings.Channel_AdminLogFilter_Section_MembersGroup : environment.strings.Channel_AdminLogFilter_Section_MembersChannel + case .settings: + totalCount = SettingsActionType.allCases.count + selectedCount = sheetState.selectedSettingsActions.count + title = isGroup ? environment.strings.Channel_AdminLogFilter_Section_SettingsGroup : environment.strings.Channel_AdminLogFilter_Section_SettingsChannel + case .messages: + totalCount = MessagesActionType.allCases.count + selectedCount = sheetState.selectedMessagesActions.count + title = environment.strings.Channel_AdminLogFilter_Section_Messages + } + + let itemTitle: AnyComponent = AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: title, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent( + theme: theme, + title: "\(selectedCount)/\(totalCount)", + isExpanded: isExpanded + ))) + ], spacing: 7.0)) + + return AnyComponentWithIdentity(id: actionTypeSection, component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: itemTitle, + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: selectedCount == totalCount, + toggle: { + component.toggleActionTypeSectionSelection(actionTypeSection) + } + )), + icon: .none, + accessory: nil, + action: { _ in + component.toggleActionTypeSectionExpansion(actionTypeSection) + }, + highlighting: .disabled + ))) + } + + let expandedActionTypeSectionItem: (ActionTypeSection) -> AnyComponentWithIdentity = { actionTypeSection in + let sectionId: AnyHashable + let selectedActionTypes: Set + let actionTypes: [ActionType] + switch actionTypeSection { + case .members: + sectionId = "members-sub" + actionTypes = MembersActionType.allCases.map(ActionType.members) + selectedActionTypes = Set(sheetState.selectedMembersActions.map(ActionType.members)) + case .settings: + sectionId = "settings-sub" + actionTypes = SettingsActionType.allCases.map(ActionType.settings) + selectedActionTypes = Set(sheetState.selectedSettingsActions.map(ActionType.settings)) + case .messages: + sectionId = "messages-sub" + actionTypes = MessagesActionType.allCases.map(ActionType.messages) + selectedActionTypes = Set(sheetState.selectedMessagesActions.map(ActionType.messages)) + } + + var subItems: [AnyComponentWithIdentity] = [] + for actionType in actionTypes { + let actionItemTitle: String = actionType.title(isGroup: isGroup, strings: environment.strings) + + subItems.append(AnyComponentWithIdentity(id: actionType, component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: actionItemTitle, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: selectedActionTypes.contains(actionType), + toggle: { + component.toggleActionType(actionType) + } + )), + icon: .none, + accessory: .none, + action: { _ in + component.toggleActionType(actionType) + }, + highlighting: .disabled + )))) + } + + return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListSubSectionComponent( + theme: theme, + leftInset: 62.0, + items: subItems + ))) + } + + var optionsSectionItems: [AnyComponentWithIdentity] = [] + for actionTypeSection in ActionTypeSection.allCases { + optionsSectionItems.append(actionTypeSectionItem(actionTypeSection)) + if sheetState.expandedSections.contains(actionTypeSection) { + optionsSectionItems.append(expandedActionTypeSectionItem(actionTypeSection)) + } + } + + self.optionsSection.parentState = state + let optionsSectionSize = self.optionsSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: theme, + style: .glass, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_AdminLogFilter_FilterActionsTypeTitle, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: optionsSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) + ) + let optionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: optionsSectionSize) + if let optionsSectionView = self.optionsSection.view { + if optionsSectionView.superview == nil { + self.addSubview(optionsSectionView) + } + transition.setFrame(view: optionsSectionView, frame: optionsSectionFrame) + } + contentHeight += optionsSectionSize.height + contentHeight += 24.0 + + var peerItems: [AnyComponentWithIdentity] = [] + for peer in component.adminPeers { + peerItems.append(AnyComponentWithIdentity(id: peer.id, component: AnyComponent(AdminUserActionsPeerComponent( + context: component.context, + theme: theme, + strings: environment.strings, + baseFontSize: presentationData.listsFontSize.baseDisplaySize, + sideInset: 0.0, + title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: peer, + selectionState: .editing(isSelected: sheetState.selectedAdmins.contains(peer.id)), + action: { peer in + component.toggleAdmin(peer) + } + )))) + } + + var adminsSectionItems: [AnyComponentWithIdentity] = [] + adminsSectionItems.append(AnyComponentWithIdentity(id: adminsSectionItems.count, component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_AdminLogFilter_ShowAllAdminsActions, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: .check(ListActionItemComponent.LeftIcon.Check( + isSelected: sheetState.selectedAdmins.count == component.adminPeers.count, + toggle: { + component.toggleAllAdmins() + } + )), + icon: .none, + accessory: .none, + action: { _ in + component.toggleAllAdmins() + }, + highlighting: .disabled + )))) + adminsSectionItems.append(AnyComponentWithIdentity(id: adminsSectionItems.count, component: AnyComponent(ListSubSectionComponent( + theme: theme, + leftInset: 62.0, + items: peerItems + )))) + + self.adminsSection.parentState = state + let adminsSectionSize = self.adminsSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: theme, + style: .glass, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Channel_AdminLogFilter_FilterActionsAdminsTitle, + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: adminsSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) + ) + let adminsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: adminsSectionSize) + if let adminsSectionView = self.adminsSection.view { + if adminsSectionView.superview == nil { + self.addSubview(adminsSectionView) + } + transition.setFrame(view: adminsSectionView, frame: adminsSectionFrame) + } + contentHeight += adminsSectionSize.height + contentHeight += 106.0 + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class RecentActionsSettingsResizableSheetComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + let context: AccountContext let peer: EnginePeer let adminPeers: [EnginePeer] let initialValue: RecentActionsSettingsSheet.Value let completion: (RecentActionsSettingsSheet.Value) -> Void - + init( context: AccountContext, peer: EnginePeer, @@ -185,8 +518,8 @@ private final class RecentActionsSettingsSheetComponent: Component { self.initialValue = initialValue self.completion = completion } - - static func ==(lhs: RecentActionsSettingsSheetComponent, rhs: RecentActionsSettingsSheetComponent) -> Bool { + + static func ==(lhs: RecentActionsSettingsResizableSheetComponent, rhs: RecentActionsSettingsResizableSheetComponent) -> Bool { if lhs.context !== rhs.context { return false } @@ -198,158 +531,29 @@ private final class RecentActionsSettingsSheetComponent: Component { } return true } - - private struct ItemLayout: Equatable { - var containerSize: CGSize - var containerInset: CGFloat - var bottomInset: CGFloat - var topInset: CGFloat - - init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { - self.containerSize = containerSize - self.containerInset = containerInset - self.bottomInset = bottomInset - self.topInset = topInset - } - } - - private final class ScrollView: UIScrollView { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return super.hitTest(point, with: event) - } - } - - final class View: UIView, UIScrollViewDelegate { - private let dimView: UIView - private let backgroundLayer: SimpleLayer - private let navigationBarContainer: SparseContainerView - private let navigationBackgroundView: BlurredBackgroundView - private let navigationBarSeparator: SimpleLayer - private let scrollView: ScrollView - private let scrollContentClippingView: SparseContainerView - private let scrollContentView: UIView - - private let leftButton = ComponentView() - - private let title = ComponentView() - private let actionButton = ComponentView() - - private let optionsSection = ComponentView() - private let adminsSection = ComponentView() - - private let bottomOverscrollLimit: CGFloat - - private var ignoreScrolling: Bool = false - - private var component: RecentActionsSettingsSheetComponent? + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>() + private let animateOut = ActionSlot>() + + private var component: RecentActionsSettingsResizableSheetComponent? private weak var state: EmptyComponentState? - private var environment: ViewControllerComponentContainer.Environment? - private var isUpdating: Bool = false - - private var itemLayout: ItemLayout? - - private var topOffsetDistance: CGFloat? - + private var isDismissing: Bool = false + private var expandedSections = Set() private var selectedMembersActions = Set() private var selectedSettingsActions = Set() private var selectedMessagesActions = Set() private var selectedAdmins = Set() - + override init(frame: CGRect) { - self.bottomOverscrollLimit = 200.0 - - self.dimView = UIView() - - self.backgroundLayer = SimpleLayer() - self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - self.backgroundLayer.cornerRadius = 10.0 - - self.navigationBarContainer = SparseContainerView() - - self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) - self.navigationBarSeparator = SimpleLayer() - - self.scrollView = ScrollView() - - self.scrollContentClippingView = SparseContainerView() - self.scrollContentClippingView.clipsToBounds = true - - self.scrollContentView = UIView() - super.init(frame: frame) - - self.addSubview(self.dimView) - self.layer.addSublayer(self.backgroundLayer) - - 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 = false - self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.alwaysBounceHorizontal = false - self.scrollView.alwaysBounceVertical = true - self.scrollView.scrollsToTop = false - self.scrollView.delegate = self - self.scrollView.clipsToBounds = true - - self.addSubview(self.scrollContentClippingView) - self.scrollContentClippingView.addSubview(self.scrollView) - - self.scrollView.addSubview(self.scrollContentView) - - self.addSubview(self.navigationBarContainer) - - self.navigationBarContainer.addSubview(self.navigationBackgroundView) - self.navigationBarContainer.layer.addSublayer(self.navigationBarSeparator) - - self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - if !self.ignoreScrolling { - self.updateScrolling(transition: .immediate) - } - } - - func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.bounds.contains(point) { - return nil - } - if !self.backgroundLayer.frame.contains(point) { - return self.dimView - } - - if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { - return result - } - - let result = super.hitTest(point, with: event) - return result - } - - @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - guard let environment = self.environment, let controller = environment.controller() else { - return - } - controller.dismiss() - } - } - + private func calculateResult() -> RecentActionsSettingsSheet.Value { var events: AdminLogEventsFlags = [] var admins: [EnginePeer.Id] = [] @@ -370,598 +574,249 @@ private final class RecentActionsSettingsSheetComponent: Component { admins: admins ) } - - private func updateScrolling(isFirstTime: Bool = false, transition: ComponentTransition) { - guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { - return - } - var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset - - let navigationAlpha: CGFloat = 1.0 - max(0.0, min(1.0, (topOffset + 20.0) / 20.0)) - transition.setAlpha(view: self.navigationBackgroundView, alpha: navigationAlpha) - transition.setAlpha(layer: self.navigationBarSeparator, alpha: navigationAlpha) - - topOffset = max(0.0, topOffset) - transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) - - transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) - - let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) - self.topOffsetDistance = topOffsetDistance - var topOffsetFraction = topOffset / topOffsetDistance - topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) - - let modalStyleOverlayTransition: ContainedViewLayoutTransition - if isFirstTime { - modalStyleOverlayTransition = .animated(duration: 0.4, curve: .spring) - } else { - modalStyleOverlayTransition = transition.containedViewLayoutTransition - } - - let transitionFactor: CGFloat = 1.0 - topOffsetFraction - if self.isUpdating { - DispatchQueue.main.async { [weak controller] in - guard let controller else { - return - } - controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalStyleOverlayTransition) - } - } else { - controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalStyleOverlayTransition) - } - } - - func animateIn() { - self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - if let actionButtonView = self.actionButton.view { - actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } - } - - func animateOut(completion: @escaping () -> Void) { - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - - self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in - completion() - }) - self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - if let actionButtonView = self.actionButton.view { - actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - } - - if let environment = self.environment, let controller = environment.controller() { - controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) - } - } - - func update(component: RecentActionsSettingsSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.isUpdating = true - defer { - self.isUpdating = false - } - - let environment = environment[ViewControllerComponentContainer.Environment.self].value - let themeUpdated = self.environment?.theme !== environment.theme - - let resetScrolling = self.scrollView.bounds.width != availableSize.width - - let sideInset: CGFloat = 16.0 + environment.safeInsets.left - - var isFirstTime = false + + func update(component: RecentActionsSettingsResizableSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value + let controller = environmentValue.controller + let theme = environmentValue.theme.withModalBlocksBackground() + if self.component == nil { - isFirstTime = true self.selectedMembersActions = Set(MembersActionType.actionTypesFromFlags(component.initialValue.events)) self.selectedSettingsActions = Set(SettingsActionType.actionTypesFromFlags(component.initialValue.events)) self.selectedMessagesActions = Set(MessagesActionType.actionTypesFromFlags(component.initialValue.events)) self.selectedAdmins = component.initialValue.admins.flatMap { Set($0) } ?? Set(component.adminPeers.map(\.id)) } - - var isGroup = true - if case let .channel(channel) = component.peer, case .broadcast = channel.info { - isGroup = false - } - + self.component = component self.state = state - self.environment = environment - - if themeUpdated { - self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor - - self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor - } - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - - transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) - - var contentHeight: CGFloat = 0.0 - contentHeight += 54.0 - contentHeight += 16.0 - - let leftButtonSize = self.leftButton.update( - transition: transition, - component: AnyComponent(GlassBarButtonComponent( - size: CGSize(width: 44.0, height: 44.0), - backgroundColor: nil, - isDark: environment.theme.overallDarkAppearance, - state: .glass, - component: AnyComponentWithIdentity(id: "close", component: AnyComponent( - BundleIconComponent( - name: "Navigation/Close", - tintColor: environment.theme.chat.inputPanel.panelControlColor - ) - )), - action: { [weak self] _ in - guard let self, let controller = self.environment?.controller() else { - return - } - controller.dismiss() - } - )), - environment: {}, - containerSize: CGSize(width: 44.0, height: 44.0) - ) - let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: 16.0), size: leftButtonSize) - if let leftButtonView = self.leftButton.view { - if leftButtonView.superview == nil { - self.navigationBarContainer.addSubview(leftButtonView) - } - transition.setFrame(view: leftButtonView, frame: leftButtonFrame) - } - - let containerInset: CGFloat = environment.statusBarHeight + 10.0 - - let clippingY: CGFloat - - let actionTypeSectionItem: (ActionTypeSection) -> AnyComponentWithIdentity = { actionTypeSection in - let sectionId: AnyHashable - let totalCount: Int - let selectedCount: Int - let isExpanded: Bool - let title: String - - sectionId = actionTypeSection - isExpanded = self.expandedSections.contains(actionTypeSection) - - switch actionTypeSection { - case .members: - totalCount = MembersActionType.allCases.count - selectedCount = self.selectedMembersActions.count - title = isGroup ? environment.strings.Channel_AdminLogFilter_Section_MembersGroup : environment.strings.Channel_AdminLogFilter_Section_MembersChannel - case .settings: - totalCount = SettingsActionType.allCases.count - selectedCount = self.selectedSettingsActions.count - title = isGroup ? environment.strings.Channel_AdminLogFilter_Section_SettingsGroup : environment.strings.Channel_AdminLogFilter_Section_SettingsChannel - case .messages: - totalCount = MessagesActionType.allCases.count - selectedCount = self.selectedMessagesActions.count - title = environment.strings.Channel_AdminLogFilter_Section_Messages - } - - let itemTitle: AnyComponent = AnyComponent(HStack([ - AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: title, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - ))), - AnyComponentWithIdentity(id: 1, component: AnyComponent(MediaSectionExpandIndicatorComponent( - theme: environment.theme, - title: "\(selectedCount)/\(totalCount)", - isExpanded: isExpanded - ))) - ], spacing: 7.0)) - - let toggleAction: () -> Void = { [weak self] in - guard let self else { - return - } - - switch actionTypeSection { - case .members: - if self.selectedMembersActions.isEmpty { - self.selectedMembersActions = Set(MembersActionType.allCases) - } else { - self.selectedMembersActions.removeAll() - } - case .settings: - if self.selectedSettingsActions.isEmpty { - self.selectedSettingsActions = Set(SettingsActionType.allCases) - } else { - self.selectedSettingsActions.removeAll() - } - case .messages: - if self.selectedMessagesActions.isEmpty { - self.selectedMessagesActions = Set(MessagesActionType.allCases) - } else { - self.selectedMessagesActions.removeAll() - } - } - - self.state?.updated(transition: .spring(duration: 0.35)) - } - - return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - style: .glass, - title: itemTitle, - leftIcon: .check(ListActionItemComponent.LeftIcon.Check( - isSelected: selectedCount == totalCount, - toggle: { - toggleAction() - } - )), - icon: .none, - accessory: nil, - action: { [weak self] _ in - guard let self else { - return - } - if self.expandedSections.contains(actionTypeSection) { - self.expandedSections.remove(actionTypeSection) - } else { - self.expandedSections.insert(actionTypeSection) - } - - self.state?.updated(transition: .spring(duration: 0.35)) - }, - highlighting: .disabled - ))) - } - - let expandedActionTypeSectionItem: (ActionTypeSection) -> AnyComponentWithIdentity = { actionTypeSection in - let sectionId: AnyHashable - let selectedActionTypes: Set - let actionTypes: [ActionType] - switch actionTypeSection { - case .members: - sectionId = "members-sub" - actionTypes = MembersActionType.allCases.map(ActionType.members) - selectedActionTypes = Set(self.selectedMembersActions.map(ActionType.members)) - case .settings: - sectionId = "settings-sub" - actionTypes = SettingsActionType.allCases.map(ActionType.settings) - selectedActionTypes = Set(self.selectedSettingsActions.map(ActionType.settings)) - case .messages: - sectionId = "messages-sub" - actionTypes = MessagesActionType.allCases.map(ActionType.messages) - selectedActionTypes = Set(self.selectedMessagesActions.map(ActionType.messages)) - } - - var subItems: [AnyComponentWithIdentity] = [] - for actionType in actionTypes { - let actionItemTitle: String = actionType.title(isGroup: isGroup, strings: environment.strings) - - let subItemToggleAction: () -> Void = { [weak self] in - guard let self else { - return - } - - switch actionType { - case let .members(value): - if self.selectedMembersActions.contains(value) { - self.selectedMembersActions.remove(value) - } else { - self.selectedMembersActions.insert(value) - } - case let .settings(value): - if self.selectedSettingsActions.contains(value) { - self.selectedSettingsActions.remove(value) - } else { - self.selectedSettingsActions.insert(value) - } - case let .messages(value): - if self.selectedMessagesActions.contains(value) { - self.selectedMessagesActions.remove(value) - } else { - self.selectedMessagesActions.insert(value) - } - } - - self.state?.updated(transition: .spring(duration: 0.35)) - } - - subItems.append(AnyComponentWithIdentity(id: actionType, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - style: .glass, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: actionItemTitle, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 1 - ))), - ], alignment: .left, spacing: 2.0)), - leftIcon: .check(ListActionItemComponent.LeftIcon.Check( - isSelected: selectedActionTypes.contains(actionType), - toggle: { - subItemToggleAction() - } - )), - icon: .none, - accessory: .none, - action: { _ in - subItemToggleAction() - }, - highlighting: .disabled - )))) - } - - return AnyComponentWithIdentity(id: sectionId, component: AnyComponent(ListSubSectionComponent( - theme: environment.theme, - leftInset: 62.0, - items: subItems - ))) - } - - let titleString: String = environment.strings.Channel_AdminLogFilter_RecentActionsTitle - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) - ) - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((72.0 - titleSize.height) * 0.5)), size: titleSize) - if let titleView = title.view { - if titleView.superview == nil { - self.navigationBarContainer.addSubview(titleView) - } - transition.setFrame(view: titleView, frame: titleFrame) - } - - let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0)) - transition.setFrame(view: self.navigationBackgroundView, frame: navigationBackgroundFrame) - self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) - transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - - var optionsSectionItems: [AnyComponentWithIdentity] = [] - for actionTypeSection in ActionTypeSection.allCases { - optionsSectionItems.append(actionTypeSectionItem(actionTypeSection)) - if self.expandedSections.contains(actionTypeSection) { - optionsSectionItems.append(expandedActionTypeSectionItem(actionTypeSection)) - } - } - - let optionsSectionTransition = transition - let optionsSectionSize = self.optionsSection.update( - transition: optionsSectionTransition, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - style: .glass, - header: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Channel_AdminLogFilter_FilterActionsTypeTitle, - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor - )), - maximumNumberOfLines: 0 - )), - footer: nil, - items: optionsSectionItems - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) - ) - let optionsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: optionsSectionSize) - if let optionsSectionView = self.optionsSection.view { - if optionsSectionView.superview == nil { - self.scrollContentView.addSubview(optionsSectionView) - self.optionsSection.parentState = state - } - transition.setFrame(view: optionsSectionView, frame: optionsSectionFrame) - } - contentHeight += optionsSectionSize.height - contentHeight += 24.0 - - var peerItems: [AnyComponentWithIdentity] = [] - for peer in component.adminPeers { - peerItems.append(AnyComponentWithIdentity(id: peer.id, component: AnyComponent(AdminUserActionsPeerComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - baseFontSize: presentationData.listsFontSize.baseDisplaySize, - sideInset: 0.0, - title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), - peer: peer, - selectionState: .editing(isSelected: self.selectedAdmins.contains(peer.id)), - action: { [weak self] peer in - guard let self else { - return - } - - if self.selectedAdmins.contains(peer.id) { - self.selectedAdmins.remove(peer.id) - } else { - self.selectedAdmins.insert(peer.id) - } - - self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .easeInOut))) - } - )))) - } - - var adminsSectionItems: [AnyComponentWithIdentity] = [] - let allAdminsToggleAction: () -> Void = { [weak self] in - guard let self, let component = self.component else { + + let dismiss: (Bool) -> Void = { [weak self] animated in + guard let self, !self.isDismissing else { return } - - if self.selectedAdmins.isEmpty { - self.selectedAdmins = Set(component.adminPeers.map(\.id)) - } else { - self.selectedAdmins.removeAll() + self.isDismissing = true + + let performDismiss: () -> Void = { + if let controller = controller() as? RecentActionsSettingsSheet { + controller.completePendingDismiss() + controller.dismiss(animated: false) + } + } + + if animated { + self.animateOut.invoke(Action { _ in + performDismiss() + }) + } else { + performDismiss() } - - self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .easeInOut))) } - adminsSectionItems.append(AnyComponentWithIdentity(id: adminsSectionItems.count, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - style: .glass, - title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + + let currentState = RecentActionsSettingsSheetState( + expandedSections: self.expandedSections, + selectedMembersActions: self.selectedMembersActions, + selectedSettingsActions: self.selectedSettingsActions, + selectedMessagesActions: self.selectedMessagesActions, + selectedAdmins: self.selectedAdmins + ) + + let sheetSize = self.sheet.update( + transition: transition, + component: AnyComponent(ResizableSheetComponent( + content: AnyComponent(RecentActionsSettingsContentComponent( + context: component.context, + peer: component.peer, + adminPeers: component.adminPeers, + theme: theme, + sheetState: currentState, + toggleActionTypeSectionSelection: { [weak self] actionTypeSection in + guard let self else { + return + } + + switch actionTypeSection { + case .members: + if self.selectedMembersActions.isEmpty { + self.selectedMembersActions = Set(MembersActionType.allCases) + } else { + self.selectedMembersActions.removeAll() + } + case .settings: + if self.selectedSettingsActions.isEmpty { + self.selectedSettingsActions = Set(SettingsActionType.allCases) + } else { + self.selectedSettingsActions.removeAll() + } + case .messages: + if self.selectedMessagesActions.isEmpty { + self.selectedMessagesActions = Set(MessagesActionType.allCases) + } else { + self.selectedMessagesActions.removeAll() + } + } + + self.state?.updated(transition: .spring(duration: 0.35)) + }, + toggleActionTypeSectionExpansion: { [weak self] actionTypeSection in + guard let self else { + return + } + if self.expandedSections.contains(actionTypeSection) { + self.expandedSections.remove(actionTypeSection) + } else { + self.expandedSections.insert(actionTypeSection) + } + + self.state?.updated(transition: .spring(duration: 0.35)) + }, + toggleActionType: { [weak self] actionType in + guard let self else { + return + } + + switch actionType { + case let .members(value): + if self.selectedMembersActions.contains(value) { + self.selectedMembersActions.remove(value) + } else { + self.selectedMembersActions.insert(value) + } + case let .settings(value): + if self.selectedSettingsActions.contains(value) { + self.selectedSettingsActions.remove(value) + } else { + self.selectedSettingsActions.insert(value) + } + case let .messages(value): + if self.selectedMessagesActions.contains(value) { + self.selectedMessagesActions.remove(value) + } else { + self.selectedMessagesActions.insert(value) + } + } + + self.state?.updated(transition: .spring(duration: 0.35)) + }, + toggleAdmin: { [weak self] peer in + guard let self else { + return + } + + if self.selectedAdmins.contains(peer.id) { + self.selectedAdmins.remove(peer.id) + } else { + self.selectedAdmins.insert(peer.id) + } + + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .easeInOut))) + }, + toggleAllAdmins: { [weak self] in + guard let self, let component = self.component else { + return + } + + if self.selectedAdmins.isEmpty { + self.selectedAdmins = Set(component.adminPeers.map(\.id)) + } else { + self.selectedAdmins.removeAll() + } + + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .easeInOut))) + } + )), + titleItem: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: environment.strings.Channel_AdminLogFilter_ShowAllAdminsActions, - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor + string: environmentValue.strings.Channel_AdminLogFilter_RecentActionsTitle, + font: Font.semibold(17.0), + textColor: theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 - ))), - ], alignment: .left, spacing: 2.0)), - leftIcon: .check(ListActionItemComponent.LeftIcon.Check( - isSelected: self.selectedAdmins.count == component.adminPeers.count, - toggle: { - allAdminsToggleAction() - } - )), - icon: .none, - accessory: .none, - action: { _ in - allAdminsToggleAction() - }, - highlighting: .disabled - )))) - adminsSectionItems.append(AnyComponentWithIdentity(id: adminsSectionItems.count, component: AnyComponent(ListSubSectionComponent( - theme: environment.theme, - leftInset: 62.0, - items: peerItems - )))) - let adminsSectionSize = self.adminsSection.update( - transition: transition, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - style: .glass, - header: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: environment.strings.Channel_AdminLogFilter_FilterActionsAdminsTitle, - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor - )), - maximumNumberOfLines: 0 )), - footer: nil, - items: adminsSectionItems - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100000.0) - ) - let adminsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: adminsSectionSize) - if let adminsSectionView = self.adminsSection.view { - if adminsSectionView.superview == nil { - self.scrollContentView.addSubview(adminsSectionView) - self.adminsSection.parentState = state - } - transition.setFrame(view: adminsSectionView, frame: adminsSectionFrame) - } - contentHeight += adminsSectionSize.height - - contentHeight += 30.0 - - let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) - let actionButtonSize = self.actionButton.update( - transition: transition, - component: AnyComponent(ButtonComponent( - background: ButtonComponent.Background( - style: .glass, - color: environment.theme.list.itemCheckColors.fillColor, - foreground: environment.theme.list.itemCheckColors.foregroundColor, - pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) ), - content: AnyComponentWithIdentity( - id: AnyHashable(0), - component: AnyComponent(ButtonTextContentComponent( - text: environment.strings.Channel_AdminLogFilter_ApplyFilter, - badge: 0, - textColor: environment.theme.list.itemCheckColors.foregroundColor, - badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, - badgeForeground: environment.theme.list.itemCheckColors.fillColor - )) - ), - isEnabled: true, - displaysProgress: false, - action: { [weak self] in - guard let self, let component = self.component else { - return + bottomItem: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(ButtonTextContentComponent( + text: environmentValue.strings.Channel_AdminLogFilter_ApplyFilter, + badge: 0, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + )) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + let result = self.calculateResult() + dismiss(true) + component.completion(result) } - self.environment?.controller()?.dismiss() - component.completion(self.calculateResult()) - } + )), + backgroundColor: .color(theme.list.modalBlocksBackgroundColor), + animateOut: self.animateOut )), - environment: {}, - containerSize: CGSize(width: availableSize.width - bottomInsets.left - bottomInsets.right, height: 52.0) + environment: { + environmentValue + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environmentValue.statusBarHeight, + safeInsets: environmentValue.safeInsets, + inputHeight: 0.0, + metrics: environmentValue.metrics, + deviceMetrics: environmentValue.deviceMetrics, + isDisplaying: environmentValue.isVisible, + isCentered: environmentValue.metrics.widthClass == .regular, + screenSize: availableSize, + regularMetricsSize: nil, + dismiss: { animated in + dismiss(animated) + } + ) + }, + forceUpdate: true, + containerSize: availableSize ) - let bottomPanelHeight = actionButtonSize.height + bottomInsets.bottom - let actionButtonFrame = CGRect(origin: CGPoint(x: bottomInsets.left, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) - if let actionButtonView = actionButton.view { - if actionButtonView.superview == nil { - self.addSubview(actionButtonView) + self.sheet.parentState = state + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) } - transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize)) } - - contentHeight += bottomPanelHeight - - clippingY = actionButtonFrame.minY - 24.0 - - let topInset: CGFloat = max(0.0, availableSize.height - containerInset - contentHeight) - - let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) - - self.scrollContentClippingView.layer.cornerRadius = 10.0 - - self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) - - transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) - - transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) - transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) - - let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset)) - transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) - transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) - - self.ignoreScrolling = true - let previousBounds = self.scrollView.bounds - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) - let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) - if contentSize != self.scrollView.contentSize { - self.scrollView.contentSize = contentSize - } - if resetScrolling { - self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) - } else { - if !previousBounds.isEmpty, !transition.animation.isImmediate { - let bounds = self.scrollView.bounds - if bounds.maxY != previousBounds.maxY { - let offsetY = previousBounds.maxY - bounds.maxY - transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) - } - } - } - self.ignoreScrolling = false - self.updateScrolling(isFirstTime: isFirstTime, transition: transition) - + 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) } @@ -981,11 +836,12 @@ public class RecentActionsSettingsSheet: ViewControllerComponentContainer { private let context: AccountContext private var isDismissed: Bool = false + private var dismissCompletion: (() -> Void)? public init(context: AccountContext, peer: EnginePeer, adminPeers: [EnginePeer], initialValue: Value, completion: @escaping (Value) -> Void) { self.context = context - super.init(context: context, component: RecentActionsSettingsSheetComponent(context: context, peer: peer, adminPeers: adminPeers, initialValue: initialValue, completion: completion), navigationBarAppearance: .none) + super.init(context: context, component: RecentActionsSettingsResizableSheetComponent(context: context, peer: peer, adminPeers: adminPeers, initialValue: initialValue, completion: completion), navigationBarAppearance: .none) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal @@ -1003,22 +859,29 @@ public class RecentActionsSettingsSheet: ViewControllerComponentContainer { super.viewDidAppear(animated) self.view.disablesInteractiveModalDismiss = true - - if let componentView = self.node.hostView.componentView as? RecentActionsSettingsSheetComponent.View { - componentView.animateIn() + } + + fileprivate func completePendingDismiss() { + let dismissCompletion = self.dismissCompletion + self.dismissCompletion = nil + dismissCompletion?() + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() } } override public func dismiss(completion: (() -> Void)? = nil) { if !self.isDismissed { self.isDismissed = true + self.dismissCompletion = completion - if let componentView = self.node.hostView.componentView as? RecentActionsSettingsSheetComponent.View { - componentView.animateOut(completion: { [weak self] in - completion?() - self?.dismiss(animated: false) - }) + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() } else { + self.completePendingDismiss() self.dismiss(animated: false) } } diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD b/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD index 1b071b8cfd..99414e0246 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift index 49f9b1e7d7..1d484546b6 100644 --- a/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift @@ -19,6 +19,7 @@ public final class AlertMultilineInputFieldComponent: Component { public fileprivate(set) var value: NSAttributedString = NSAttributedString() public fileprivate(set) var animateError: () -> Void = {} public fileprivate(set) var activateInput: () -> Void = {} + fileprivate var setValueImpl: ((NSAttributedString, Range) -> Void)? fileprivate let valuePromise = ValuePromise(NSAttributedString()) public var valueSignal: Signal { return self.valuePromise.get() @@ -32,6 +33,13 @@ public final class AlertMultilineInputFieldComponent: Component { public init() { } + + public func setValue(_ value: NSAttributedString, selectionRange: Range? = nil) { + let selectionRange = selectionRange ?? (value.length ..< value.length) + self.value = value + self.valuePromise.set(value) + self.setValueImpl?(value, selectionRange) + } } public enum FormatMenuAvailability: Equatable { @@ -226,13 +234,18 @@ public final class AlertMultilineInputFieldComponent: Component { func update(component: AlertMultilineInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { var resetText: NSAttributedString? if self.component == nil { - resetText = component.initialValue + resetText = component.initialValue ?? (component.externalState.value.length == 0 ? nil : component.externalState.value) component.externalState.animateError = { [weak self] in self?.animateError() } component.externalState.activateInput = { [weak self] in self?.activateInput() } + component.externalState.setValueImpl = { [weak self] value, selectionRange in + if let textFieldView = self?.textField.view as? TextFieldComponent.View { + textFieldView.updateText(value, selectionRange: selectionRange) + } + } } let isFirstTime = self.component == nil diff --git a/submodules/TelegramUI/Components/AnimationCache/BUILD b/submodules/TelegramUI/Components/AnimationCache/BUILD index e655aa9fe6..3d182a866a 100644 --- a/submodules/TelegramUI/Components/AnimationCache/BUILD +++ b/submodules/TelegramUI/Components/AnimationCache/BUILD @@ -11,9 +11,6 @@ swift_library( ], deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/CryptoUtils:CryptoUtils", - "//submodules/ManagedFile:ManagedFile", - "//submodules/TelegramUI/Components/AnimationCache/ImageDCT:ImageDCT", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index 0c99aada38..6c6391bb73 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -1,51 +1,19 @@ import Foundation import UIKit import SwiftSignalKit -import CryptoUtils -import ManagedFile -import Compression - -private let algorithm: compression_algorithm = COMPRESSION_LZFSE - -private func alignUp(size: Int, align: Int) -> Int { - precondition(((align - 1) & align) == 0, "Align must be a power of two") - - let alignmentMask = align - 1 - return (size + alignmentMask) & ~alignmentMask -} - -private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { - if useTotalFileAllocatedSize { - let url = URL(fileURLWithPath: path) - if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .fileAllocatedSizeKey]))) { - if values.isRegularFile ?? false { - if let fileSize = values.fileAllocatedSize { - return Int64(fileSize) - } - } - } - } - - var value = stat() - if stat(path, &value) == 0 { - return value.st_size - } else { - return nil - } -} public final class AnimationCacheItemFrame { public enum RequestedFormat { case rgba case yuva(rowAlignment: Int) } - + public final class Plane { public let data: Data public let width: Int public let height: Int public let bytesPerRow: Int - + public init(data: Data, width: Int, height: Int, bytesPerRow: Int) { self.data = data self.width = width @@ -53,15 +21,15 @@ public final class AnimationCacheItemFrame { self.bytesPerRow = bytesPerRow } } - + public enum Format { case rgba(data: Data, width: Int, height: Int, bytesPerRow: Int) case yuva(y: Plane, u: Plane, v: Plane, a: Plane) } - + public let format: Format public let duration: Double - + public init(format: Format, duration: Double) { self.format = format self.duration = duration @@ -73,31 +41,31 @@ public final class AnimationCacheItem { case duration(Double) case frames(Int) } - + public struct AdvanceResult { public let frame: AnimationCacheItemFrame public let didLoop: Bool - + public init(frame: AnimationCacheItemFrame, didLoop: Bool) { self.frame = frame self.didLoop = didLoop } } - + public let numFrames: Int private let advanceImpl: (Advance, AnimationCacheItemFrame.RequestedFormat) -> AdvanceResult? private let resetImpl: () -> Void - + public init(numFrames: Int, advanceImpl: @escaping (Advance, AnimationCacheItemFrame.RequestedFormat) -> AdvanceResult?, resetImpl: @escaping () -> Void) { self.numFrames = numFrames self.advanceImpl = advanceImpl self.resetImpl = resetImpl } - + public func advance(advance: Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AdvanceResult? { return self.advanceImpl(advance, requestedFormat) } - + public func reset() { self.resetImpl() } @@ -109,7 +77,7 @@ public struct AnimationCacheItemDrawingSurface { public let height: Int public let bytesPerRow: Int public let length: Int - + public init( argb: UnsafeMutablePointer, width: Int, @@ -128,7 +96,7 @@ public struct AnimationCacheItemDrawingSurface { public protocol AnimationCacheItemWriter: AnyObject { var queue: Queue { get } var isCancelled: Bool { get } - + func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) func finish() } @@ -136,7 +104,7 @@ public protocol AnimationCacheItemWriter: AnyObject { public final class AnimationCacheItemResult { public let item: AnimationCacheItem? public let isFinal: Bool - + public init(item: AnimationCacheItem?, isFinal: Bool) { self.item = item self.isFinal = isFinal @@ -147,7 +115,7 @@ public struct AnimationCacheFetchOptions { public let size: CGSize public let writer: AnimationCacheItemWriter public let firstFrameOnly: Bool - + public init( size: CGSize, writer: AnimationCacheItemWriter, @@ -164,1525 +132,3 @@ public protocol AnimationCache: AnyObject { func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable } - -private func md5Hash(_ string: String) -> String { - let hashData = string.data(using: .utf8)!.withUnsafeBytes { bytes -> Data in - return CryptoMD5(bytes.baseAddress!, Int32(bytes.count)) - } - return hashData.withUnsafeBytes { bytes -> String in - let uintBytes = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self) - return String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", uintBytes[0], uintBytes[1], uintBytes[2], uintBytes[3], uintBytes[4], uintBytes[5], uintBytes[6], uintBytes[7], uintBytes[8], uintBytes[9], uintBytes[10], uintBytes[11], uintBytes[12], uintBytes[13], uintBytes[14], uintBytes[15]) - } -} - -private func itemSubpath(hashString: String, width: Int, height: Int) -> (directory: String, fileName: String) { - assert(hashString.count == 32) - var directory = "" - - for i in 0 ..< 1 { - if !directory.isEmpty { - directory.append("/") - } - directory.append(String(hashString[hashString.index(hashString.startIndex, offsetBy: i * 2) ..< hashString.index(hashString.startIndex, offsetBy: (i + 1) * 2)])) - } - - return (directory, "\(hashString)_\(width)x\(height)") -} - -private func roundUp(_ numToRound: Int, multiple: Int) -> Int { - if multiple == 0 { - return numToRound - } - - let remainder = numToRound % multiple - if remainder == 0 { - return numToRound; - } - - return numToRound + multiple - remainder -} - -private func compressData(data: Data, addSizeHeader: Bool = false) -> Data? { - let scratchData = malloc(compression_encode_scratch_buffer_size(algorithm))! - defer { - free(scratchData) - } - - let headerSize = addSizeHeader ? 4 : 0 - var compressedData = Data(count: headerSize + data.count + 16 * 1024) - let resultSize = compressedData.withUnsafeMutableBytes { buffer -> Int in - guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return 0 - } - - if addSizeHeader { - var decompressedSize: UInt32 = UInt32(data.count) - memcpy(bytes, &decompressedSize, 4) - } - - return data.withUnsafeBytes { sourceBuffer -> Int in - return compression_encode_buffer(bytes.advanced(by: headerSize), buffer.count - headerSize, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), sourceBuffer.count, scratchData, algorithm) - } - } - - if resultSize <= 0 { - return nil - } - compressedData.count = headerSize + resultSize - return compressedData -} - -private func decompressData(data: Data, range: Range, decompressedSize: Int) -> Data? { - let scratchData = malloc(compression_decode_scratch_buffer_size(algorithm))! - defer { - free(scratchData) - } - - var decompressedFrameData = Data(count: decompressedSize) - let resultSize = decompressedFrameData.withUnsafeMutableBytes { buffer -> Int in - guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { - return 0 - } - return data.withUnsafeBytes { sourceBuffer -> Int in - return compression_decode_buffer(bytes, buffer.count, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: range.lowerBound), range.upperBound - range.lowerBound, scratchData, algorithm) - } - } - - if resultSize <= 0 { - return nil - } - if decompressedFrameData.count != resultSize { - decompressedFrameData.count = resultSize - } - return decompressedFrameData -} - -private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { - enum WriteError: Error { - case generic - } - - struct CompressedResult { - var animationPath: String - } - - private struct FrameMetadata { - var duration: Double - } - - var queue: Queue { - return self.innerQueue - } - let innerQueue: Queue - var isCancelled: Bool = false - - private let compressedPath: String - private var file: ManagedFile? - private var compressedWriter: CompressedFileWriter? - private let completion: (CompressedResult?) -> Void - - - private var currentSurface: ImageARGB? - private var currentYUVASurface: ImageYUVA420? - private var currentFrameFloat: FloatCoefficientsYUVA420? - private var previousFrameCoefficients: DctCoefficientsYUVA420? - private var deltaFrameFloat: FloatCoefficientsYUVA420? - private var previousYUVASurface: ImageYUVA420? - private var currentDctData: DctData? - private var differenceCoefficients: DctCoefficientsYUVA420? - private var currentDctCoefficients: DctCoefficientsYUVA420? - private var contentLengthOffset: Int? - private var isFailed: Bool = false - private var isFinished: Bool = false - - private var frames: [FrameMetadata] = [] - - private let dctQualityLuma: Int - private let dctQualityChroma: Int - private let dctQualityDelta: Int - - private let lock = Lock() - - init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) { - self.dctQualityLuma = 70 - self.dctQualityChroma = 88 - self.dctQualityDelta = 22 - - self.innerQueue = queue - self.compressedPath = allocateTempFile() - - guard let file = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else { - return nil - } - self.file = file - self.compressedWriter = CompressedFileWriter(file: file) - self.completion = completion - } - - func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) { - do { - try self.lock.throwingLocked { - let width = roundUp(proposedWidth, multiple: 16) - let height = roundUp(proposedHeight, multiple: 16) - - let surface: ImageARGB - if let current = self.currentSurface { - if current.argbPlane.width == width && current.argbPlane.height == height { - surface = current - surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Void in - memset(bytes.baseAddress!, 0, bytes.count) - } - } else { - self.isFailed = true - return - } - } else { - surface = ImageARGB(width: width, height: height, rowAlignment: 32) - self.currentSurface = surface - } - - let duration = surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Double? in - return drawingBlock(AnimationCacheItemDrawingSurface( - argb: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), - width: width, - height: height, - bytesPerRow: surface.argbPlane.bytesPerRow, - length: bytes.count - )) - } - - guard let duration = duration else { - return - } - - try addInternal(with: { yuvaSurface in - surface.toYUVA420(target: yuvaSurface) - - return duration - }, width: width, height: height, insertKeyframe: insertKeyframe) - } - } catch { - } - } - - func addYUV(with drawingBlock: (ImageYUVA420) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) throws { - let width = roundUp(proposedWidth, multiple: 16) - let height = roundUp(proposedHeight, multiple: 16) - - do { - try self.lock.throwingLocked { - try addInternal(with: { yuvaSurface in - return drawingBlock(yuvaSurface) - }, width: width, height: height, insertKeyframe: insertKeyframe) - } - } catch { - } - } - - func addInternal(with drawingBlock: (ImageYUVA420) -> Double?, width: Int, height: Int, insertKeyframe: Bool) throws { - if width == 0 || height == 0 { - self.isFailed = true - throw WriteError.generic - } - if self.isFailed || self.isFinished { - throw WriteError.generic - } - - guard !self.isFailed, !self.isFinished, let file = self.file, let compressedWriter = self.compressedWriter else { - throw WriteError.generic - } - - var isFirstFrame = false - - let yuvaSurface: ImageYUVA420 - if let current = self.currentYUVASurface { - if current.yPlane.width == width && current.yPlane.height == height { - yuvaSurface = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - isFirstFrame = true - - yuvaSurface = ImageYUVA420(width: width, height: height, rowAlignment: nil) - self.currentYUVASurface = yuvaSurface - } - - let currentFrameFloat: FloatCoefficientsYUVA420 - if let current = self.currentFrameFloat { - if current.yPlane.width == width && current.yPlane.height == height { - currentFrameFloat = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - currentFrameFloat = FloatCoefficientsYUVA420(width: width, height: height) - self.currentFrameFloat = currentFrameFloat - } - - let previousFrameCoefficients: DctCoefficientsYUVA420 - if let current = self.previousFrameCoefficients { - if current.yPlane.width == width && current.yPlane.height == height { - previousFrameCoefficients = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - previousFrameCoefficients = DctCoefficientsYUVA420(width: width, height: height) - self.previousFrameCoefficients = previousFrameCoefficients - } - - let deltaFrameFloat: FloatCoefficientsYUVA420 - if let current = self.deltaFrameFloat { - if current.yPlane.width == width && current.yPlane.height == height { - deltaFrameFloat = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - deltaFrameFloat = FloatCoefficientsYUVA420(width: width, height: height) - self.deltaFrameFloat = deltaFrameFloat - } - - let dctData: DctData - if let current = self.currentDctData { - dctData = current - } else { - dctData = DctData(generatingTablesAtQualityLuma: self.dctQualityLuma, chroma: self.dctQualityChroma, delta: self.dctQualityDelta) - self.currentDctData = dctData - } - - let duration = drawingBlock(yuvaSurface) - - guard let duration = duration else { - return - } - - let dctCoefficients: DctCoefficientsYUVA420 - if let current = self.currentDctCoefficients { - if current.yPlane.width == width && current.yPlane.height == height { - dctCoefficients = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - dctCoefficients = DctCoefficientsYUVA420(width: width, height: height) - self.currentDctCoefficients = dctCoefficients - } - - let differenceCoefficients: DctCoefficientsYUVA420 - if let current = self.differenceCoefficients { - if current.yPlane.width == width && current.yPlane.height == height { - differenceCoefficients = current - } else { - self.isFailed = true - throw WriteError.generic - } - } else { - differenceCoefficients = DctCoefficientsYUVA420(width: width, height: height) - self.differenceCoefficients = differenceCoefficients - } - - #if !arch(arm64) - var insertKeyframe = insertKeyframe - insertKeyframe = true - #endif - - let previousYUVASurface: ImageYUVA420 - if let current = self.previousYUVASurface { - previousYUVASurface = current - } else { - previousYUVASurface = ImageYUVA420(width: dctCoefficients.yPlane.width, height: dctCoefficients.yPlane.height, rowAlignment: nil) - self.previousYUVASurface = previousYUVASurface - } - - let isKeyframe: Bool - if !isFirstFrame && !insertKeyframe { - isKeyframe = false - - //previous + delta = current - //delta = current - previous - yuvaSurface.toCoefficients(target: differenceCoefficients) - differenceCoefficients.subtract(other: previousFrameCoefficients) - differenceCoefficients.dct4x4(dctData: dctData, target: dctCoefficients) - - //previous + delta = current - dctCoefficients.idct4x4Add(dctData: dctData, target: previousFrameCoefficients) - //previousFrameCoefficients.add(other: differenceCoefficients) - } else { - isKeyframe = true - - yuvaSurface.dct8x8(dctData: dctData, target: dctCoefficients) - - dctCoefficients.idct8x8(dctData: dctData, target: yuvaSurface) - yuvaSurface.toCoefficients(target: previousFrameCoefficients) - } - - if isFirstFrame { - file.write(6 as UInt32) - - file.write(UInt32(dctCoefficients.yPlane.width)) - file.write(UInt32(dctCoefficients.yPlane.height)) - - let lumaDctTable = dctData.lumaTable.serializedData() - file.write(UInt32(lumaDctTable.count)) - let _ = file.write(lumaDctTable) - - let chromaDctTable = dctData.chromaTable.serializedData() - file.write(UInt32(chromaDctTable.count)) - let _ = file.write(chromaDctTable) - - let deltaDctTable = dctData.deltaTable.serializedData() - file.write(UInt32(deltaDctTable.count)) - let _ = file.write(deltaDctTable) - - self.contentLengthOffset = Int(file.position()) - file.write(0 as UInt32) - } - - do { - let frameLength = dctCoefficients.yPlane.data.count + dctCoefficients.uPlane.data.count + dctCoefficients.vPlane.data.count + dctCoefficients.aPlane.data.count - try compressedWriter.writeUInt32(UInt32(frameLength)) - - try compressedWriter.writeUInt32(isKeyframe ? 1 : 0) - - for i in 0 ..< 4 { - let dctPlane: DctCoefficientPlane - switch i { - case 0: - dctPlane = dctCoefficients.yPlane - case 1: - dctPlane = dctCoefficients.uPlane - case 2: - dctPlane = dctCoefficients.vPlane - case 3: - dctPlane = dctCoefficients.aPlane - default: - preconditionFailure() - } - - try compressedWriter.writeUInt32(UInt32(dctPlane.data.count)) - try dctPlane.data.withUnsafeBytes { bytes in - try compressedWriter.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) - } - } - - self.frames.append(FrameMetadata(duration: duration)) - } catch { - self.isFailed = true - throw WriteError.generic - } - } - - func finish() { - do { - let result = try self.finishInternal() - self.completion(result) - } catch { - } - } - - func finishInternal() throws -> CompressedResult? { - var shouldComplete = false - self.lock.locked { - if !self.isFinished { - self.isFinished = true - shouldComplete = true - - guard let contentLengthOffset = self.contentLengthOffset, let file = self.file, let compressedWriter = self.compressedWriter else { - self.isFailed = true - return - } - assert(contentLengthOffset >= 0) - - do { - try compressedWriter.flush() - - let metadataPosition = file.position() - let contentLength = Int(metadataPosition) - contentLengthOffset - 4 - let _ = file.seek(position: Int64(contentLengthOffset)) - file.write(UInt32(contentLength)) - - let _ = file.seek(position: metadataPosition) - file.write(UInt32(self.frames.count)) - for frame in self.frames { - file.write(Float32(frame.duration)) - } - - if !self.isFailed { - self.compressedWriter = nil - self.file = nil - - file._unsafeClose() - } - } catch { - self.isFailed = true - } - } - } - - if shouldComplete { - if !self.isFailed { - return CompressedResult(animationPath: self.compressedPath) - } else { - let _ = try? FileManager.default.removeItem(atPath: self.compressedPath) - return nil - } - } else { - return nil - } - } -} - -private final class AnimationCacheItemAccessor { - private enum ReadError: Error { - case generic - } - - final class CurrentFrame { - let index: Int - var remainingDuration: Double - let duration: Double - let yuva: ImageYUVA420 - - init(index: Int, duration: Double, yuva: ImageYUVA420) { - self.index = index - self.duration = duration - self.remainingDuration = duration - self.yuva = yuva - } - } - - struct FrameInfo { - let duration: Double - } - - private let data: Data - private var compressedDataReader: DecompressedData? - private let range: Range - private let frameMapping: [Int: FrameInfo] - private let width: Int - private let height: Int - private let durationMapping: [Double] - - private var currentFrame: CurrentFrame? - - private var currentYUVASurface: ImageYUVA420? - private var currentCoefficients: DctCoefficientsYUVA420? - private let currentDctData: DctData - private var sharedDctCoefficients: DctCoefficientsYUVA420? - private var deltaCoefficients: DctCoefficientsYUVA420? - - init(data: Data, range: Range, frameMapping: [FrameInfo], width: Int, height: Int, dctData: DctData) { - self.data = data - self.range = range - self.width = width - self.height = height - - var resultFrameMapping: [Int: FrameInfo] = [:] - var durationMapping: [Double] = [] - - for i in 0 ..< frameMapping.count { - let frame = frameMapping[i] - resultFrameMapping[i] = frame - durationMapping.append(frame.duration) - } - - self.frameMapping = resultFrameMapping - self.durationMapping = durationMapping - - self.currentDctData = dctData - } - - private func loadNextFrame() -> Bool { - var didLoop = false - let index: Int - if let currentFrame = self.currentFrame { - if currentFrame.index + 1 >= self.durationMapping.count { - index = 0 - self.compressedDataReader = nil - didLoop = true - } else { - index = currentFrame.index + 1 - } - } else { - index = 0 - self.compressedDataReader = nil - } - - if self.compressedDataReader == nil { - self.compressedDataReader = DecompressedData(compressedData: self.data, dataRange: self.range) - } - - guard let compressedDataReader = self.compressedDataReader else { - self.currentFrame = nil - return didLoop - } - - do { - let frameLength = Int(try compressedDataReader.readUInt32()) - - let frameType = Int(try compressedDataReader.readUInt32()) - - let dctCoefficients: DctCoefficientsYUVA420 - if let sharedDctCoefficients = self.sharedDctCoefficients, sharedDctCoefficients.yPlane.width == self.width, sharedDctCoefficients.yPlane.height == self.height, !"".isEmpty { - dctCoefficients = sharedDctCoefficients - } else { - dctCoefficients = DctCoefficientsYUVA420(width: self.width, height: self.height) - self.sharedDctCoefficients = dctCoefficients - } - - var frameOffset = 0 - for i in 0 ..< 4 { - let planeLength = Int(try compressedDataReader.readUInt32()) - if planeLength < 0 || planeLength > 20 * 1024 * 1024 { - throw ReadError.generic - } - - let plane: DctCoefficientPlane - switch i { - case 0: - plane = dctCoefficients.yPlane - case 1: - plane = dctCoefficients.uPlane - case 2: - plane = dctCoefficients.vPlane - case 3: - plane = dctCoefficients.aPlane - default: - throw ReadError.generic - } - - if planeLength != plane.data.count { - throw ReadError.generic - } - - if frameOffset + plane.data.count > frameLength { - throw ReadError.generic - } - - try plane.data.withUnsafeMutableBytes { bytes in - try compressedDataReader.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) - } - frameOffset += plane.data.count - } - - let yuvaSurface: ImageYUVA420 - if let currentYUVASurface = self.currentYUVASurface { - yuvaSurface = currentYUVASurface - } else { - yuvaSurface = ImageYUVA420(width: dctCoefficients.yPlane.width, height: dctCoefficients.yPlane.height, rowAlignment: nil) - } - - let currentCoefficients: DctCoefficientsYUVA420 - if let current = self.currentCoefficients { - currentCoefficients = current - } else { - currentCoefficients = DctCoefficientsYUVA420(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height) - self.currentCoefficients = currentCoefficients - } - - /*let deltaCoefficients: DctCoefficientsYUVA420 - if let current = self.deltaCoefficients { - deltaCoefficients = current - } else { - deltaCoefficients = DctCoefficientsYUVA420(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height) - self.deltaCoefficients = deltaCoefficients - }*/ - - switch frameType { - case 1: - dctCoefficients.idct8x8(dctData: self.currentDctData, target: yuvaSurface) - yuvaSurface.toCoefficients(target: currentCoefficients) - default: - dctCoefficients.idct4x4Add(dctData: self.currentDctData, target: currentCoefficients) - //currentCoefficients.add(other: deltaCoefficients) - - currentCoefficients.toYUVA420(target: yuvaSurface) - } - - self.currentFrame = CurrentFrame(index: index, duration: self.durationMapping[index], yuva: yuvaSurface) - } catch { - self.currentFrame = nil - self.compressedDataReader = nil - } - - return didLoop - } - - func reset() { - self.currentFrame = nil - } - - func advance(advance: AnimationCacheItem.Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItem.AdvanceResult? { - var didLoop = false - switch advance { - case let .frames(count): - for _ in 0 ..< count { - if self.loadNextFrame() { - didLoop = true - } - } - case let .duration(duration): - var durationOverflow = duration - while true { - if let currentFrame = self.currentFrame { - currentFrame.remainingDuration -= durationOverflow - if currentFrame.remainingDuration <= 0.0 { - durationOverflow = -currentFrame.remainingDuration - if self.loadNextFrame() { - didLoop = true - } - } else { - break - } - } else { - if self.loadNextFrame() { - didLoop = true - } - break - } - } - } - - guard let currentFrame = self.currentFrame else { - return nil - } - - switch requestedFormat { - case .rgba: - let currentSurface = ImageARGB(width: currentFrame.yuva.yPlane.width, height: currentFrame.yuva.yPlane.height, rowAlignment: 32) - currentFrame.yuva.toARGB(target: currentSurface) - - return AnimationCacheItem.AdvanceResult( - frame: AnimationCacheItemFrame(format: .rgba(data: currentSurface.argbPlane.data, width: currentSurface.argbPlane.width, height: currentSurface.argbPlane.height, bytesPerRow: currentSurface.argbPlane.bytesPerRow), duration: currentFrame.duration), - didLoop: didLoop - ) - case .yuva: - return AnimationCacheItem.AdvanceResult( - frame: AnimationCacheItemFrame( - format: .yuva( - y: AnimationCacheItemFrame.Plane( - data: currentFrame.yuva.yPlane.data, - width: currentFrame.yuva.yPlane.width, - height: currentFrame.yuva.yPlane.height, - bytesPerRow: currentFrame.yuva.yPlane.bytesPerRow - ), - u: AnimationCacheItemFrame.Plane( - data: currentFrame.yuva.uPlane.data, - width: currentFrame.yuva.uPlane.width, - height: currentFrame.yuva.uPlane.height, - bytesPerRow: currentFrame.yuva.uPlane.bytesPerRow - ), - v: AnimationCacheItemFrame.Plane( - data: currentFrame.yuva.vPlane.data, - width: currentFrame.yuva.vPlane.width, - height: currentFrame.yuva.vPlane.height, - bytesPerRow: currentFrame.yuva.vPlane.bytesPerRow - ), - a: AnimationCacheItemFrame.Plane( - data: currentFrame.yuva.aPlane.data, - width: currentFrame.yuva.aPlane.width, - height: currentFrame.yuva.aPlane.height, - bytesPerRow: currentFrame.yuva.aPlane.bytesPerRow - ) - ), - duration: currentFrame.duration - ), - didLoop: didLoop - ) - } - } -} - -private func readData(data: Data, offset: Int, count: Int) -> Data { - var result = Data(count: count) - result.withUnsafeMutableBytes { bytes -> Void in - data.withUnsafeBytes { dataBytes -> Void in - memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), count) - } - } - return result -} - -private func readUInt32(data: Data, offset: Int) -> UInt32 { - var value: UInt32 = 0 - withUnsafeMutableBytes(of: &value, { bytes -> Void in - data.withUnsafeBytes { dataBytes -> Void in - memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) - } - }) - - return value -} - -private func readFloat32(data: Data, offset: Int) -> Float32 { - var value: Float32 = 0 - withUnsafeMutableBytes(of: &value, { bytes -> Void in - data.withUnsafeBytes { dataBytes -> Void in - memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) - } - }) - - return value -} - -private func writeUInt32(data: inout Data, value: UInt32) { - var value: UInt32 = value - withUnsafeBytes(of: &value, { bytes -> Void in - data.count += 4 - data.withUnsafeMutableBytes { dataBytes -> Void in - memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) - } - }) -} - -private func writeFloat32(data: inout Data, value: Float32) { - var value: Float32 = value - withUnsafeBytes(of: &value, { bytes -> Void in - data.count += 4 - data.withUnsafeMutableBytes { dataBytes -> Void in - memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) - } - }) -} - -private final class CompressedFileWriter { - enum WriteError: Error { - case generic - } - - private let file: ManagedFile - private let stream: UnsafeMutablePointer - - private let tempOutputBufferSize: Int = 64 * 1024 - private let tempOutputBuffer: UnsafeMutablePointer - private let tempInputBufferCapacity: Int = 64 * 1024 - private let tempInputBuffer: UnsafeMutablePointer - private var tempInputBufferSize: Int = 0 - - private var didFail: Bool = false - - init?(file: ManagedFile) { - self.file = file - - self.stream = UnsafeMutablePointer.allocate(capacity: 1) - guard compression_stream_init(self.stream, COMPRESSION_STREAM_ENCODE, algorithm) != COMPRESSION_STATUS_ERROR else { - self.stream.deallocate() - return nil - } - - self.tempOutputBuffer = UnsafeMutablePointer.allocate(capacity: self.tempOutputBufferSize) - self.tempInputBuffer = UnsafeMutablePointer.allocate(capacity: self.tempInputBufferCapacity) - } - - deinit { - compression_stream_destroy(self.stream) - self.stream.deallocate() - self.tempOutputBuffer.deallocate() - self.tempInputBuffer.deallocate() - } - - private func flushBuffer() throws { - if self.didFail { - throw WriteError.generic - } - - if self.tempInputBufferSize <= 0 { - return - } - - self.stream.pointee.src_ptr = UnsafePointer(self.tempInputBuffer) - self.stream.pointee.src_size = self.tempInputBufferSize - - while true { - self.stream.pointee.dst_ptr = self.tempOutputBuffer - self.stream.pointee.dst_size = self.tempOutputBufferSize - - let status = compression_stream_process(self.stream, 0) - if status == COMPRESSION_STATUS_ERROR { - self.didFail = true - throw WriteError.generic - } - - let writtenBytes = self.tempOutputBufferSize - self.stream.pointee.dst_size - if writtenBytes > 0 { - let _ = self.file.write(self.tempOutputBuffer, count: writtenBytes) - } - - if status == COMPRESSION_STATUS_END { - break - } else { - if self.stream.pointee.src_size == 0 { - break - } - } - } - - self.tempInputBufferSize = 0 - } - - func write(bytes: UnsafePointer, count: Int) throws { - var writtenBytes = 0 - while writtenBytes < count { - let availableBytes = self.tempInputBufferCapacity - self.tempInputBufferSize - if availableBytes == 0 { - try flushBuffer() - } else { - let writeCount = min(availableBytes, count - writtenBytes) - - memcpy(self.tempInputBuffer.advanced(by: self.tempInputBufferSize), bytes.advanced(by: writtenBytes), writeCount) - self.tempInputBufferSize += writeCount - writtenBytes += writeCount - } - } - } - - func flush() throws { - if self.didFail { - throw WriteError.generic - } - - try self.flushBuffer() - - while true { - self.stream.pointee.dst_ptr = self.tempOutputBuffer - self.stream.pointee.dst_size = self.tempOutputBufferSize - - let status = compression_stream_process(self.stream, Int32(COMPRESSION_STREAM_FINALIZE.rawValue)) - if status == COMPRESSION_STATUS_ERROR { - self.didFail = true - throw WriteError.generic - } - - let writtenBytes = self.tempOutputBufferSize - self.stream.pointee.dst_size - if writtenBytes > 0 { - let _ = self.file.write(self.tempOutputBuffer, count: writtenBytes) - } - - if status == COMPRESSION_STATUS_END { - break - } - } - } - - func writeUInt32(_ value: UInt32) throws { - var value: UInt32 = value - try withUnsafeBytes(of: &value, { bytes -> Void in - try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) - }) - } - - func writeFloat32(_ value: Float32) throws { - var value: Float32 = value - try withUnsafeBytes(of: &value, { bytes -> Void in - try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) - }) - } -} - -private final class DecompressedData { - enum ReadError: Error { - case didReadToEnd - } - - private let compressedData: Data - private let dataRange: Range - private let stream: UnsafeMutablePointer - private var isComplete = false - - init?(compressedData: Data, dataRange: Range) { - self.compressedData = compressedData - self.dataRange = dataRange - - self.stream = UnsafeMutablePointer.allocate(capacity: 1) - guard compression_stream_init(self.stream, COMPRESSION_STREAM_DECODE, algorithm) != COMPRESSION_STATUS_ERROR else { - self.stream.deallocate() - return nil - } - - self.compressedData.withUnsafeBytes { bytes in - self.stream.pointee.src_ptr = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: dataRange.lowerBound) - self.stream.pointee.src_size = dataRange.upperBound - dataRange.lowerBound - } - } - - deinit { - compression_stream_destroy(self.stream) - self.stream.deallocate() - } - - func read(bytes: UnsafeMutablePointer, count: Int) throws { - if self.isComplete { - throw ReadError.didReadToEnd - } - - self.stream.pointee.dst_ptr = bytes - self.stream.pointee.dst_size = count - - let status = compression_stream_process(self.stream, 0) - - if status == COMPRESSION_STATUS_ERROR { - self.isComplete = true - throw ReadError.didReadToEnd - } else if status == COMPRESSION_STATUS_END { - if self.stream.pointee.src_size == 0 { - self.isComplete = true - } - } - - if self.stream.pointee.dst_size != 0 { - throw ReadError.didReadToEnd - } - } - - func readUInt32() throws -> UInt32 { - var value: UInt32 = 0 - try withUnsafeMutableBytes(of: &value, { bytes -> Void in - try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) - }) - return value - } - - func readFloat32() throws -> Float32 { - var value: Float32 = 0 - try withUnsafeMutableBytes(of: &value, { bytes -> Void in - try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) - }) - return value - } -} - -private enum LoadItemError: Error { - case dataError -} - -private func loadItem(path: String) throws -> AnimationCacheItem { - guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else { - throw LoadItemError.dataError - } - - var offset: Int = 0 - let dataLength = compressedData.count - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let formatVersion = readUInt32(data: compressedData, offset: offset) - offset += 4 - if formatVersion != 6 { - throw LoadItemError.dataError - } - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let width = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let height = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let dctLumaTableLength = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + Int(dctLumaTableLength) > dataLength { - throw LoadItemError.dataError - } - let dctLumaData = readData(data: compressedData, offset: offset, count: Int(dctLumaTableLength)) - offset += Int(dctLumaTableLength) - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let dctChromaTableLength = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + Int(dctChromaTableLength) > dataLength { - throw LoadItemError.dataError - } - let dctChromaData = readData(data: compressedData, offset: offset, count: Int(dctChromaTableLength)) - offset += Int(dctChromaTableLength) - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let dctDeltaTableLength = readUInt32(data: compressedData, offset: offset) - offset += 4 - - if offset + Int(dctDeltaTableLength) > dataLength { - throw LoadItemError.dataError - } - let dctDeltaData = readData(data: compressedData, offset: offset, count: Int(dctDeltaTableLength)) - offset += Int(dctDeltaTableLength) - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let contentLength = Int(readUInt32(data: compressedData, offset: offset)) - offset += 4 - - let compressedFrameDataRange = offset ..< (offset + contentLength) - offset += contentLength - - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let frameCount = Int(readUInt32(data: compressedData, offset: offset)) - offset += 4 - - var frameMapping: [AnimationCacheItemAccessor.FrameInfo] = [] - for _ in 0 ..< frameCount { - if offset + 4 > dataLength { - throw LoadItemError.dataError - } - let frameDuration = readFloat32(data: compressedData, offset: offset) - offset += 4 - - frameMapping.append(AnimationCacheItemAccessor.FrameInfo(duration: Double(frameDuration))) - } - - guard let dctData = DctData(lumaTable: dctLumaData, chromaTable: dctChromaData, deltaTable: dctDeltaData) else { - throw LoadItemError.dataError - } - - let itemAccessor = AnimationCacheItemAccessor(data: compressedData, range: compressedFrameDataRange, frameMapping: frameMapping, width: Int(width), height: Int(height), dctData: dctData) - - return AnimationCacheItem(numFrames: frameMapping.count, advanceImpl: { advance, requestedFormat in - return itemAccessor.advance(advance: advance, requestedFormat: requestedFormat) - }, resetImpl: { - itemAccessor.reset() - }) -} - -private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { - guard let higherResolutionItem = try? loadItem(path: higherResolutionPath) else { - return nil - } - guard let writer = AnimationCacheItemWriterImpl(queue: currentQueue, allocateTempFile: allocateTempFile, completion: { - _ in - }) else { - return nil - } - - do { - for _ in 0 ..< higherResolutionItem.numFrames { - try writer.addYUV(with: { yuva in - guard let frame = higherResolutionItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: yuva.yPlane.rowAlignment)) else { - return nil - } - switch frame.frame.format { - case .rgba: - return nil - case let .yuva(y, u, v, a): - yuva.yPlane.copyScaled(fromPlane: y) - yuva.uPlane.copyScaled(fromPlane: u) - yuva.vPlane.copyScaled(fromPlane: v) - yuva.aPlane.copyScaled(fromPlane: a) - } - - return frame.frame.duration - }, proposedWidth: width, proposedHeight: height, insertKeyframe: true) - } - - guard let result = try writer.finishInternal() else { - return nil - } - guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { - return nil - } - let _ = try? FileManager.default.removeItem(atPath: itemPath) - guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { - return nil - } - if let size = fileSize(itemPath) { - updateStorageStats(itemPath, size) - } - - guard let item = try? loadItem(path: itemPath) else { - return nil - } - return item - } catch { - return nil - } -} - -private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, animationItemPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> Bool { - guard let animationItem = try? loadItem(path: animationItemPath) else { - return false - } - guard let writer = AnimationCacheItemWriterImpl(queue: currentQueue, allocateTempFile: allocateTempFile, completion: { _ in - }) else { - return false - } - - do { - for _ in 0 ..< min(1, animationItem.numFrames) { - guard let frame = animationItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: 1)) else { - return false - } - switch frame.frame.format { - case .rgba: - return false - case let .yuva(y, u, v, a): - try writer.addYUV(with: { yuva in - assert(yuva.yPlane.bytesPerRow == y.bytesPerRow) - assert(yuva.uPlane.bytesPerRow == u.bytesPerRow) - assert(yuva.vPlane.bytesPerRow == v.bytesPerRow) - assert(yuva.aPlane.bytesPerRow == a.bytesPerRow) - - yuva.yPlane.copyScaled(fromPlane: y) - yuva.uPlane.copyScaled(fromPlane: u) - yuva.vPlane.copyScaled(fromPlane: v) - yuva.aPlane.copyScaled(fromPlane: a) - - return frame.frame.duration - }, proposedWidth: y.width, proposedHeight: y.height, insertKeyframe: true) - } - } - - guard let result = try writer.finishInternal() else { - return false - } - - let _ = try? FileManager.default.removeItem(atPath: itemPath) - guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { - return false - } - if let size = fileSize(itemPath) { - updateStorageStats(itemPath, size) - } - return true - } catch { - return false - } -} - -private func findHigherResolutionFileForAdaptation(itemDirectoryPath: String, baseName: String, baseSuffix: String, width: Int, height: Int) -> String? { - var candidates: [(path: String, width: Int, height: Int)] = [] - if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: itemDirectoryPath), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants, errorHandler: nil) { - for url in enumerator { - guard let url = url as? URL else { - continue - } - let fileName = url.lastPathComponent - if fileName.hasPrefix(baseName) { - let scanner = Scanner(string: fileName) - guard scanner.scanString(baseName) != nil else { - continue - } - guard let itemWidth = scanner.scanInt() else { - continue - } - guard scanner.scanString("x") != nil else { - continue - } - guard let itemHeight = scanner.scanInt() else { - continue - } - if !baseSuffix.isEmpty { - guard scanner.scanString(baseSuffix) != nil else { - continue - } - } - guard scanner.isAtEnd else { - continue - } - if itemWidth > width && itemHeight > height { - candidates.append((url.path, itemWidth, itemHeight)) - } - } - } - } - if !candidates.isEmpty { - candidates.sort(by: { $0.width < $1.width }) - return candidates[0].path - } - return nil -} - -public final class AnimationCacheImpl: AnimationCache { - private final class Impl { - private struct ItemKey: Hashable { - var id: String - var width: Int - var height: Int - } - - private final class ItemContext { - let subscribers = Bag<(AnimationCacheItemResult) -> Void>() - let disposable = MetaDisposable() - - deinit { - self.disposable.dispose() - } - } - - private let queue: Queue - private let basePath: String - private let allocateTempFile: () -> String - private let updateStorageStats: (String, Int64) -> Void - - private let fetchQueues: [Queue] - private var nextFetchQueueIndex: Int = 0 - - private var itemContexts: [ItemKey: ItemContext] = [:] - - init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { - self.queue = queue - - let fetchQueueCount: Int - if ProcessInfo.processInfo.processorCount > 2 { - fetchQueueCount = 3 - } else { - fetchQueueCount = 2 - } - - self.fetchQueues = (0 ..< fetchQueueCount).map { i in Queue(name: "AnimationCacheImpl-Fetch\(i)", qos: .default) } - self.basePath = basePath - self.allocateTempFile = allocateTempFile - self.updateStorageStats = updateStorageStats - } - - deinit { - } - - func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { - let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) - let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" - let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" - let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - - if FileManager.default.fileExists(atPath: itemPath), let item = try? loadItem(path: itemPath) { - updateResult(AnimationCacheItemResult(item: item, isFinal: true)) - - return EmptyDisposable - } - let key = ItemKey(id: sourceId, width: Int(size.width), height: Int(size.height)) - - let itemContext: ItemContext - var beginFetch = false - if let current = self.itemContexts[key] { - itemContext = current - } else { - itemContext = ItemContext() - self.itemContexts[key] = itemContext - beginFetch = true - } - - let queue = self.queue - let index = itemContext.subscribers.add(updateResult) - - updateResult(AnimationCacheItemResult(item: nil, isFinal: false)) - - if beginFetch { - let fetchQueueIndex = self.nextFetchQueueIndex - self.nextFetchQueueIndex += 1 - let allocateTempFile = self.allocateTempFile - let updateStorageStats = self.updateStorageStats - guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in - queue.async { - guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { - return - } - - strongSelf.itemContexts.removeValue(forKey: key) - - guard let result = result else { - return - } - guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { - return - } - let _ = try? FileManager.default.removeItem(atPath: itemPath) - guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { - return - } - if let size = fileSize(itemPath) { - updateStorageStats(itemPath, size) - } - - let _ = generateFirstFrameFromItem(currentQueue: queue, itemPath: itemFirstFramePath, animationItemPath: itemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) - - for f in itemContext.subscribers.copyItems() { - guard let item = try? loadItem(path: itemPath) else { - continue - } - f(AnimationCacheItemResult(item: item, isFinal: true)) - } - } - }) else { - return EmptyDisposable - } - - let fetchDisposable = MetaDisposable() - fetchDisposable.set(fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: false))) - - itemContext.disposable.set(ActionDisposable { [weak writer] in - if let writer = writer { - writer.isCancelled = true - } - - fetchDisposable.dispose() - }) - } - - return ActionDisposable { [weak self, weak itemContext] in - queue.async { - guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { - return - } - itemContext.subscribers.remove(index) - if itemContext.subscribers.isEmpty { - itemContext.disposable.dispose() - strongSelf.itemContexts.removeValue(forKey: key) - } - } - } - } - - static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { - let hashString = md5Hash(sourceId) - let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) - let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" - let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - - if FileManager.default.fileExists(atPath: itemFirstFramePath) { - if let item = try? loadItem(path: itemFirstFramePath) { - return item - } - } - - if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { - if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { - return adaptedItem - } - } - - return nil - } - - static func getFirstFrame(queue: Queue, basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { - let hashString = md5Hash(sourceId) - let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) - let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" - let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - - if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = try? loadItem(path: itemFirstFramePath) { - completion(AnimationCacheItemResult(item: item, isFinal: true)) - return EmptyDisposable - } - - if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { - if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { - completion(AnimationCacheItemResult(item: adaptedItem, isFinal: true)) - return EmptyDisposable - } - } - - if let fetch = fetch { - completion(AnimationCacheItemResult(item: nil, isFinal: false)) - - guard let writer = AnimationCacheItemWriterImpl(queue: queue, allocateTempFile: allocateTempFile, completion: { result in - queue.async { - guard let result = result else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return - } - guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return - } - let _ = try? FileManager.default.removeItem(atPath: itemFirstFramePath) - guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemFirstFramePath) else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return - } - if let size = fileSize(itemFirstFramePath) { - updateStorageStats(itemFirstFramePath, size) - } - guard let item = try? loadItem(path: itemFirstFramePath) else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return - } - - completion(AnimationCacheItemResult(item: item, isFinal: true)) - } - }) else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return EmptyDisposable - } - - let fetchDisposable = fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: true)) - return fetchDisposable - } else { - completion(AnimationCacheItemResult(item: nil, isFinal: true)) - return EmptyDisposable - } - } - } - - private let queue: Queue - private let basePath: String - private let impl: QueueLocalObject - private let allocateTempFile: () -> String - private let updateStorageStats: (String, Int64) -> Void - - public init(basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { - let queue = Queue() - self.queue = queue - self.basePath = basePath - self.allocateTempFile = allocateTempFile - self.updateStorageStats = updateStorageStats - self.impl = QueueLocalObject(queue: queue, generate: { - return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) - }) - } - - public func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Signal { - return Signal { subscriber in - let disposable = MetaDisposable() - - self.impl.with { impl in - disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { result in - subscriber.putNext(result) - if result.isFinal { - subscriber.putCompletion() - } - })) - } - - return disposable - } - |> runOn(self.queue) - } - - public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { - return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size, allocateTempFile: self.allocateTempFile, updateStorageStats: self.updateStorageStats) - } - - public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { - let disposable = MetaDisposable() - - let basePath = self.basePath - let allocateTempFile = self.allocateTempFile - let updateStorageStats = self.updateStorageStats - queue.async { - disposable.set(Impl.getFirstFrame(queue: queue, basePath: basePath, sourceId: sourceId, size: size, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats, fetch: fetch, completion: completion)) - } - - return disposable - } -} diff --git a/submodules/TelegramUI/Components/AsyncListComponent/Sources/AsyncListComponent.swift b/submodules/TelegramUI/Components/AsyncListComponent/Sources/AsyncListComponent.swift index cd85867620..cc9cf65b6a 100644 --- a/submodules/TelegramUI/Components/AsyncListComponent/Sources/AsyncListComponent.swift +++ b/submodules/TelegramUI/Components/AsyncListComponent/Sources/AsyncListComponent.swift @@ -576,7 +576,7 @@ public final class AsyncListComponent: Component { case let .curve(duration, curve): updateSizeAndInsets.duration = duration switch curve { - case .linear, .easeInOut: + case .linear, .easeInOut, .easeIn: updateSizeAndInsets.curve = .Default(duration: duration) case .spring: updateSizeAndInsets.curve = .Spring(duration: duration) diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift index d69c236735..2d8cc47b63 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift @@ -629,7 +629,7 @@ public func makeAttachmentFileControllerImpl( if let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { peer in - guard let peer, let peerReference = PeerReference(peer._asPeer()) else { + guard let peer, let peerReference = PeerReference(peer) else { return } send([.savedMusic(peer: peerReference, media: file)], false, nil, nil) @@ -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, groupId: nil, tags: [.music], minDate: nil, maxDate: nil, folderId: nil), query: "", state: nil) |> map { result -> [Message]? in return result.0.messages } @@ -981,7 +981,7 @@ public func makeAttachmentFileControllerImpl( return .single(result) } ).start(next: { peer, remoteMessages in - guard let peer, let peerReference = PeerReference(peer._asPeer()) else { + guard let peer, let peerReference = PeerReference(peer) else { return } var messageMap: [MessageId: Message] = [:] diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift index f3edb35f99..6784fce307 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, groupId: nil, 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/AuthConfirmationScreen/BUILD b/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD index 4136ffb3e3..12bc07b85a 100644 --- a/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD +++ b/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index 00063fed48..19df7c4f3d 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -165,6 +165,7 @@ final class AvatarEditorScreenComponent: Component { var id: AnyHashable var version: Int var isPreset: Bool + var canLoadMore: Bool } private struct EmojiSearchState { @@ -211,6 +212,7 @@ final class AvatarEditorScreenComponent: Component { private var data: AvatarKeyboardInputData? private let emojiSearchDisposable = MetaDisposable() + private var stickerSearchContext: StickerSearchContext? private let emojiSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) { didSet { @@ -273,156 +275,173 @@ final class AvatarEditorScreenComponent: Component { switch query { case .none: + self.stickerSearchContext = nil self.emojiSearchDisposable.set(nil) - self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) case let .text(rawQuery, languageCode): let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) if query.isEmpty { + self.stickerSearchContext = nil self.emojiSearchDisposable.set(nil) - self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) } else { - var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) - if !languageCode.lowercased().hasPrefix("en") { - signal = signal - |> mapToSignal { keywords in - return .single(keywords) - |> then( - context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3) - |> map { englishKeywords in - return keywords + englishKeywords + self.stickerSearchContext = nil + + let emojiItemsSignal = context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) + |> take(1) + + let buildGroups: (ItemCollectionsView, [String: String], StickerSearchContext.State, StickerSearchContext?) -> (groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: StickerSearchContext?) = { view, allEmoticons, stickerState, searchContext in + let hasPremium = true + + var emoji: [(String, TelegramMediaFile.Accessor?, String)] = [] + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + if let alt = item.file.customEmojiAlt { + if !item.file.isPremiumEmoji || hasPremium { + if !alt.isEmpty, let keyword = allEmoticons[alt] { + emoji.append((alt, item.file, keyword)) + } else if alt == query { + emoji.append((alt, item.file, alt)) + } } + } + } + + var emojiItems: [EmojiPagerContentComponent.Item] = [] + var existingIds = Set() + for item in emoji { + if let itemFile = item.1 { + if existingIds.contains(itemFile.fileId) { + continue + } + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, + subgroupId: nil, + icon: .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + emojiItems.append(item) + } + } + + var stickerItems: [EmojiPagerContentComponent.Item] = [] + for sticker in stickerState.items { + if existingIds.contains(sticker.file.fileId) { + continue + } + existingIds.insert(sticker.file.fileId) + let animationData = EntityKeyboardAnimationData(file: TelegramMediaFile.Accessor(sticker.file)) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: TelegramMediaFile.Accessor(sticker.file), + subgroupId: nil, + icon: .none, + tintMode: .none + ) + stickerItems.append(item) + } + + var result: [EmojiPagerContentComponent.ItemGroup] = [] + if !emojiItems.isEmpty { + result.append( + EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "emoji", + title: "Emoji", + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: emojiItems + ) ) } + if !stickerItems.isEmpty { + result.append( + EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "stickers", + title: "Stickers", + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: stickerItems + ) + ) + } + return (result, stickerState.canLoadMore, stickerState.items.isEmpty && stickerState.isLoadingMore, searchContext) } - - let resultSignal = signal - |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in - return combineLatest( - context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1), - combineLatest(keywords.map { context.engine.stickers.searchStickers(query: query, emoticon: $0.emoticons, inputLanguageCode: languageCode) - |> map { items -> [FoundStickerItem] in - return items.items + + let resultSignal: Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: StickerSearchContext?), NoError> + if query.isSingleEmoji { + let allEmoticons = [query.basicEmoji.0: query.basicEmoji.0] + let searchContext = context.engine.stickers.stickerSearchContext(query: nil, emoticon: [query.basicEmoji.0], inputLanguageCode: languageCode) + resultSignal = combineLatest(emojiItemsSignal, searchContext.state) + |> map { view, stickerState in + return buildGroups(view, allEmoticons, stickerState, searchContext) + } + } else { + var keywordsSignal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) + if !languageCode.lowercased().hasPrefix("en") { + keywordsSignal = keywordsSignal + |> mapToSignal { keywords in + return .single(keywords) + |> then( + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3) + |> map { englishKeywords in + return keywords + englishKeywords + } + ) } - }) - ) - |> map { view, stickers -> [EmojiPagerContentComponent.ItemGroup] in - let hasPremium = true - - var emoji: [(String, TelegramMediaFile.Accessor?, String)] = [] - - var existingEmoticons = Set() + } + + resultSignal = keywordsSignal + |> mapToSignal { keywords -> Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: StickerSearchContext?), NoError> in var allEmoticons: [String: String] = [:] for keyword in keywords { for emoticon in keyword.emoticons { allEmoticons[emoticon] = keyword.keyword - existingEmoticons.insert(emoticon) } } - for entry in view.entries { - guard let item = entry.item as? StickerPackItem else { - continue - } - if let alt = item.file.customEmojiAlt { - if !item.file.isPremiumEmoji || hasPremium { - if !alt.isEmpty, let keyword = allEmoticons[alt] { - emoji.append((alt, item.file, keyword)) - } else if alt == query { - emoji.append((alt, item.file, alt)) - } - } + let emoticon = Array(allEmoticons.keys) + guard !emoticon.isEmpty else { + return combineLatest(emojiItemsSignal, .single(StickerSearchContext.State(items: [], canLoadMore: false, isLoadingMore: false))) + |> map { view, stickerState in + return buildGroups(view, allEmoticons, stickerState, nil) } } - var emojiItems: [EmojiPagerContentComponent.Item] = [] - - var existingIds = Set() - for item in emoji { - if let itemFile = item.1 { - if existingIds.contains(itemFile.fileId) { - continue - } - existingIds.insert(itemFile.fileId) - let animationData = EntityKeyboardAnimationData(file: itemFile) - let item = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: itemFile, subgroupId: nil, - icon: .none, - tintMode: animationData.isTemplate ? .primary : .none - ) - emojiItems.append(item) - } + let searchContext = context.engine.stickers.stickerSearchContext(query: query, emoticon: emoticon, inputLanguageCode: languageCode) + return combineLatest(emojiItemsSignal, searchContext.state) + |> map { view, stickerState in + return buildGroups(view, allEmoticons, stickerState, searchContext) } - - var stickerItems: [EmojiPagerContentComponent.Item] = [] - for stickerResult in stickers { - for sticker in stickerResult { - if existingIds.contains(sticker.file.fileId) { - continue - } - - existingIds.insert(sticker.file.fileId) - let animationData = EntityKeyboardAnimationData(file: TelegramMediaFile.Accessor(sticker.file)) - let item = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: TelegramMediaFile.Accessor(sticker.file), - subgroupId: nil, - icon: .none, - tintMode: .none - ) - stickerItems.append(item) - } - } - - var result: [EmojiPagerContentComponent.ItemGroup] = [] - if !emojiItems.isEmpty { - result.append( - EmojiPagerContentComponent.ItemGroup( - supergroupId: "search", - groupId: "emoji", - title: "Emoji", - subtitle: nil, - badge: nil, - actionButtonTitle: nil, - isFeatured: false, - isPremiumLocked: false, - isEmbedded: false, - hasClear: false, - hasEdit: false, - collapsedLineCount: nil, - displayPremiumBadges: false, - headerItem: nil, - fillWithLoadingPlaceholders: false, - items: emojiItems - ) - ) - } - if !stickerItems.isEmpty { - result.append( - EmojiPagerContentComponent.ItemGroup( - supergroupId: "search", - groupId: "stickers", - title: "Stickers", - subtitle: nil, - badge: nil, - actionButtonTitle: nil, - isFeatured: false, - isPremiumLocked: false, - isEmbedded: false, - hasClear: false, - hasEdit: false, - collapsedLineCount: nil, - displayPremiumBadges: false, - headerItem: nil, - fillWithLoadingPlaceholders: false, - items: stickerItems - ) - ) - } - return result } } @@ -435,11 +454,13 @@ final class AvatarEditorScreenComponent: Component { return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + self.stickerSearchContext = result.searchContext + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.groups, id: AnyHashable(query), version: version, isPreset: false, canLoadMore: result.canLoadMore), isSearching: result.isSearching) version += 1 })) } case let .category(value): + self.stickerSearchContext = nil let resultSignal = context.engine.stickers.searchEmoji(category: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -515,11 +536,11 @@ final class AvatarEditorScreenComponent: Component { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) version += 1 })) } @@ -559,7 +580,7 @@ final class AvatarEditorScreenComponent: Component { if installed { return .complete() } else { - return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) + return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) |> map { _ in return Void() } } case .fetching: break @@ -648,6 +669,9 @@ final class AvatarEditorScreenComponent: Component { } } }, + loadMore: { [weak self] in + self?.stickerSearchContext?.loadMore() + }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, @@ -694,7 +718,7 @@ final class AvatarEditorScreenComponent: Component { if installed { return .complete() } else { - return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) + return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) |> map { _ in return Void() } } case .fetching: break @@ -779,6 +803,9 @@ final class AvatarEditorScreenComponent: Component { } } }, + loadMore: { [weak self] in + self?.stickerSearchContext?.loadMore() + }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, @@ -919,17 +946,24 @@ final class AvatarEditorScreenComponent: Component { if let searchResult = emojiSearchState.result { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? - if !searchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { + if !emojiSearchState.isSearching && !searchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( text: presentationData.strings.EmojiSearch_SearchEmojiEmptyResult, iconFile: nil ) } + let searchState: EmojiPagerContentComponent.SearchState = emojiSearchState.isSearching ? .searching : .active if state?.keyboardContentId == AnyHashable("emoji") { - data.emoji = data.emoji.withUpdatedItemGroups(panelItemGroups: data.emoji.panelItemGroups, contentItemGroups: searchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: searchResult.id, version: searchResult.version), emptySearchResults: emptySearchResults, searchState: .active) + data.emoji = data.emoji.withUpdatedItemGroups(panelItemGroups: data.emoji.panelItemGroups, contentItemGroups: searchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: searchResult.id, version: searchResult.version), emptySearchResults: emptySearchResults, searchState: searchState, canLoadMore: searchResult.canLoadMore) } else { - data.stickers = data.stickers?.withUpdatedItemGroups(panelItemGroups: data.stickers?.panelItemGroups ?? searchResult.groups, contentItemGroups: searchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: searchResult.id, version: searchResult.version), emptySearchResults: emptySearchResults, searchState: .active) + data.stickers = data.stickers?.withUpdatedItemGroups(panelItemGroups: data.stickers?.panelItemGroups ?? searchResult.groups, contentItemGroups: searchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: searchResult.id, version: searchResult.version), emptySearchResults: emptySearchResults, searchState: searchState, canLoadMore: searchResult.canLoadMore) + } + } else if emojiSearchState.isSearching { + if state?.keyboardContentId == AnyHashable("emoji") { + data.emoji = data.emoji.withUpdatedItemGroups(panelItemGroups: data.emoji.panelItemGroups, contentItemGroups: data.emoji.contentItemGroups, itemContentUniqueId: data.emoji.itemContentUniqueId, emptySearchResults: data.emoji.emptySearchResults, searchState: .searching) + } else { + data.stickers = data.stickers?.withUpdatedItemGroups(panelItemGroups: data.stickers?.panelItemGroups ?? [], contentItemGroups: data.stickers?.contentItemGroups ?? [], itemContentUniqueId: data.stickers?.itemContentUniqueId, emptySearchResults: data.stickers?.emptySearchResults, searchState: .searching) } } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarPreviewComponent.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarPreviewComponent.swift index 42c959c8a7..45256d2787 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarPreviewComponent.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarPreviewComponent.swift @@ -11,7 +11,6 @@ import AccountContext import TelegramCore import MultilineTextComponent import EmojiStatusComponent -import Postbox import AnimatedStickerNode import TelegramAnimatedStickerNode import StickerResources diff --git a/submodules/TelegramUI/Components/AvatarUploadToastScreen/BUILD b/submodules/TelegramUI/Components/AvatarUploadToastScreen/BUILD index a1ebb83491..cfb18fecd6 100644 --- a/submodules/TelegramUI/Components/AvatarUploadToastScreen/BUILD +++ b/submodules/TelegramUI/Components/AvatarUploadToastScreen/BUILD @@ -14,7 +14,6 @@ swift_library( "//submodules/TelegramPresentationData", "//submodules/ComponentFlow", "//submodules/Components/ComponentDisplayAdapters", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Components/ViewControllerComponent", diff --git a/submodules/TelegramUI/Components/BatchVideoRendering/Sources/BatchVideoRenderingContext.swift b/submodules/TelegramUI/Components/BatchVideoRendering/Sources/BatchVideoRenderingContext.swift index cbb7053aa4..ac4810831b 100644 --- a/submodules/TelegramUI/Components/BatchVideoRendering/Sources/BatchVideoRenderingContext.swift +++ b/submodules/TelegramUI/Components/BatchVideoRendering/Sources/BatchVideoRenderingContext.swift @@ -205,12 +205,12 @@ public final class BatchVideoRenderingContext { ).startStrict() } if targetContext.dataDisposable == nil { - targetContext.dataDisposable = (self.context.account.postbox.mediaBox.resourceData(targetContext.file.media.resource) + targetContext.dataDisposable = (self.context.engine.resources.data(resource: EngineMediaResource(targetContext.file.media.resource)) |> deliverOnMainQueue).startStrict(next: { [weak self, weak targetContext] data in guard let self, let targetContext else { return } - if data.complete && targetContext.dataPath == nil { + if data.isComplete && targetContext.dataPath == nil { targetContext.dataPath = data.path self.update() } diff --git a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift index 8a14bf06a1..2b28a3c333 100644 --- a/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift +++ b/submodules/TelegramUI/Components/ButtonComponent/Sources/ButtonComponent.swift @@ -282,7 +282,7 @@ public final class ButtonTextContentComponent: Component { } if let badgeSize, let badge = self.badge { - let badgeFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width + badgeSpacing, y: floorToScreenPixels((size.height - badgeSize.height) * 0.5) + 1.0), size: badgeSize) + let badgeFrame = CGRect(origin: CGPoint(x: contentFrame.minX + contentSize.width + badgeSpacing, y: floorToScreenPixels((size.height - badgeSize.height) * 0.5) + UIScreenPixel), size: badgeSize) if let badgeView = badge.view { var animateIn = false @@ -461,7 +461,11 @@ public final class ButtonComponent: Component { private var containerView: UIView private var glassContainerView: GlassBackgroundView? + private var glassShadowView: UIImageView? + private var glassShadowCornerRadius: CGFloat? + private var glassHighlightContainerView: UIView? private let button: HighlightTrackingButton + private let glassHighlightRecognizer: GlassHighlightGestureRecognizer private var shimmeringView: ButtonShimmeringView? private var chromeView: UIImageView? @@ -476,6 +480,7 @@ public final class ButtonComponent: Component { self.containerView.isUserInteractionEnabled = false self.button = HighlightTrackingButton() + self.glassHighlightRecognizer = GlassHighlightGestureRecognizer(target: nil, action: nil) super.init(frame: frame) @@ -484,6 +489,8 @@ public final class ButtonComponent: Component { self.addSubview(self.containerView) self.addSubview(self.button) + self.addGestureRecognizer(self.glassHighlightRecognizer) + self.glassHighlightRecognizer.isEnabled = false self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) @@ -495,28 +502,6 @@ public final class ButtonComponent: Component { self.button.highligthedChanged = { [weak self] highlighted in if let self, let component = self.component, component.isEnabled { switch component.background.style { - case .glass: - let transition = ComponentTransition(animation: .curve(duration: highlighted ? 0.25 : 0.35, curve: .spring)) - if highlighted { - self.layer.shouldRasterize = true - - let highlightedColor = component.background.color.withMultiplied(hue: 1.0, saturation: 0.77, brightness: 1.01) - transition.setBackgroundColor(view: self.containerView, color: highlightedColor) - transition.setScale(view: self.containerView, scale: 1.05, completion: { finished in - if finished { - self.layer.shouldRasterize = false - } - }) - } else { - self.layer.shouldRasterize = true - - transition.setBackgroundColor(view: self.containerView, color: component.background.color) - transition.setScale(view: self.containerView, scale: 1.0, completion: { finished in - if finished { - self.layer.shouldRasterize = false - } - }) - } case .legacy: if highlighted { self.containerView.layer.removeAnimation(forKey: "opacity") @@ -536,6 +521,74 @@ public final class ButtonComponent: Component { preconditionFailure() } + private func removeGlassEffect(transition: ComponentTransition) { + self.glassHighlightRecognizer.isEnabled = false + self.glassHighlightRecognizer.highlightContainerView = nil + + if let glassShadowView = self.glassShadowView, glassShadowView.superview != nil { + if transition.animation.isImmediate { + glassShadowView.removeFromSuperview() + } else { + transition.setAlpha(view: glassShadowView, alpha: 0.0, completion: { _ in + glassShadowView.removeFromSuperview() + }) + } + } + if let glassHighlightContainerView = self.glassHighlightContainerView, glassHighlightContainerView.superview != nil { + glassHighlightContainerView.removeFromSuperview() + } + self.glassShadowCornerRadius = nil + + self.layer.removeAnimation(forKey: "sublayerTransform") + self.layer.sublayerTransform = CATransform3DIdentity + } + + private func updateGlassEffect(component: ButtonComponent, size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) { + let shadowInset: CGFloat = 48.0 + + let glassShadowView: UIImageView + if let current = self.glassShadowView { + glassShadowView = current + } else { + glassShadowView = UIImageView() + glassShadowView.isUserInteractionEnabled = false + self.glassShadowView = glassShadowView + } + if glassShadowView.superview == nil { + self.insertSubview(glassShadowView, at: 0) + } else { + self.sendSubviewToBack(glassShadowView) + } + if self.glassShadowCornerRadius != cornerRadius || glassShadowView.image == nil { + glassShadowView.image = GlassBackgroundView.generateLegacyShadowImage(cornerRadius: cornerRadius, shadowInset: shadowInset, shadowIntensity: 0.18, shadowBlur: 64.0) + self.glassShadowCornerRadius = cornerRadius + } + transition.setFrame(view: glassShadowView, frame: CGRect(origin: .zero, size: size).insetBy(dx: -shadowInset, dy: -shadowInset)) + transition.setAlpha(view: glassShadowView, alpha: 1.0) + + let glassHighlightContainerView: UIView + if let current = self.glassHighlightContainerView { + glassHighlightContainerView = current + } else { + glassHighlightContainerView = UIView() + glassHighlightContainerView.isUserInteractionEnabled = false + glassHighlightContainerView.clipsToBounds = true + self.glassHighlightContainerView = glassHighlightContainerView + } + if glassHighlightContainerView.superview == nil { + self.insertSubview(glassHighlightContainerView, aboveSubview: self.containerView) + } else if self.button.superview === self { + self.insertSubview(glassHighlightContainerView, belowSubview: self.button) + } else { + self.bringSubviewToFront(glassHighlightContainerView) + } + transition.setFrame(view: glassHighlightContainerView, frame: CGRect(origin: .zero, size: size)) + transition.setCornerRadius(layer: glassHighlightContainerView.layer, cornerRadius: cornerRadius) + + self.glassHighlightRecognizer.highlightContainerView = glassHighlightContainerView + self.glassHighlightRecognizer.isEnabled = component.isEnabled && !component.displaysProgress + } + @objc private func pressed() { guard let component = self.component else { return @@ -641,6 +694,12 @@ public final class ButtonComponent: Component { transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: cornerRadius) } + if component.background.style == .glass, component.background.color.alpha > 1.0 - .ulpOfOne { + self.updateGlassEffect(component: component, size: size, cornerRadius: cornerRadius, transition: transition) + } else { + self.removeGlassEffect(transition: transition) + } + if let contentView = contentItem.view.view { var animateIn = false var contentTransition = transition diff --git a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Metal/VoiceChatActionButtonShaders.metal b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Metal/VoiceChatActionButtonShaders.metal index 8834ab1672..0ccd808798 100644 --- a/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Metal/VoiceChatActionButtonShaders.metal +++ b/submodules/TelegramUI/Components/Calls/VoiceChatActionButton/Metal/VoiceChatActionButtonShaders.metal @@ -7,15 +7,6 @@ struct Rectangle { float2 size; }; -constant static float2 quadVertices[6] = { - float2(0.0, 0.0), - float2(1.0, 0.0), - float2(0.0, 1.0), - float2(1.0, 0.0), - float2(0.0, 1.0), - float2(1.0, 1.0) -}; - struct QuadVertexOut { float4 position [[position]]; float2 uv; diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 456d01e1fc..737457540a 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1957,7 +1957,7 @@ private final class CameraScreenComponent: CombinedComponent { availableSize: CGSize(width: 40.0, height: 40.0), transition: .immediate ) - if component.cameraState.isCollageEnabled { + if state.displayingCollageSelection { nextButtonX += 48.0 } var collageButtonX = nextButtonX diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift index c1d2974745..c06460e8dc 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift @@ -34,7 +34,7 @@ final class CameraVideoSource: VideoSource { let index = self.onUpdatedListeners.add(f) return ActionDisposable { [weak self] in - DispatchQueue.main.async { + Queue.mainQueue().async { guard let self else { return } @@ -252,7 +252,7 @@ final class LiveStreamMediaSource { let index = self.onVideoUpdatedListeners.add(f) return ActionDisposable { [weak self] in - DispatchQueue.main.async { + Queue.mainQueue().async { guard let self else { return } diff --git a/submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/BUILD b/submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/BUILD index c60929eefc..00c812385b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/Sources/ChatAgeRestrictionAlertController.swift b/submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/Sources/ChatAgeRestrictionAlertController.swift index cf87df8389..8e4bb123d8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/Sources/ChatAgeRestrictionAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController/Sources/ChatAgeRestrictionAlertController.swift @@ -3,7 +3,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import AccountContext diff --git a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/BUILD index 01012aac94..9d60cdcca9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/SSignalKit/SwiftSignalKit", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/AvatarNode", "//submodules/ContextUI", diff --git a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift index 4687fad8fe..27b644ffa8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode/Sources/ChatAvatarNavigationNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import SwiftSignalKit -import Postbox import TelegramCore import AvatarNode import ContextUI @@ -113,7 +112,18 @@ public final class ChatAvatarNavigationNode: ASDisplayNode { self.avatarNode.isHidden = true } - public func setPeer(context: AccountContext, theme: PresentationTheme, peer: EnginePeer?, authorOfMessage: MessageReference? = nil, overrideImage: AvatarNodeImageOverride? = nil, emptyColor: UIColor? = nil, clipStyle: AvatarNodeClipStyle = .round, synchronousLoad: Bool = false, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), storeUnrounded: Bool = false) { + public func setPeer( + context: AccountContext, + theme: PresentationTheme, + peer: EnginePeer?, + authorOfMessage: MessageReference? = nil, + overrideImage: AvatarNodeImageOverride? = nil, + emptyColor: UIColor? = nil, + clipStyle: AvatarNodeClipStyle = .round, + synchronousLoad: Bool = false, + displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), + storeUnrounded: Bool = false + ) { self.context = context if let statusComponentView = self.statusView.view { @@ -163,6 +173,10 @@ public final class ChatAvatarNavigationNode: ASDisplayNode { profilePhoto = maybePhoto isKnown = true } + if profilePhoto == nil, case let .known(maybePhoto) = cachedPeerData.fallbackPhoto { + profilePhoto = maybePhoto + isKnown = true + } } if isKnown { diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift index 6c92a201d3..053026ef6c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -174,7 +174,7 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { private var presentationInterfaceState: ChatPresentationInterfaceState? - private var layoutData: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, CGFloat, Bool, LayoutMetrics)? + private var layoutData: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, CGFloat, Bool, LayoutMetrics, DeviceMetrics)? public override init() { super.init() @@ -214,8 +214,8 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { switch action { case .join, .joinGroup, .applyToJoin: self.isJoining = true - if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, isSecondary, metrics) = self.layoutData, let presentationInterfaceState = self.presentationInterfaceState { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, force: true) + if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, isSecondary, metrics, deviceMetrics) = self.layoutData, let presentationInterfaceState = self.presentationInterfaceState { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, deviceMetrics: deviceMetrics, force: true) } self.actionDisposable.set((context.peerChannelMemberCategoriesContextsManager.join(engine: context.engine, peerId: peer.id, hash: nil) |> afterDisposed { [weak self] in @@ -292,8 +292,8 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { } } - override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { - return self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, force: false) + override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) -> CGFloat { + return self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, deviceMetrics: deviceMetrics, force: false) } private var displayedGiftOrSuggestTooltip = false @@ -389,9 +389,9 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { }) } - private func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, force: Bool) -> CGFloat { + private func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, force: Bool) -> CGFloat { let isFirstTime = self.layoutData == nil - self.layoutData = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, isSecondary, metrics) + self.layoutData = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, isSecondary, metrics, deviceMetrics) var transition = transition if !isFirstTime && !transition.isAnimated { @@ -433,10 +433,9 @@ public final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { var leftInset = leftInset + 8.0 var rightInset = rightInset + 8.0 - if bottomInset <= 32.0 { - leftInset += 18.0 - rightInset += 18.0 - } + let compactBottomSideInset = self.compactBottomSideInset(bottomInset: bottomInset, deviceMetrics: deviceMetrics) + leftInset += compactBottomSideInset + rightInset += compactBottomSideInset var leftPanelItems: [GlassControlGroupComponent.Item] = [] if displaySuggestPost { diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index bfee82ed6b..79f11f91bb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -297,177 +297,6 @@ public final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNod } } -public final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerContentNode, ChatEmptyNodeContent, ASGestureRecognizerDelegate { - private let context: AccountContext - private let interaction: ChatPanelInterfaceInteraction? - - private let titleNode: ImmediateTextNode - private let textNode: ImmediateTextNode - - private var stickerItem: ChatMediaInputStickerGridItem? - public let stickerNode: ChatMediaInputStickerGridItemNode - - private var currentTheme: PresentationTheme? - private var currentStrings: PresentationStrings? - - private var didSetupSticker = false - private let disposable = MetaDisposable() - - public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { - self.context = context - self.interaction = interaction - - self.titleNode = ImmediateTextNode() - self.titleNode.maximumNumberOfLines = 0 - self.titleNode.lineSpacing = 0.15 - self.titleNode.textAlignment = .center - self.titleNode.isUserInteractionEnabled = false - self.titleNode.displaysAsynchronously = false - - self.textNode = ImmediateTextNode() - self.textNode.maximumNumberOfLines = 0 - self.textNode.lineSpacing = 0.15 - self.textNode.textAlignment = .center - self.textNode.isUserInteractionEnabled = false - self.textNode.displaysAsynchronously = false - - self.stickerNode = ChatMediaInputStickerGridItemNode() - - super.init() - - self.addSubnode(self.titleNode) - self.addSubnode(self.textNode) - self.addSubnode(self.stickerNode) - } - - override public func didLoad() { - super.didLoad() - - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.stickerTapGesture(_:))) - tapRecognizer.delegate = self.wrappedGestureRecognizerDelegate - self.stickerNode.view.addGestureRecognizer(tapRecognizer) - } - - deinit { - self.disposable.dispose() - } - - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true - } - - @objc private func stickerTapGesture(_ gestureRecognizer: UITapGestureRecognizer) { - guard let stickerItem = self.stickerItem else { - return - } - let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file._parse()), false, self.view, self.stickerNode.bounds, nil, []) - } - - public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize { - if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { - self.currentTheme = interfaceState.theme - self.currentStrings = interfaceState.strings - - var displayName = "" - let distance = interfaceState.peerNearbyData?.distance ?? 0 - - if let renderedPeer = interfaceState.renderedPeer { - if let chatPeer = renderedPeer.chatOrMonoforumMainPeer { - displayName = EnginePeer(chatPeer).compactDisplayTitle - } - } - - let titleString = interfaceState.strings.Conversation_PeerNearbyTitle(displayName, shortStringForDistance(strings: interfaceState.strings, distance: distance)).string - let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) - - self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText) - - self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_PeerNearbyText, font: messageFont, textColor: serviceColor.primaryText) - } - - let stickerSize = CGSize(width: 160.0, height: 160.0) - if let item = self.stickerItem { - self.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true) - } else if !self.didSetupSticker { - let sticker: Signal - if let preloadedSticker = interfaceState.greetingData?.sticker { - sticker = preloadedSticker - } else { - sticker = self.context.engine.stickers.randomGreetingSticker() - |> map { item -> TelegramMediaFile? in - return item?.file - } - } - - self.didSetupSticker = true - self.disposable.set((sticker - |> deliverOnMainQueue).startStrict(next: { [weak self] sticker in - if let strongSelf = self, let sticker = sticker { - let inputNodeInteraction = ChatMediaInputNodeInteraction( - navigateToCollectionId: { _ in - }, - navigateBackToStickers: { - }, - setGifMode: { _ in - }, - openSettings: { - }, - openTrending: { _ in - }, - dismissTrendingPacks: { _ in - }, - toggleSearch: { _, _, _ in - }, - openPeerSpecificSettings: { - }, - dismissPeerSpecificSettings: { - }, - clearRecentlyUsedStickers: { - } - ) - inputNodeInteraction.displayStickerPlaceholder = false - - let index = ItemCollectionItemIndex(index: 0, id: 0) - let collectionId = ItemCollectionId(namespace: 0, id: 0) - let stickerPackItem = StickerPackItem(index: index, file: sticker, indexKeys: []) - let item = ChatMediaInputStickerGridItem(context: strongSelf.context, collectionId: collectionId, stickerPackInfo: nil, index: ItemCollectionViewEntryIndex(collectionIndex: 0, collectionId: collectionId, itemIndex: index), stickerItem: stickerPackItem, canManagePeerSpecificPack: nil, interfaceInteraction: nil, inputNodeInteraction: inputNodeInteraction, hasAccessory: false, theme: interfaceState.theme, large: true, selected: {}) - strongSelf.stickerItem = item - strongSelf.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true) - strongSelf.stickerNode.isVisibleInGrid = true - strongSelf.stickerNode.updateIsPanelVisible(true) - } - })) - } - - let insets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) - let titleSpacing: CGFloat = 5.0 - let stickerSpacing: CGFloat = 5.0 - - var contentWidth: CGFloat = 210.0 - var contentHeight: CGFloat = 0.0 - - let titleSize = self.titleNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) - let textSize = self.textNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) - - contentWidth = max(contentWidth, max(titleSize.width, textSize.width)) - - contentHeight += titleSize.height + titleSpacing + textSize.height + stickerSpacing + stickerSize.height - - let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: contentWidth, height: contentHeight)) - - let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - titleSize.width) / 2.0), y: contentRect.minY), size: titleSize) - transition.updateFrame(node: self.titleNode, frame: titleFrame) - - let textFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - textSize.width) / 2.0), y: titleFrame.maxY + titleSpacing), size: textSize) - transition.updateFrame(node: self.textNode, frame: textFrame) - - let stickerFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - stickerSize.width) / 2.0), y: textFrame.maxY + stickerSpacing), size: stickerSize) - transition.updateFrame(node: self.stickerNode, frame: stickerFrame) - - return contentRect.insetBy(dx: -insets.left, dy: -insets.top).size - } -} - private final class ChatEmptyNodeSecretChatContent: ASDisplayNode, ChatEmptyNodeContent { private let titleNode: ImmediateTextNode private let subtitleNode: ImmediateTextNode @@ -1461,7 +1290,6 @@ private enum ChatEmptyNodeContentType: Equatable { case secret case group case cloud - case peerNearby case greeting case topic case premiumRequired @@ -1851,8 +1679,6 @@ public final class ChatEmptyNode: ASDisplayNode { contentType = .group } else if let channel = peer as? TelegramChannel, case .group = channel.info, channel.flags.contains(.isCreator) && !channel.flags.contains(.isGigagroup) && !channel.isMonoForum { contentType = .group - } else if let _ = interfaceState.peerNearbyData { - contentType = .peerNearby } else if let peer = peer as? TelegramUser { if let sendPaidMessageStars = interfaceState.sendPaidMessageStars, interfaceState.businessIntro == nil { contentType = .starsRequired(sendPaidMessageStars.value) @@ -1917,8 +1743,6 @@ public final class ChatEmptyNode: ASDisplayNode { }) interfaceInteraction.presentControllerInCurrent(controller, nil) } - case .peerNearby: - node = ChatEmptyNodeNearbyChatContent(context: self.context, interaction: self.interaction) case .greeting: node = ChatEmptyNodeGreetingChatContent(context: self.context, interaction: self.interaction) updateGreetingSticker = true @@ -1939,7 +1763,7 @@ public final class ChatEmptyNode: ASDisplayNode { } } switch contentType { - case .peerNearby, .greeting, .premiumRequired, .starsRequired, .cloud: + case .greeting, .premiumRequired, .starsRequired, .cloud: self.isUserInteractionEnabled = true default: self.isUserInteractionEnabled = false diff --git a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift index ebea61edb7..ded214f775 100644 --- a/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift +++ b/submodules/TelegramUI/Components/Chat/ChatHistoryEntry/Sources/ChatHistoryEntry.swift @@ -20,8 +20,9 @@ public struct ChatMessageEntryAttributes: Equatable { public var isCentered: Bool public var authorStoryStats: PeerStoryStats? public var displayContinueThreadFooter: Bool + public var pinToTop: Bool - public init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?, isPlaying: Bool, isCentered: Bool, authorStoryStats: PeerStoryStats?, displayContinueThreadFooter: Bool) { + public init(rank: CachedChannelAdminRank?, isContact: Bool, contentTypeHint: ChatMessageEntryContentType, updatingMedia: ChatUpdatingMessageMedia?, isPlaying: Bool, isCentered: Bool, authorStoryStats: PeerStoryStats?, displayContinueThreadFooter: Bool, pinToTop: Bool) { self.rank = rank self.isContact = isContact self.contentTypeHint = contentTypeHint @@ -30,6 +31,7 @@ public struct ChatMessageEntryAttributes: Equatable { self.isCentered = isCentered self.authorStoryStats = authorStoryStats self.displayContinueThreadFooter = displayContinueThreadFooter + self.pinToTop = pinToTop } public init() { @@ -41,6 +43,7 @@ public struct ChatMessageEntryAttributes: Equatable { self.isCentered = false self.authorStoryStats = nil self.displayContinueThreadFooter = false + self.pinToTop = false } } 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/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift b/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift index f3e4c1de58..28980b0af3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputMessageAccessoryPanel/Sources/ChatInputMessageAccessoryPanel.swift @@ -222,6 +222,7 @@ public final class ChatInputMessageAccessoryPanel: Component { let contents: Contents let chatPeerId: EnginePeer.Id? let action: ((UIView) -> Void)? + let longPressAction: ((UIView) -> Void)? let dismiss: (UIView) -> Void public init( @@ -229,12 +230,14 @@ public final class ChatInputMessageAccessoryPanel: Component { contents: Contents, chatPeerId: EnginePeer.Id?, action: ((UIView) -> Void)?, + longPressAction: ((UIView) -> Void)? = nil, dismiss: @escaping (UIView) -> Void ) { self.context = context self.contents = contents self.chatPeerId = chatPeerId self.action = action + self.longPressAction = longPressAction self.dismiss = dismiss } @@ -251,12 +254,16 @@ public final class ChatInputMessageAccessoryPanel: Component { if (lhs.action == nil) != (rhs.action == nil) { return false } + if (lhs.longPressAction == nil) != (rhs.longPressAction == nil) { + return false + } return true } public final class View: UIView, ChatInputAccessoryPanelView { private let closeButton: HighlightTrackingButton private let closeButtonIcon: GlassBackgroundView.ContentImageView + private let longPressGestureRecognizer: UILongPressGestureRecognizer private let lineView: UIImageView private let titleNode: CompositeTextNode @@ -295,6 +302,7 @@ public final class ChatInputMessageAccessoryPanel: Component { self.closeButton = HighlightTrackingButton() self.closeButtonIcon = GlassBackgroundView.ContentImageView() + self.longPressGestureRecognizer = UILongPressGestureRecognizer() self.lineView = UIImageView() self.titleNode = CompositeTextNode() @@ -311,6 +319,10 @@ public final class ChatInputMessageAccessoryPanel: Component { self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), for: .touchUpInside) self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + + self.longPressGestureRecognizer.addTarget(self, action: #selector(self.longPressGesture(_:))) + self.longPressGestureRecognizer.isEnabled = false + self.addGestureRecognizer(self.longPressGestureRecognizer) } required public init?(coder: NSCoder) { @@ -330,6 +342,15 @@ public final class ChatInputMessageAccessoryPanel: Component { } } + @objc private func longPressGesture(_ recognizer: UILongPressGestureRecognizer) { + guard let component = self.component else { + return + } + if case .began = recognizer.state { + component.longPressAction?(self) + } + } + @objc private func closeButtonPressed() { guard let component = self.component else { return @@ -384,6 +405,7 @@ public final class ChatInputMessageAccessoryPanel: Component { self.component = component self.state = state self.environment = environment + self.longPressGestureRecognizer.isEnabled = component.longPressAction != nil if self.closeButtonIcon.image == nil { self.closeButtonIcon.image = generateCloseIcon() diff --git a/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/BUILD index bc482b0ebb..db19e2ba85 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/AccountContext", "//submodules/ChatPresentationInterfaceState", diff --git a/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/Sources/ChatInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/Sources/ChatInputPanelNode.swift index 25a2afaf63..b3a4b9b000 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/Sources/ChatInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputPanelNode/Sources/ChatInputPanelNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import AccountContext import ChatPresentationInterfaceState @@ -23,7 +22,15 @@ open class ChatInputPanelNode: ASDisplayNode { open func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { } - open func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + public final func compactBottomSideInset(bottomInset: CGFloat, deviceMetrics: DeviceMetrics) -> CGFloat { + if bottomInset <= 32.0 && deviceMetrics.screenCornerRadius > 0.0 { + return 18.0 + } else { + return 0.0 + } + } + + open func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) -> CGFloat { return 0.0 } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index 00da8a1024..45786ac58b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -660,7 +660,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } let fittedSize = isEmoji ? dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)) : dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) - let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let pathPrefix = item.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.resource.id)) let mode: AnimatedStickerMode = .direct(cachePathPrefix: pathPrefix) self.animationSize = fittedSize animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier, isVideo: file.mimeType == "video/webm"), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode) @@ -2257,7 +2257,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let incomingMessage = item.message.effectivelyIncoming(item.context.account.peerId) do { - let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id) + let pathPrefix = item.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(resource.id)) let additionalAnimationNode = DefaultAnimatedStickerNodeImpl() additionalAnimationNode.setup(source: source, width: Int(animationSize.width * 1.6), height: Int(animationSize.height * 1.6), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix)) var animationFrame: CGRect @@ -2404,10 +2404,10 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let peach = 0x1F351 let coffin = 0x26B0 - let appConfiguration = item.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + let appConfiguration = item.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) |> take(1) |> map { view in - return view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue + return view?.get(AppConfiguration.self) ?? .defaultValue } let text = item.message.text diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 4cb3db577c..80cc53511c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -356,7 +356,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { contentMediaValue = file } else if file.isVideo { contentMediaValue = file - } else if file.isSticker || file.isAnimatedSticker { + } else if file.isSticker || file.isAnimatedSticker || file.isCustomEmoji { contentMediaValue = file } else { contentFileValue = file @@ -377,7 +377,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { if case .full = contentMediaAutomaticDownload { willDownloadOrLocal = true } else { - willDownloadOrLocal = context.account.postbox.mediaBox.completedResourcePath(file.resource) != nil + willDownloadOrLocal = context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.resource.id)) != nil } if willDownloadOrLocal { contentMediaAutomaticPlayback = true @@ -1115,7 +1115,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { inlineMedia.setSignal(updateInlineImageSignal) } case let .peerAvatar(peer): - if let peerReference = PeerReference(peer._asPeer()) { + if let peerReference = PeerReference(peer) { if let signal = peerAvatarImage(account: context.account, peerReference: peerReference, authorOfMessage: nil, representation: peer.largeProfileImage, displayDimensions: inlineMediaSize, clipStyle: .none, blurred: false, inset: 0.0, emptyColor: mainColor.withMultipliedAlpha(0.1), synchronousLoad: synchronousLoads, provideUnrounded: false) { let updateInlineImageSignal = signal |> map { images -> (TransformImageArguments) -> DrawingContext? in let image = images?.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBirthdateSuggestionContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBirthdateSuggestionContentNode/BUILD index ab37ef98ba..1050ac5b5d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBirthdateSuggestionContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBirthdateSuggestionContentNode/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/SSignalKit/SwiftSignalKit", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/AccountContext", "//submodules/TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBirthdateSuggestionContentNode/Sources/ChatMessageBirthdateSuggestionContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBirthdateSuggestionContentNode/Sources/ChatMessageBirthdateSuggestionContentNode.swift index f99ae32c5b..720cb37e88 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBirthdateSuggestionContentNode/Sources/ChatMessageBirthdateSuggestionContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBirthdateSuggestionContentNode/Sources/ChatMessageBirthdateSuggestionContentNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import SwiftSignalKit -import Postbox import TelegramCore import AccountContext import TelegramPresentationData diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift index 01f95aa3ff..0e849d382e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode/Sources/ChatMessageBubbleContentNode.swift @@ -225,7 +225,7 @@ open class ChatMessageBubbleContentNode: ASDisplayNode { public var updateIsTextSelectionActive: ((Bool) -> Void)? public var requestInlineUpdate: (() -> Void)? - public var requestFullUpdate: (() -> Void)? + public var requestFullUpdate: ((ControlledTransition?) -> Void)? open var disablesClipping: Bool { return false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD index 21cd7ce809..a739a9a4e9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD @@ -96,6 +96,7 @@ swift_library( "//submodules/AvatarNode", "//submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode", "//submodules/TelegramUI/Components/PremiumAlertController", + "//submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 19fc2d759f..36f8c6a08d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -35,6 +35,7 @@ import ChatMessageDateAndStatusNode import ChatMessageBubbleContentNode import ChatHistoryEntry import ChatMessageTextBubbleContentNode +import ChatMessageRichDataBubbleContentNode import ChatMessageItemCommon import ChatMessageReplyInfoNode import ChatMessageCallBubbleContentNode @@ -329,7 +330,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ messageText = updatingMedia.text } - if !messageText.isEmpty || isUnsupportedMedia || isStoryWithText { + if !messageText.isEmpty || message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) || isUnsupportedMedia || isStoryWithText { if !skipText { if case .group = item.content, !isFile { messageWithCaptionToAdd = (message, itemAttributes) @@ -382,7 +383,11 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ if let attribute = message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute, attribute.leadingPreview { result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: addedPriceInfo ? 1 : 0) } else { - result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + if content.instantPage != nil && item.context.sharedContext.immediateExperimentalUISettings.debugRichText { + result.append((message, ChatMessageRichDataBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + } else { + result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + } } needReactions = false } @@ -1366,8 +1371,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() @@ -1577,6 +1585,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let fontSize = floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0) let nameFont = Font.semibold(fontSize) + let regularFont = Font.regular(fontSize) let inlineBotPrefixFont = Font.regular(fontSize - 1.0) let boostBadgeFont = Font.regular(fontSize - 1.0) @@ -1616,6 +1625,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var allowFullWidth = false let chatLocationPeerId: PeerId = item.chatLocation.peerId ?? item.content.firstMessage.id.peerId + + var isInlinePage = false + if item.context.sharedContext.immediateExperimentalUISettings.debugRichText, let webpage = item.message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.instantPage != nil { + allowFullWidth = true + isInlinePage = true + } do { let peerId = chatLocationPeerId @@ -1656,6 +1671,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI ignoreForward = true effectiveAuthor = author displayAuthorInfo = !mergedTop.merged && incoming + } else if let _ = item.content.firstMessage.guestChatAttribute { + effectiveAuthor = firstMessage.author + displayAuthorInfo = !mergedTop.merged && incoming + hasAvatar = true } else { effectiveAuthor = firstMessage.author @@ -1758,9 +1777,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let forwardInfo = item.content.firstMessage.forwardInfo, forwardInfo.source == nil, forwardInfo.author?.id.namespace == Namespaces.Peer.CloudUser { for media in item.content.firstMessage.media { if let file = media as? TelegramMediaFile { - if file.isMusic { - ignoreForward = true - } else if file.isInstantVideo { + if file.isInstantVideo { isInstantVideo = true } break @@ -1882,6 +1899,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let subject = item.associatedData.subject, case .messageOptions = subject { needsShareButton = false } + + if isInlinePage { + needsShareButton = false + } var tmpWidth: CGFloat if allowFullWidth { @@ -1889,7 +1910,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if (needsShareButton && !isSidePanelOpen) || isAd { tmpWidth -= 45.0 } else { - tmpWidth -= 4.0 + tmpWidth -= 3.0 } } else { tmpWidth = layoutConstants.bubble.maximumWidthFill.widthFor(baseWidth) @@ -2006,6 +2027,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI break } + var guestChatViaFromNameString: String? var inlineBotNameString: String? var replyMessage: Message? var replyForward: QuotedReplyMessageAttribute? @@ -2016,7 +2038,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI var authorNameColor: UIColor? for attribute in firstMessage.attributes { - if let attribute = attribute as? InlineBotMessageAttribute { + if let attribute = attribute as? GuestChatMessageAttribute { + if let peer = firstMessage.peers[attribute.peerId] { + guestChatViaFromNameString = EnginePeer(peer).compactDisplayTitle + } + } else if let attribute = attribute as? InlineBotMessageAttribute { if let peerId = attribute.peerId, let bot = firstMessage.peers[peerId] as? TelegramUser { inlineBotNameString = bot.addressName } else { @@ -2251,7 +2277,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI bubbleReactions = ReactionsMessageAttribute(canViewList: false, isTags: false, reactions: [], recentPeers: [], topPeers: []) } if !bubbleReactions.reactions.isEmpty && !item.presentationData.isPreview { - bottomNodeMergeStatus = .Both + if incoming { + bottomNodeMergeStatus = .Both + } else { + bottomNodeMergeStatus = .Right + } } var currentCredibilityIcon: (EmojiStatusComponent.Content, UIColor?)? @@ -2395,6 +2425,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if authorNameString != nil { displayHeader = true } + if guestChatViaFromNameString != nil { + displayHeader = true + } if inlineBotNameString != nil { displayHeader = true } @@ -2603,14 +2636,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI switch authorRank { case let .creator(rank): if let rank, !rank.isEmpty { - string = rank.trimmingEmojis + string = rank } else { string = item.presentationData.strings.Conversation_Owner } rankBadgeColor = UIColor(rgb: 0x956ac8) case let .admin(rank): if let rank, !rank.isEmpty { - string = rank.trimmingEmojis + string = rank } else { string = item.presentationData.strings.Conversation_Admin } @@ -2621,7 +2654,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI string = item.presentationData.strings.Chat_TagPlaceholder defaultRankColor = defaultRankColor.withMultipliedAlpha(0.5) } else { - string = rank.trimmingEmojis + string = rank } } else { string = "" @@ -2644,6 +2677,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI mutableString.append(botString) attributedString = mutableString viaSuffix = botString + } else if let authorNameString = authorNameString, let authorNameColor = authorNameColor, let guestChatViaFromNameString = guestChatViaFromNameString { + let mutableString = NSMutableAttributedString(string: "\(authorNameString) ", attributes: [NSAttributedString.Key.font: nameFont, NSAttributedString.Key.foregroundColor: authorNameColor]) + let bodyAttributes = MarkdownAttributeSet(font: regularFont, textColor: authorNameColor) + let botString = addAttributesToStringWithRanges(item.presentationData.strings.Conversation_MessageGuestChatForUser(guestChatViaFromNameString)._tuple, body: bodyAttributes, argumentAttributes: [:]) + mutableString.append(botString) + attributedString = mutableString + viaSuffix = botString } else if let authorNameString = authorNameString, let authorNameColor = authorNameColor { attributedString = NSAttributedString(string: authorNameString, font: nameFont, textColor: authorNameColor) } else if let inlineBotNameString = inlineBotNameString { @@ -4776,12 +4816,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI strongSelf.internalUpdateLayout() } - contentNode.requestFullUpdate = { [weak strongSelf] in + contentNode.requestFullUpdate = { [weak strongSelf] customTransition in guard let strongSelf, let item = strongSelf.item else { return } - item.controllerInteraction.requestMessageUpdate(item.message.id, false) + item.controllerInteraction.requestMessageUpdate(item.message.id, false, customTransition) } } } @@ -5596,7 +5636,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } if attribute.isQuote, !replyInfoNode.isQuoteExpanded { replyInfoNode.isQuoteExpanded = true - item.controllerInteraction.requestMessageUpdate(item.message.id, false) + item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) return } var progress: Promise? @@ -5628,7 +5668,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } if attribute.isQuote, !replyInfoNode.isQuoteExpanded { replyInfoNode.isQuoteExpanded = true - item.controllerInteraction.requestMessageUpdate(item.message.id, false) + item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) return } @@ -5949,55 +5989,75 @@ 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 } case let .phone(number): - return .action(InternalBubbleTapAction.Action({ [weak self] in - guard let self, let contentNode = self.contextContentNodeForLink(number, rects: rects) else { - return - } - item.controllerInteraction.longTap(.phone(number), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) - }, contextMenuOnLongPress: !tapAction.hasLongTapAction)) + if tapAction.hasLongTapAction { + return .action(InternalBubbleTapAction.Action({}, actionWithLongTapRecognizer: { [weak self] gesture in + guard let self, let contentNode = self.contextContentNodeForLink(number, rects: rects) else { + return + } + item.controllerInteraction.longTap(.phone(number), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?(), gesture: gesture)) + }, contextMenuOnLongPress: false)) + } else { + disableDefaultPressAnimation = true + } case let .peerMention(peerId, mention, _): - return .action(InternalBubbleTapAction.Action { [weak self] in - guard let self, let contentNode = self.contextContentNodeForLink(mention, rects: rects) else { - return - } - item.controllerInteraction.longTap(.peerMention(peerId, mention), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) - }) + if tapAction.hasLongTapAction { + return .action(InternalBubbleTapAction.Action({}, actionWithLongTapRecognizer: { [weak self] gesture in + guard let self, let contentNode = self.contextContentNodeForLink(mention, rects: rects) else { + return + } + item.controllerInteraction.longTap(.peerMention(peerId, mention), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?(), gesture: gesture)) + }, contextMenuOnLongPress: false)) + } else { + disableDefaultPressAnimation = true + } case let .textMention(name): - return .action(InternalBubbleTapAction.Action { [weak self] in - guard let self, let contentNode = self.contextContentNodeForLink(name, rects: rects) else { - return - } - item.controllerInteraction.longTap(.mention(name), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) - }) + if tapAction.hasLongTapAction { + return .action(InternalBubbleTapAction.Action({}, actionWithLongTapRecognizer: { [weak self] gesture in + guard let self, let contentNode = self.contextContentNodeForLink(name, rects: rects) else { + return + } + item.controllerInteraction.longTap(.mention(name), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?(), gesture: gesture)) + }, contextMenuOnLongPress: false)) + } else { + disableDefaultPressAnimation = true + } case let .botCommand(command): - return .action(InternalBubbleTapAction.Action { [weak self] in - guard let self, let contentNode = self.contextContentNodeForLink(command, rects: rects) else { - return - } - item.controllerInteraction.longTap(.command(command), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) - }) + if tapAction.hasLongTapAction { + return .action(InternalBubbleTapAction.Action({}, actionWithLongTapRecognizer: { [weak self] gesture in + guard let self, let contentNode = self.contextContentNodeForLink(command, rects: rects) else { + return + } + item.controllerInteraction.longTap(.command(command), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?(), gesture: gesture)) + }, contextMenuOnLongPress: false)) + } else { + disableDefaultPressAnimation = true + } case let .hashtag(peerName, hashtag): var fullHashtag = hashtag if let peerName { fullHashtag += "@\(peerName)" } - return .action(InternalBubbleTapAction.Action { [weak self] in - guard let self, let contentNode = self.contextContentNodeForLink(fullHashtag, rects: rects) else { - return - } - item.controllerInteraction.longTap(.hashtag(fullHashtag), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) - }) + if tapAction.hasLongTapAction { + return .action(InternalBubbleTapAction.Action({}, actionWithLongTapRecognizer: { [weak self] gesture in + guard let self, let contentNode = self.contextContentNodeForLink(fullHashtag, rects: rects) else { + return + } + item.controllerInteraction.longTap(.hashtag(fullHashtag), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?(), gesture: gesture)) + }, contextMenuOnLongPress: false)) + } else { + disableDefaultPressAnimation = true + } case .instantPage: break case .wallpaper: @@ -6011,21 +6071,29 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI case .openMessage: break case let .timecode(timecode, text): - if let mediaMessage = mediaMessage { - return .action(InternalBubbleTapAction.Action { [weak self] in - guard let self, let contentNode = self.contextContentNodeForLink(text, rects: rects) else { - return - } - item.controllerInteraction.longTap(.timecode(timecode, text), ChatControllerInteraction.LongTapParams(message: mediaMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) - }) + if let mediaMessage { + if tapAction.hasLongTapAction { + return .action(InternalBubbleTapAction.Action({}, actionWithLongTapRecognizer: { [weak self] gesture in + guard let self, let contentNode = self.contextContentNodeForLink(text, rects: rects) else { + return + } + item.controllerInteraction.longTap(.timecode(timecode, text), ChatControllerInteraction.LongTapParams(message: mediaMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?(), gesture: gesture)) + }, contextMenuOnLongPress: false)) + } else { + disableDefaultPressAnimation = true + } } case let .bankCard(number): - return .action(InternalBubbleTapAction.Action { [weak self] in - guard let self, let contentNode = self.contextContentNodeForLink(number, rects: rects) else { - return - } - item.controllerInteraction.longTap(.bankCard(number), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) - }) + if tapAction.hasLongTapAction { + return .action(InternalBubbleTapAction.Action({}, actionWithLongTapRecognizer: { [weak self] gesture in + guard let self, let contentNode = self.contextContentNodeForLink(number, rects: rects) else { + return + } + item.controllerInteraction.longTap(.bankCard(number), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?(), gesture: gesture)) + }, contextMenuOnLongPress: false)) + } else { + disableDefaultPressAnimation = true + } case .tooltip: break case .openPollResults: @@ -6037,13 +6105,17 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI case .customEmoji: break case let .date(date, _): - return .action(InternalBubbleTapAction.Action { [weak self] in - let fullDate = stringForEntityFormattedDate(timestamp: date, format: .full(timeFormat: .short, dateFormat: .long, dayOfWeek: false), strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat) - guard let self, let contentNode = self.contextContentNodeForLink(fullDate, rects: rects) else { - return - } - item.controllerInteraction.longTap(.date(date), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) - }) + if tapAction.hasLongTapAction { + return .action(InternalBubbleTapAction.Action({}, actionWithLongTapRecognizer: { [weak self] gesture in + let fullDate = stringForEntityFormattedDate(timestamp: date, format: .full(timeFormat: .short, dateFormat: .long, dayOfWeek: false), strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat) + guard let self, let contentNode = self.contextContentNodeForLink(fullDate, rects: rects) else { + return + } + item.controllerInteraction.longTap(.date(date), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?(), gesture: gesture)) + }, contextMenuOnLongPress: false)) + } else { + disableDefaultPressAnimation = true + } case .custom: break } @@ -6206,7 +6278,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if strongSelf.backgroundNode.supernode != nil, let backgroundView = strongSelf.backgroundNode.view.snapshotContentTree(unhide: true) { let backgroundContainer = UIView() - let backdropView = strongSelf.backgroundWallpaperNode.view.snapshotContentTree(unhide: true) + let backdropView = strongSelf.backgroundWallpaperNode.view.snapshotContentTree(unhide: true, keepPortals: true) if let backdropView = backdropView { let backdropFrame = strongSelf.backgroundWallpaperNode.layer.convert(strongSelf.backgroundWallpaperNode.bounds, to: strongSelf.backgroundNode.layer) backdropView.frame = backdropFrame @@ -7237,10 +7309,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if item.controllerInteraction.summarizedMessageIds.contains(item.message.id) { item.controllerInteraction.summarizedMessageIds.remove(item.message.id) - let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) } else { item.controllerInteraction.summarizedMessageIds.insert(item.message.id) - let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) let translateToLanguage = item.associatedData.translateToLanguage var requestSummary = true @@ -7255,7 +7327,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI |> deliverOnMainQueue).start(error: { error in if case .limitExceededPremium = error, let parentController = item.controllerInteraction.navigationController()?.topViewController as? ViewController { item.controllerInteraction.summarizedMessageIds.remove(item.message.id) - let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) let controller = premiumAlertController( context: item.context, parentController: parentController, @@ -7295,7 +7367,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } for contentNode in self.contentNodes { - if contentNode is ChatMessageMediaBubbleContentNode || contentNode is ChatMessageGiftBubbleContentNode || contentNode is ChatMessageWebpageBubbleContentNode || contentNode is ChatMessageInvoiceBubbleContentNode || contentNode is ChatMessageGameBubbleContentNode || contentNode is ChatMessageInstantVideoBubbleContentNode { + if contentNode is ChatMessageMediaBubbleContentNode || contentNode is ChatMessageGiftBubbleContentNode || contentNode is ChatMessageWebpageBubbleContentNode || contentNode is ChatMessageInvoiceBubbleContentNode || contentNode is ChatMessageGameBubbleContentNode || contentNode is ChatMessageInstantVideoBubbleContentNode || contentNode is ChatMessageRichDataBubbleContentNode { contentNode.visibility = mapVisibility(effectiveMediaVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) } else { contentNode.visibility = mapVisibility(effectiveVisibility, boundsSize: self.bounds.size, insets: self.insets, to: contentNode) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift index 33a6223ac6..7f20815c29 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift @@ -134,7 +134,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode guard let item = self.item else{ return } - let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) } public override func willUpdateIsExtractedToContextPreview(_ value: Bool) { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift index 0f8680dbaf..1aebf306db 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFileBubbleContentNode/Sources/ChatMessageFileBubbleContentNode.swift @@ -54,7 +54,7 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { self.interactiveFileNode.requestUpdateLayout = { [weak self] _ in if let strongSelf = self, let item = strongSelf.item { - let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index f1263d1588..bf1b1cde37 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -308,7 +308,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { guard let item = self.item else{ return } - let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) } private func makeProgress() -> Promise { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift index b7b1f1fd10..0b9936d978 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInstantVideoBubbleContentNode/Sources/ChatMessageInstantVideoBubbleContentNode.swift @@ -94,7 +94,7 @@ public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentN self.interactiveVideoNode.requestUpdateLayout = { [weak self] _ in if let strongSelf = self, let item = strongSelf.item { - let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) } } self.interactiveVideoNode.updateTranscriptionExpanded = { [weak self] state in @@ -102,13 +102,13 @@ public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentN let previous = strongSelf.audioTranscriptionState strongSelf.audioTranscriptionState = state strongSelf.interactiveFileNode.audioTranscriptionState = state - let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, state != .inProgress && previous != state) + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, state != .inProgress && previous != state, nil) } } self.interactiveVideoNode.updateTranscriptionText = { [weak self] text in if let strongSelf = self, let item = strongSelf.item { strongSelf.interactiveFileNode.forcedAudioTranscriptionText = text - let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) } } self.interactiveFileNode.updateTranscriptionExpanded = { [weak self] state in @@ -116,7 +116,7 @@ public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentN let previous = strongSelf.audioTranscriptionState strongSelf.audioTranscriptionState = state strongSelf.interactiveVideoNode.audioTranscriptionState = state - let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, previous != state) + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, previous != state, nil) } } @@ -134,7 +134,7 @@ public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentN self.interactiveFileNode.requestUpdateLayout = { [weak self] _ in if let strongSelf = self, let item = strongSelf.item { - let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index cbff3c4238..318593351f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -431,10 +431,10 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { guard let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile else { return .single(nil) } - return context.account.postbox.mediaBox.resourceData(id: file.resource.id) + return context.engine.resources.data(id: EngineMediaResource.Id(file.resource.id)) |> take(1) |> mapToSignal { data -> Signal in - if !data.complete { + if !data.isComplete { return .single(nil) } return .single(data.path) @@ -772,7 +772,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { displayTranscribe = false } else if arguments.message.id.peerId.namespace != Namespaces.Peer.SecretChat && !isViewOnceMessage && !arguments.presentationData.isPreview { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: arguments.context.currentAppConfiguration.with { $0 }) - if arguments.associatedData.isPremium { + if arguments.associatedData.isPremium || arguments.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost { displayTranscribe = true } else if premiumConfiguration.audioTransciptionTrialCount > 0 { if arguments.incoming { @@ -786,8 +786,6 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } else if arguments.incoming && isConsumed == false && arguments.associatedData.alwaysDisplayTranscribeButton.displayForNotConsumed { displayTranscribe = true } - } else if arguments.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost { - displayTranscribe = true } } @@ -2227,4 +2225,3 @@ public final class FileMessageSelectionNode: ASDisplayNode { self.checkNode.frame = CGRect(origin: checkOrigin, size: checkSize) } } - diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift index 70ac211208..b6b8530047 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveInstantVideoNode/Sources/ChatMessageInteractiveInstantVideoNode.swift @@ -703,7 +703,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { if let updatedFile = updatedFile, updatedMedia { if let resource = updatedFile.previewRepresentations.first?.resource { - strongSelf.fetchedThumbnailDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .peer(item.message.id.peerId), userContentType: .video, reference: FileMediaReference.message(message: MessageReference(item.message), media: updatedFile).resourceReference(resource)).startStrict()) + strongSelf.fetchedThumbnailDisposable.set(item.context.engine.resources.fetch(reference: FileMediaReference.message(message: MessageReference(item.message), media: updatedFile).resourceReference(resource), userLocation: .peer(item.message.id.peerId), userContentType: .video).startStrict()) } else { strongSelf.fetchedThumbnailDisposable.set(nil) } @@ -840,7 +840,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { var displayTranscribe = false if item.message.id.peerId.namespace != Namespaces.Peer.SecretChat && statusDisplayType == .free && !isViewOnceMessage && !item.presentationData.isPreview { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: item.context.currentAppConfiguration.with { $0 }) - if item.associatedData.isPremium { + if item.associatedData.isPremium || item.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost { displayTranscribe = true } else if premiumConfiguration.audioTransciptionTrialCount > 0 { if incoming { @@ -852,8 +852,6 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode { } else { displayTranscribe = false } - } else if item.associatedData.alwaysDisplayTranscribeButton.providedByGroupBoost { - displayTranscribe = true } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 9f9796ea95..28097ae4cb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -1471,7 +1471,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } }, cancel: { if file.isAnimated { - context.account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) + context.engine.resources.cancelInteractiveResourceFetch(id: EngineMediaResource.Id(file.resource.id)) } else { messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file) } @@ -1706,7 +1706,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } }, cancel: { if file.isAnimated { - context.account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) + context.engine.resources.cancelInteractiveResourceFetch(id: EngineMediaResource.Id(file.resource.id)) } else { messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file) } @@ -1766,7 +1766,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr if NativeVideoContent.isHLSVideo(file: file), let minimizedQuality = HLSVideoContent.minimizedHLSQuality(file: .standalone(media: file), codecConfiguration: HLSCodecConfiguration(context: context)) { let postbox = context.account.postbox - let playlistStatusSignal = postbox.mediaBox.resourceStatus(minimizedQuality.playlist.media.resource) + let playlistStatusSignal = context.engine.resources.status(resource: EngineMediaResource(minimizedQuality.playlist.media.resource)) |> map { status -> MediaResourceStatus in switch status { case .Fetching, .Paused: @@ -1796,7 +1796,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr return .single((.Local, nil)) } - return postbox.mediaBox.resourceStatus(preloadData.0.media.resource) + return context.engine.resources.status(resource: EngineMediaResource(preloadData.0.media.resource)) |> map { status -> Bool in if case .Fetching = status { return true @@ -1806,7 +1806,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } |> distinctUntilChanged |> mapToSignal { isFetching -> Signal<(MediaResourceStatus, MediaResourceStatus?), NoError> in - return postbox.mediaBox.resourceRangesStatus(preloadData.0.media.resource) + return context.engine.resources.resourceRangesStatus(resource: EngineMediaResource(preloadData.0.media.resource)) |> map { status -> (MediaResourceStatus, MediaResourceStatus?) in let preloadRanges = RangeSet(preloadData.1) let intersection = status.intersection(preloadRanges) @@ -2094,6 +2094,32 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } if currentReplaceAnimatedStickerNode, let updatedAnimatedStickerFile = updateAnimatedStickerFile { + var allowSticker = false + if message.id.peerId.namespace == Namespaces.Peer.SecretChat { + if updatedAnimatedStickerFile.fileId.namespace == Namespaces.Media.CloudFile { + var isValidated = false + for attribute in updatedAnimatedStickerFile.attributes { + if case .hintIsValidated = attribute { + isValidated = true + break + } + } + + inner: for attribute in updatedAnimatedStickerFile.attributes { + if case let .Sticker(_, packReference, _) = attribute { + if case .name = packReference { + allowSticker = true + } else if isValidated { + allowSticker = true + } + break inner + } + } + } + } else { + allowSticker = true + } + let animatedStickerNode = DefaultAnimatedStickerNodeImpl() animatedStickerNode.isUserInteractionEnabled = false animatedStickerNode.started = { @@ -2105,7 +2131,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr strongSelf.animatedStickerNode = animatedStickerNode let dimensions = updatedAnimatedStickerFile.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0)) - animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: updatedAnimatedStickerFile.resource, isVideo: updatedAnimatedStickerFile.isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil)) + if allowSticker { + animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: updatedAnimatedStickerFile.resource, isVideo: updatedAnimatedStickerFile.isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil)) + } strongSelf.pinchContainerNode.contentNode.insertSubnode(animatedStickerNode, aboveSubnode: strongSelf.imageNode) animatedStickerNode.visibility = strongSelf.visibility } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index 42b4a3638f..05c31d02ab 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -1102,6 +1102,10 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat profilePhoto = maybePhoto isKnown = true } + if profilePhoto == nil, case let .known(maybePhoto) = cachedPeerData.fallbackPhoto { + profilePhoto = maybePhoto + isKnown = true + } } if isKnown { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift index 881982906f..f8c40ccc3d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageItemImpl.swift @@ -270,10 +270,19 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible public var failed: Bool { switch self.content { - case let .message(message, _, _, _, _): - return message.flags.contains(.Failed) - case let .group(messages): - return messages[0].0.flags.contains(.Failed) + case let .message(message, _, _, _, _): + return message.flags.contains(.Failed) + case let .group(messages): + return messages[0].0.flags.contains(.Failed) + } + } + + public var pinToEdgeWithInset: Bool { + switch self.content { + case let .message(_, _, _, attributes, _): + return attributes.pinToTop + case let .group(messages): + return messages[0].3.pinToTop } } @@ -327,6 +336,10 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } displayAuthorInfo = incoming && peerId.isGroupOrChannel && effectiveAuthor != nil + if let _ = content.firstMessage.guestChatAttribute { + displayAuthorInfo = true + } + if let chatPeer = content.firstMessage.peers[content.firstMessage.id.peerId], chatPeer.isForumOrMonoForum { if case .replyThread = chatLocation { if chatPeer.isMonoForum && chatLocation.threadId != context.account.peerId.toInt64() { @@ -518,7 +531,7 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible } } - if viewClassName == ChatMessageBubbleItemNode.self && self.presentationData.largeEmoji && self.message.media.isEmpty { + if viewClassName == ChatMessageBubbleItemNode.self && self.presentationData.largeEmoji && self.message.media.isEmpty && !self.message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) { if case let .message(_, _, _, attributes, _) = self.content { switch attributes.contentTypeHint { case .largeEmoji: diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index 6fdb5b8347..44c77786f8 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 } } @@ -1019,7 +1021,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { let incomingMessage = item.message.effectivelyIncoming(item.context.account.peerId) do { - let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id) + let pathPrefix = item.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(resource.id)) let additionalAnimationNode: AnimatedStickerNode var effectiveScale: CGFloat = 1.0 diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift index 0455a1c4a5..d769f7231b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode/Sources/ChatMessageJoinedChannelBubbleContentNode.swift @@ -777,7 +777,7 @@ private final class ChannelItemComponent: Component { self.mergedAvatarsNode = mergedAvatarsNode } - mergedAvatarsNode.update(context: component.context, peers: component.peers.map { $0._asPeer() }, synchronousLoad: false, imageSize: 60.0, imageSpacing: 10.0, borderWidth: 2.0, avatarFontSize: 26.0) + mergedAvatarsNode.update(context: component.context, peers: component.peers, synchronousLoad: false, imageSize: 60.0, imageSpacing: 10.0, borderWidth: 2.0, avatarFontSize: 26.0) let avatarsSize = CGSize(width: avatarSize.width + 20.0, height: avatarSize.height) mergedAvatarsNode.updateLayout(size: avatarsSize) mergedAvatarsNode.frame = CGRect(origin: CGPoint(x: avatarFrame.midX - avatarsSize.width / 2.0, y: avatarFrame.minY), size: avatarsSize) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift index 367234a0e1..484d9a2312 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMapBubbleContentNode/Sources/ChatMessageMapBubbleContentNode.swift @@ -439,7 +439,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode { if let strongSelf = self { strongSelf.timeoutTimer?.0.invalidate() strongSelf.timeoutTimer = nil - item.controllerInteraction.requestMessageUpdate(item.message.id, false) + item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) } }, queue: Queue.mainQueue()) strongSelf.timeoutTimer = (timer, timeoutDeadline) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index a979e9f00f..61af684c63 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -158,13 +158,13 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if case .full = automaticDownload { automaticPlayback = true } else { - automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil + automaticPlayback = item.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(telegramFile.resource.id)) != nil } } else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo { if case .full = automaticDownload { automaticPlayback = true } else { - automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil + automaticPlayback = item.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(telegramFile.resource.id)) != nil } } } @@ -184,7 +184,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if case .full = automaticDownload { automaticPlayback = true } else { - automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil + automaticPlayback = item.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(telegramFile.resource.id)) != nil } } else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo { if let _ = telegramFile.videoCover { @@ -194,7 +194,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } else if case .full = automaticDownload { automaticPlayback = true } else { - automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil + automaticPlayback = item.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(telegramFile.resource.id)) != nil } } } @@ -240,7 +240,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } else if case .full = automaticDownload { automaticPlayback = true } else { - automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil + automaticPlayback = item.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(telegramFile.resource.id)) != nil } } } @@ -265,7 +265,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if case .full = automaticDownload { automaticPlayback = true } else { - automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil + automaticPlayback = item.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(telegramFile.resource.id)) != nil } } else if (telegramFile.isVideo && !telegramFile.isAnimated) && item.context.sharedContext.energyUsageSettings.autoplayVideo { if let _ = telegramFile.videoCover { @@ -275,7 +275,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } else if case .full = automaticDownload { automaticPlayback = true } else { - automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(telegramFile.resource) != nil + automaticPlayback = item.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(telegramFile.resource.id)) != nil } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift index 0c2b0da404..cd776d9aa5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageNotificationItem/Sources/ChatCallNotificationItem.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD index 491db8f060..3e5dfa3db3 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/BUILD @@ -36,6 +36,8 @@ swift_library( "//submodules/TelegramUI/Components/ChatControllerInteraction", "//submodules/RadialStatusNode", "//submodules/TelegramUI/Components/ComposePollScreen", + "//submodules/UndoUI", + "//submodules/TelegramStringFormatting", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index e13612c2b5..1be7762dff 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -29,6 +29,8 @@ import ComponentFlow import TextFieldComponent import PlainButtonComponent import LottieComponent +import UndoUI +import TelegramStringFormatting private final class ChatMessagePollOptionRadioNodeParameters: NSObject { let timestamp: Double @@ -852,9 +854,9 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { // } let rightInset: CGFloat = 10.0 + mediaInset - let recentVoterPeers: [Peer] + let recentVoterPeers: [EnginePeer] if let optionResult { - recentVoterPeers = optionResult.recentVoterPeerIds.compactMap { message.peers[$0] } + recentVoterPeers = optionResult.recentVoterPeerIds.compactMap { message.peers[$0] }.map(EnginePeer.init) } else { recentVoterPeers = [] } @@ -1346,7 +1348,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: 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) @@ -2163,7 +2165,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { guard let item = self.item else { return } - item.controllerInteraction.requestMessageUpdate(item.message.id, false) + item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) } private func updatePollAddOptionFocused(_ focus: Bool) { @@ -2281,7 +2283,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { case let .result(result): if case let .media(resultMedia) = result { if let resultImage = resultMedia.media as? TelegramMediaImage, let resultLargest = largestImageRepresentation(resultImage.representations) { - item.context.account.postbox.mediaBox.moveResourceData(from: largest.resource.id, to: resultLargest.resource.id, synchronous: true) + item.context.engine.resources.moveResourceData(from: EngineMediaResource.Id(largest.resource.id), to: EngineMediaResource.Id(resultLargest.resource.id), synchronous: true) } media.media = resultMedia media.progress = nil @@ -2314,7 +2316,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { case let .result(result): if case let .media(resultMedia) = result { if let resultFile = resultMedia.media as? TelegramMediaFile { - item.context.account.postbox.mediaBox.moveResourceData(from: file.resource.id, to: resultFile.resource.id, synchronous: true) + item.context.engine.resources.moveResourceData(from: EngineMediaResource.Id(file.resource.id), to: EngineMediaResource.Id(resultFile.resource.id), synchronous: true) } media.media = resultMedia media.progress = nil @@ -2370,7 +2372,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { item.controllerInteraction.requestOpenMessagePollResults(item.message.id, pollId) case .anonymous: self.isPreviewingResults = !self.isPreviewingResults - item.controllerInteraction.requestMessageUpdate(item.message.id, false) + item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) } } } else if !selectedOpaqueIdentifiers.isEmpty { @@ -2412,21 +2414,21 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { return { item, layoutConstants, _, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) - + return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in let message = item.message - + let incoming = item.message.effectivelyIncoming(item.context.account.peerId) var isBotChat: Bool = false if let peer = item.message.peers[item.message.id.peerId] as? TelegramUser, peer.botInfo != nil { isBotChat = true } - + let additionalTextRightInset: CGFloat = 24.0 - + let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset - additionalTextRightInset, height: constrainedSize.height) - + var edited = false if item.attributes.updatingMedia != nil { edited = true @@ -2451,7 +2453,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { starsCount = attribute.stars.value } } - + let dateFormat: MessageTimestampStatusFormat if item.presentationData.isPreview { dateFormat = .full @@ -2459,7 +2461,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { dateFormat = .regular } let dateText = stringForMessageTimestampStatus(accountPeerId: item.context.account.peerId, message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: dateFormat, associatedData: item.associatedData) - + let statusType: ChatMessageDateAndStatusType? if case .customChatContents = item.associatedData.subject { statusType = nil @@ -2481,15 +2483,15 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { statusType = nil } } - + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? - + if let statusType = statusType { var isReplyThread = false if case .replyThread = item.chatLocation { isReplyThread = true } - + statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( context: item.context, presentationData: item.presentationData, @@ -2516,7 +2518,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) } - + var poll: TelegramMediaPoll? for media in item.message.media { if let media = media as? TelegramMediaPoll { @@ -2524,13 +2526,13 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { break } } - + let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing - + var pollTitleText = poll?.text ?? "" var pollTitleEntities = poll?.textEntities ?? [] var pollOptions: [TranslationMessageAttribute.Additional] = [] - + var isTranslating = false if let poll, let translateToLanguage = item.associatedData.translateToLanguage, !poll.text.isEmpty && incoming { isTranslating = true @@ -2544,7 +2546,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - + let attributedText = stringWithAppliedEntities( pollTitleText, entities: pollTitleEntities, @@ -2559,21 +2561,21 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { blockQuoteFont: item.presentationData.messageBoldFont, message: message ) - + let textInsets = UIEdgeInsets(top: 2.0, left: 0.0, bottom: 5.0, right: 0.0) - + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) let typeText: String - - var avatarPeers: [Peer] = [] + + var avatarPeers: [EnginePeer] = [] if let poll = poll { for peerId in poll.results.recentVoters { if let peer = item.message.peers[peerId] { - avatarPeers.append(peer) + avatarPeers.append(EnginePeer(peer)) } } } - + if let poll = poll, isPollEffectivelyClosed(message: message, poll: poll) { typeText = item.presentationData.strings.MessagePoll_LabelClosed } else if let poll = poll { @@ -2597,9 +2599,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { typeText = item.presentationData.strings.MessagePoll_LabelAnonymous } let (typeLayout, typeApply) = makeTypeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: typeText, font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - + let votersString: String? - + if isBotChat { votersString = nil } else if let poll = poll, let totalVoters = poll.results.totalVoters { @@ -2621,17 +2623,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { votersString = " " } let (votersLayout, votersApply) = makeVotersLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: votersString ?? "", font: labelsFont, textColor: messageTheme.secondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) - - var hasVoted = false - if let poll, let voters = poll.results.voters { - for voter in voters { - if voter.selected { - hasVoted = true - break - } - } - } - + let viewResultsString: String if let poll, let totalVoters = poll.results.totalVoters { if case .public = poll.publicity { @@ -2655,18 +2647,18 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { let chevronImage = incoming ? PresentationResourcesChat.chatBubblePollChevronRightIncomingIcon(item.presentationData.theme.theme) : PresentationResourcesChat.chatBubblePollChevronRightOutgoingIcon(item.presentationData.theme.theme) viewResultsAttributedString.addAttribute(.attachment, value: chevronImage!, range: NSRange(range, in: viewResultsAttributedString.string)) } - + let (buttonSubmitInactiveTextLayout, buttonSubmitInactiveTextApply) = makeSubmitInactiveTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.MessagePoll_SubmitVote, font: Font.regular(17.0), textColor: messageTheme.accentControlDisabledColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) let (buttonSubmitActiveTextLayout, buttonSubmitActiveTextApply) = makeSubmitActiveTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.MessagePoll_SubmitVote, font: Font.regular(17.0), textColor: messageTheme.polls.bar), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) let (buttonSaveTextLayout, buttonSaveTextApply) = makeSaveTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Common_Save, font: Font.regular(17.0), textColor: messageTheme.polls.bar), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) let (buttonViewResultsTextLayout, buttonViewResultsTextApply) = makeViewResultsTextLayout(TextNodeLayoutArguments(attributedString: viewResultsAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets)) - + var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), size: textLayout.size) var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom)) - + textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) - + var boundingSize: CGSize = textFrameWithoutInsets.size boundingSize.width += additionalTextRightInset boundingSize.width = max(boundingSize.width, typeLayout.size.width) @@ -2674,27 +2666,41 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { boundingSize.width = max(boundingSize.width, buttonSubmitInactiveTextLayout.size.width + 4.0/* + (statusSize?.width ?? 0.0)*/) boundingSize.width = max(boundingSize.width, buttonSaveTextLayout.size.width + 4.0/* + (statusSize?.width ?? 0.0)*/) boundingSize.width = max(boundingSize.width, buttonViewResultsTextLayout.size.width + 4.0/* + (statusSize?.width ?? 0.0)*/) - + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { boundingSize.width = max(boundingSize.width, statusSuggestedWidthAndContinue.0) } - + boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom - + let isClosed: Bool if let poll = poll { isClosed = isPollEffectivelyClosed(message: message, poll: poll) } else { isClosed = false } - + var pollOptionsFinalizeLayouts: [(hasResult: Bool, layout: (CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))] = [] var addOptionFinalizeLayout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollAddOptionNode))? var orderedPollOptions: [(Int, TelegramMediaPollOption)] = [] + + var isRestricted = false if let poll = poll { + if !poll.countries.isEmpty, let accountCountry = item.associatedData.accountCountry, !poll.countries.contains(accountCountry) { + isRestricted = true + } + if poll.restrictToSubscribers { + let period: Int32 = item.context.account.testingEnvironment ? 5 * 60 : 24 * 60 * 60 + if !item.associatedData.isParticipant { + isRestricted = true + } else if let invitedOn = item.associatedData.invitedOn, invitedOn + period > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { + isRestricted = true + } + } + orderedPollOptions = resolvedOptionOrder(for: item) - + var optionVoterCount: [Int: Int32] = [:] var maxOptionVoterCount: Int32 = 0 var totalVoterCount: Int32 = 0 @@ -2714,12 +2720,11 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } totalVoterCount = totalVoters - if didVote || isClosed || isPreviewingResults { + if didVote || isClosed || isPreviewingResults || isRestricted { for i in 0 ..< poll.options.count { inner: for optionVoters in voters { if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier { optionVoterCount[i] = optionVoters.count - //TODO:correct maxOptionVoterCount = max(maxOptionVoterCount, optionVoters.count ?? 0) break inner } @@ -2727,16 +2732,16 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - + var optionVoterCounts: [Int] if totalVoterCount != 0 { optionVoterCounts = countNicePercent(votes: (0 ..< poll.options.count).map({ Int(optionVoterCount[$0] ?? 0) }), total: Int(totalVoterCount)) } else { optionVoterCounts = Array(repeating: 0, count: poll.options.count) } - + let hasAnyOptionMedia = orderedPollOptions.contains(where: { $0.1.media != nil }) - + for (i, option) in orderedPollOptions { let makeLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ presentationContext: ChatPresentationContext, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ translation: TranslationMessageAttribute.Additional?, _ optionResult: ChatMessagePollOptionResult?, _ forceSelected: Bool?, _ hasAnyMedia: Bool, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))) if let previous = previousOptionNodeLayouts[option.opaqueIdentifier] { @@ -2755,13 +2760,13 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if let count = optionVoterCount[i] { if maxOptionVoterCount != 0 && totalVoterCount != 0 { optionResult = ChatMessagePollOptionResult(normalized: CGFloat(count) / CGFloat(maxOptionVoterCount), percent: optionVoterCounts[i], count: count, recentVoterPeerIds: recentVoterPeerIds) - } else if isClosed { + } else if isClosed || isRestricted { optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0, recentVoterPeerIds: recentVoterPeerIds) } - } else if isClosed { + } else if isClosed || isRestricted { optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0, recentVoterPeerIds: recentVoterPeerIds) } - + var translation: TranslationMessageAttribute.Additional? if !pollOptions.isEmpty && i < pollOptions.count { translation = pollOptions[i] @@ -2771,42 +2776,34 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if !votedFor.isEmpty && optionResult == nil { forceSelected = votedFor.contains(option.opaqueIdentifier) } - + let result = makeLayout(item.context, item.presentationData, item.controllerInteraction.presentationContext, item.message, poll, option, translation, optionResult, forceSelected, hasAnyOptionMedia, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0) boundingSize.width = max(boundingSize.width, result.minimumWidth + layoutConstants.bubble.borderInset * 2.0) pollOptionsFinalizeLayouts.append((optionResult != nil, result.1)) } - + var maxPollOptions: Int = 20 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) && 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) addOptionFinalizeLayout = addOptionResult.layout } } - - boundingSize.width = max(boundingSize.width, min(280.0, constrainedSize.width)) - - var canVote = false - if (item.message.id.namespace == Namespaces.Message.Cloud || Namespaces.Message.allNonRegular.contains(item.message.id.namespace)), let poll = poll, poll.pollId.namespace == Namespaces.Media.CloudPoll, !isClosed { - if !hasVoted { - canVote = true - } - } - let _ = canVote - + boundingSize.width = max(boundingSize.width, min(280.0, constrainedSize.width)) + return (boundingSize.width, { boundingWidth in var resultSize = CGSize(width: max(boundingSize.width, boundingWidth), height: boundingSize.height) - + let titleTypeSpacing: CGFloat = -4.0 let typeOptionsSpacing: CGFloat = 3.0 resultSize.height += titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing - + var optionNodesSizesAndApply: [(CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)] = [] for finalizeLayout in pollOptionsFinalizeLayouts { let result = finalizeLayout.layout(boundingWidth - layoutConstants.bubble.borderInset * 2.0) @@ -2825,7 +2822,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { resultSize.height += result.0.height addOptionSizeAndApply = result } - + let optionsVotersSpacing: CGFloat = 11.0 let optionsButtonSpacing: CGFloat = 9.0 let votersBottomSpacing: CGFloat = 11.0 @@ -2838,26 +2835,26 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if let poll, case .poll = poll.kind, !poll.isClosed, let _ = poll.deadlineTimeout { resultSize.height += 6.0 } - + var statusSizeAndApply: (CGSize, (ListViewItemUpdateAnimation) -> Void)? if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth) } - + if let statusSizeAndApply = statusSizeAndApply { resultSize.height += statusSizeAndApply.0.height - 6.0 } - + let buttonSubmitInactiveTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonSubmitInactiveTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonSubmitInactiveTextLayout.size) let buttonSubmitActiveTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonSubmitActiveTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonSubmitActiveTextLayout.size) let buttonSaveTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonSaveTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonSaveTextLayout.size) let buttonViewResultsTextFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonViewResultsTextLayout.size.width) / 2.0), y: optionsButtonSpacing), size: buttonViewResultsTextLayout.size) - + return (resultSize, { [weak self] animation, synchronousLoad, _ in if let strongSelf = self { strongSelf.item = item strongSelf.poll = poll - + let cachedLayout = strongSelf.textNode.textNode.cachedLayout if case .System = animation { if let cachedLayout = cachedLayout { @@ -2877,7 +2874,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } } } - + let _ = textApply(TextNodeWithEntities.Arguments( context: item.context, cache: item.context.animationCache, @@ -2886,7 +2883,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { attemptSynchronous: synchronousLoad) ) let _ = typeApply() - + var verticalOffset = textFrame.maxY + titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing var updatedOptionNodes: [ChatMessagePollOptionNode] = [] for i in 0 ..< optionNodesSizesAndApply.count { @@ -2904,17 +2901,25 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { let option = optionNode.option optionNode.pressed = { [weak self] in - guard let self, let item = self.item, let option else { + guard let self, + let item = self.item, + let option else { return } item.controllerInteraction.requestSelectMessagePollOptions(item.message.id, [option.opaqueIdentifier]) } optionNode.resultPressed = { [weak self] in - guard let self, let item = self.item, let option else { + guard let self, + let item = self.item, + let option else { return } - if let poll, case .public = poll.publicity { - item.controllerInteraction.openMessagePollResults(item.message.id, option.opaqueIdentifier) + if let poll { + if case .public = poll.publicity { + item.controllerInteraction.openMessagePollResults(item.message.id, option.opaqueIdentifier) + } else if isRestricted { + item.controllerInteraction.displayPollRestrictedToast(item.message.id) + } } } optionNode.selectionUpdated = { [weak self] in @@ -2924,7 +2929,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { self.updateSelection() } optionNode.longTapped = { [weak self] in - guard let self, let item = self.item, let option else { + guard let self, + let item = self.item, + let option else { return } item.controllerInteraction.pollOptionLongTap(option.opaqueIdentifier, ChatControllerInteraction.LongTapParams(message: item.message, contentNode: optionNode.contextSourceNode, messageNode: strongSelf, progress: nil)) @@ -2933,7 +2940,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } else { animation.animator.updateFrame(layer: optionNode.layer, frame: optionNodeFrame, completion: nil) } - + if optionNode.currentResult != nil { verticalOffset += size.height - 7.0 } else { @@ -2942,7 +2949,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { updatedOptionNodes.append(optionNode) optionNode.isUserInteractionEnabled = !strongSelf.newOptionIsFocused && item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] == nil optionNode.alpha = strongSelf.newOptionIsFocused ? 0.5 : 1.0 - + if i > 0 { optionNode.previousOptionNode = updatedOptionNodes[i - 1] } else { @@ -2956,7 +2963,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.optionNodes = updatedOptionNodes strongSelf.updatePollOptionsInteraction(animated: animation.isAnimated) - + if let (size, apply) = addOptionSizeAndApply { let isRequesting = item.controllerInteraction.pollActionState.pollMessageIdsInProgress[item.message.id] != nil let addOptionNode = apply(animation.isAnimated, isRequesting) @@ -2999,11 +3006,11 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.addOptionNode = nil addOptionNode.removeFromSupernode() } - + if let poll = poll, let pendingNewOptionSubmissionText, let pendingNewOptionOptionCount = strongSelf.pendingNewOptionOptionCount, poll.options.count > pendingNewOptionOptionCount, poll.options.contains(where: { $0.text == pendingNewOptionSubmissionText }) { strongSelf.clearNewOptionInput() } - + if textLayout.hasRTL { strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: resultSize.width - textFrame.size.width - textInsets.left - layoutConstants.text.bubbleInsets.right - additionalTextRightInset, y: textFrame.origin.y), size: textFrame.size) } else { @@ -3011,11 +3018,11 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } let typeFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size) animation.animator.updateFrame(layer: strongSelf.typeNode.layer, frame: typeFrame, completion: nil) - + let deadlineTimeout = poll?.deadlineTimeout var displayDeadlineTimer = true var hasSelected = false - + if let poll { if let voters = poll.results.voters { for voter in voters { @@ -3033,7 +3040,8 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } var endDate: Int32? - if let deadlineTimeout, message.id.namespace == Namespaces.Message.Cloud { + if let deadlineTimeout, + message.id.namespace == Namespaces.Message.Cloud { let startDate: Int32 if let forwardInfo = message.forwardInfo { startDate = forwardInfo.date @@ -3063,12 +3071,13 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.timerNode = timerNode strongSelf.addSubnode(timerNode) timerNode.reachedTimeout = { - guard let strongSelf = self, let _ = strongSelf.item else { + guard let strongSelf = self, + let _ = strongSelf.item else { return } //item.controllerInteraction.requestMessageUpdate(item.message.id) } - + let timerTransition: ContainedViewLayoutTransition if animation.isAnimated { timerTransition = .animated(duration: 0.25, curve: .easeInOut) @@ -3086,7 +3095,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { timerNode.frame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right, y: typeFrame.minY), size: CGSize()) } else if let timerNode = strongSelf.timerNode { strongSelf.timerNode = nil - + let timerTransition: ContainedViewLayoutTransition if animation.isAnimated { timerTransition = .animated(duration: 0.25, curve: .easeInOut) @@ -3119,7 +3128,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { timerNode = DeadlineTimerNode() strongSelf.deadlineTimerNode = timerNode strongSelf.addSubnode(timerNode) - + let timerTransition: ContainedViewLayoutTransition if animation.isAnimated { timerTransition = .animated(duration: 0.25, curve: .easeInOut) @@ -3138,7 +3147,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { statusOffset += 6.0 } else if let timerNode = strongSelf.deadlineTimerNode { strongSelf.deadlineTimerNode = nil - + let timerTransition: ContainedViewLayoutTransition if animation.isAnimated { timerTransition = .animated(duration: 0.25, curve: .easeInOut) @@ -3150,11 +3159,11 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { }) timerTransition.updateTransformScale(node: timerNode, scale: 0.1) } - + let solutionButtonSize = CGSize(width: 32.0, height: 32.0) let solutionButtonFrame = CGRect(origin: CGPoint(x: resultSize.width - layoutConstants.text.bubbleInsets.right - solutionButtonSize.width + 5.0, y: typeFrame.minY - 16.0), size: solutionButtonSize) strongSelf.solutionButtonNode.frame = solutionButtonFrame - + if (strongSelf.timerNode == nil || !displayDeadlineTimer), let poll = poll, case .quiz = poll.kind, let _ = poll.results.solution, (isClosed || hasSelected) { if strongSelf.solutionButtonNode.alpha.isZero { let timerTransition: ContainedViewLayoutTransition @@ -3175,7 +3184,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } timerTransition.updateAlpha(node: strongSelf.solutionButtonNode, alpha: 0.0) } - + let avatarsFrame = CGRect(origin: CGPoint(x: typeFrame.maxX + 6.0, y: typeFrame.minY + floor((typeFrame.height - MergedAvatarsNode.defaultMergedImageSize) / 2.0)), size: CGSize(width: MergedAvatarsNode.defaultMergedImageSize + MergedAvatarsNode.defaultMergedImageSpacing * 2.0, height: MergedAvatarsNode.defaultMergedImageSize)) strongSelf.avatarsNode.frame = avatarsFrame strongSelf.avatarsNode.updateLayout(size: avatarsFrame.size) @@ -3188,20 +3197,20 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } else { alphaTransition = .immediate } - + let _ = votersApply() let votersFrame = CGRect(origin: CGPoint(x: floor((resultSize.width - votersLayout.size.width) / 2.0), y: verticalOffset + optionsVotersSpacing), size: votersLayout.size) animation.animator.updateFrame(layer: strongSelf.votersNode.layer, frame: votersFrame, completion: nil) - + if animation.isAnimated, let previousPoll = previousPoll, let poll = poll { if previousPoll.results.totalVoters == nil && poll.results.totalVoters != nil { strongSelf.votersNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } } - + if let statusSizeAndApply = statusSizeAndApply { let statusFrame = CGRect(origin: CGPoint(x: resultSize.width - statusSizeAndApply.0.width - layoutConstants.text.bubbleInsets.right, y: votersFrame.maxY + statusOffset), size: statusSizeAndApply.0) - + if strongSelf.statusNode.supernode == nil { statusSizeAndApply.1(.None) strongSelf.statusNode.frame = statusFrame @@ -3213,22 +3222,22 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { } else if strongSelf.statusNode.supernode != nil { strongSelf.statusNode.removeFromSupernode() } - + let _ = buttonSubmitInactiveTextApply() strongSelf.buttonSubmitInactiveTextNode.frame = buttonSubmitInactiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset) - + let _ = buttonSubmitActiveTextApply() strongSelf.buttonSubmitActiveTextNode.frame = buttonSubmitActiveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset) - + let _ = buttonSaveTextApply() strongSelf.buttonSaveTextNode.frame = buttonSaveTextFrame.offsetBy(dx: 0.0, dy: verticalOffset) - + let _ = buttonViewResultsTextApply() strongSelf.buttonViewResultsTextNode.frame = buttonViewResultsTextFrame.offsetBy(dx: 0.0, dy: verticalOffset) - + strongSelf.updateSelection() strongSelf.updatePollTooltipMessageState(animated: false) - + let buttonWidth: CGFloat = floor(max(strongSelf.buttonSaveTextNode.frame.width, max(strongSelf.buttonViewResultsTextNode.frame.width, strongSelf.buttonSubmitActiveTextNode.frame.width)) * 1.1) strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: floor((resultSize.width - buttonWidth) / 2.0), y: verticalOffset), size: CGSize(width: buttonWidth, height: 44.0)) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift index 8cf534d057..29a4cda102 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode/Sources/ChatMessageReplyInfoNode.swift @@ -430,8 +430,6 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } let textColor: UIColor - let iconColor: UIColor - switch arguments.type { case let .bubble(incoming): if isExpiredStory || isStory { @@ -441,10 +439,8 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { } else { textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.primaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.primaryTextColor } - iconColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor case .standalone: textColor = titleColor - iconColor = titleColor } var textLeftInset: CGFloat = 0.0 @@ -872,6 +868,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { expiredStoryIconView.frame = CGRect(origin: CGPoint(x: textFrame.minX - 1.0, y: textFrame.minY + 3.0 + UIScreenPixel), size: imageSize) } } + expiredStoryIconView.tintColor = titleColor } else if let expiredStoryIconView = node.expiredStoryIconView { expiredStoryIconView.removeFromSuperview() } @@ -951,7 +948,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode { if let todoItemCompleted { let checkLayerFrame = CGRect(origin: CGPoint(x: textFrame.minX - 16.0, y: textFrame.minY + 5.0), size: CGSize(width: 12.0, height: 12.0)) - let checkTheme = CheckNodeTheme(backgroundColor: iconColor, strokeColor: .clear, borderColor: iconColor, overlayBorder: false, hasInset: true, hasShadow: false, borderWidth: 1.0) + let checkTheme = CheckNodeTheme(backgroundColor: titleColor, strokeColor: .clear, borderColor: titleColor, overlayBorder: false, hasInset: true, hasShadow: false, borderWidth: 1.0) let checkLayer: CheckLayer if let current = node.checkLayer { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/BUILD new file mode 100644 index 0000000000..7c9d63aaa9 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/BUILD @@ -0,0 +1,27 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageRichDataBubbleContentNode", + module_name = "ChatMessageRichDataBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/AccountContext", + "//submodules/InstantPageUI", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUIPreferences", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift new file mode 100644 index 0000000000..dbb3525d44 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageRichDataBubbleContentNode/Sources/ChatMessageRichDataBubbleContentNode.swift @@ -0,0 +1,478 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit +import AccountContext +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import InstantPageUI +import TelegramUIPreferences + +public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode { + public final class ContainerNode: ASDisplayNode { + } + + private let containerNode: ContainerNode + private var currentLayoutTiles: [InstantPageTile] = [] + private var visibleTiles: [Int: InstantPageTileNode] = [:] + private var visibleItemsWithNodes: [Int: InstantPageNode] = [:] + private var currentPageLayout: (boundingWidth: CGFloat, layout: InstantPageLayout)? + private var distanceThresholdGroupCount: [Int: Int] = [:] + private var currentLayoutItemsWithNodes: [InstantPageItem] = [] + private var currentExpandedDetails: [Int : Bool]? + + override public var visibility: ListViewItemNodeVisibility { + didSet { + if oldValue != self.visibility { + self.updateVisibility() + } + } + } + + required public init() { + self.containerNode = ContainerNode() + self.containerNode.clipsToBounds = true + + super.init() + + self.addSubnode(self.containerNode) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + let currentPageLayout = self.currentPageLayout + let previousCurrentLayoutTiles = self.currentLayoutTiles + + return { [weak self] item, layoutConstants, _, _, _, _ in + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) + + return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in + let suggestedBoundingWidth: CGFloat = constrainedSize.width + + return (suggestedBoundingWidth, { boundingWidth in + var boundingSize = CGSize(width: boundingWidth, height: 0.0) + + var pageLayout: InstantPageLayout? + var currentLayoutTiles: [InstantPageTile] = [] + + if let webpage = item.message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let instantPage = content.instantPage { + if let current = currentPageLayout, current.boundingWidth == boundingSize.width { + pageLayout = current.layout + currentLayoutTiles = previousCurrentLayoutTiles + } else { + let pageTheme = instantPageThemeForType(item.presentationData.theme.theme.overallDarkAppearance ? .dark : .light, settings: InstantPagePresentationSettings( + themeType: item.presentationData.theme.theme.overallDarkAppearance ? .dark : .light, + fontSize: .standard, + forceSerif: false, + autoNightMode: false, + ignoreAutoNightModeUntil: 0 + )) + pageLayout = instantPageLayoutForWebPage(webpage, instantPage: instantPage._parse(), userLocation: .other, boundingWidth: boundingWidth - 2.0, safeInset: 0.0, strings: item.presentationData.strings, theme: pageTheme, dateTimeFormat: item.presentationData.dateTimeFormat, webEmbedHeights: [:], addFeedback: false) + if let pageLayout { + currentLayoutTiles = instantPageTilesFromLayout(pageLayout, boundingWidth: boundingWidth) + } + } + } + + if let pageLayout { + boundingSize.height = pageLayout.contentSize.height + 2.0 + } + + return (boundingSize, { animation, synchronousLoads, itemApply in + guard let self else { + return + } + self.item = item + + self.containerNode.frame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: boundingSize.width - 2.0, height: boundingSize.height - 2.0)) + + if let pageLayout { + self.currentPageLayout = (boundingSize.width, pageLayout) + self.currentLayoutTiles = currentLayoutTiles + + var distanceThresholdGroupCount: [Int : Int] = [:] + + for item in pageLayout.items { + if item.wantsNode { + self.currentLayoutItemsWithNodes.append(item) + + if let group = item.distanceThresholdGroup() { + let count: Int + if let currentCount = distanceThresholdGroupCount[Int(group)] { + count = currentCount + } else { + count = 0 + } + distanceThresholdGroupCount[Int(group)] = count + 1 + } + } + } + + self.distanceThresholdGroupCount = distanceThresholdGroupCount + } else { + self.currentPageLayout = nil + self.currentLayoutTiles = [] + self.distanceThresholdGroupCount = [:] + } + + self.updateVisibility() + }) + }) + }) + } + } + + private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect { + let layoutOrigin = tile.frame.origin + let origin = layoutOrigin + return CGRect(origin: origin, size: tile.frame.size) + } + + private func updateVisibility() { + switch self.visibility { + case .none: + self.updateVisibleItems(visibleBounds: CGRect(), animated: false) + case let .visible(_, subRect): + self.updateVisibleItems(visibleBounds: subRect, animated: false) + } + } + + private func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) { + guard let messageItem = self.item else { + return + } + let pageTheme = instantPageThemeForType(messageItem.presentationData.theme.theme.overallDarkAppearance ? .dark : .light, settings: InstantPagePresentationSettings( + themeType: messageItem.presentationData.theme.theme.overallDarkAppearance ? .dark : .light, + fontSize: .standard, + forceSerif: false, + autoNightMode: false, + ignoreAutoNightModeUntil: 0 + )) + let sourceLocation = InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate) + + var visibleTileIndices = Set() + var visibleItemIndices = Set() + + var topNode: ASDisplayNode? + let topTileNode = topNode + if let containerSubnodes = self.containerNode.subnodes { + for node in containerSubnodes.reversed() { + if let node = node as? InstantPageTileNode { + topNode = node + break + } + } + } + + var collapseOffset: CGFloat = 0.0 + collapseOffset = 0.0 + let transition: ContainedViewLayoutTransition + if animated { + transition = .animated(duration: 0.3, curve: .spring) + } else { + transition = .immediate + } + + var itemIndex = -1 + var embedIndex = -1 + var detailsIndex = -1 + + var previousDetailsNode: InstantPageDetailsNode? + + for item in self.currentLayoutItemsWithNodes { + itemIndex += 1 + if item is InstantPageWebEmbedItem { + embedIndex += 1 + } + if let imageItem = item as? InstantPageImageItem, case .webpage = imageItem.media.media { + embedIndex += 1 + } + if item is InstantPageDetailsItem { + detailsIndex += 1 + } + + var itemThreshold: CGFloat = 0.0 + if let group = item.distanceThresholdGroup() { + var count: Int = 0 + if let currentCount = self.distanceThresholdGroupCount[group] { + count = currentCount + } + itemThreshold = item.distanceThresholdWithGroupCount(count) + } + + let itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset) + var thresholdedItemFrame = itemFrame + thresholdedItemFrame.origin.y -= itemThreshold + thresholdedItemFrame.size.height += itemThreshold * 2.0 + + if visibleBounds.intersects(thresholdedItemFrame) { + visibleItemIndices.insert(itemIndex) + + var itemNode = self.visibleItemsWithNodes[itemIndex] + if let currentItemNode = itemNode { + if !item.matchesNode(currentItemNode) { + currentItemNode.removeFromSupernode() + self.visibleItemsWithNodes.removeValue(forKey: itemIndex) + itemNode = nil + } + } + + if itemNode == nil { + let itemIndex = itemIndex + //let embedIndex = embedIndex + //let detailsIndex = detailsIndex + if let newNode = item.node(context: messageItem.context, strings: messageItem.presentationData.strings, nameDisplayOrder: messageItem.presentationData.nameDisplayOrder, theme: pageTheme, sourceLocation: sourceLocation, openMedia: { [weak self] media in + let _ = self + //self?.openMedia(media) + }, longPressMedia: { [weak self] media in + //self?.longPressMedia(media) + let _ = self + }, activatePinchPreview: { [weak self] sourceNode in + /*guard let strongSelf = self, let controller = strongSelf.controller else { + return + } + let pinchController = makePinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + guard let strongSelf = self else { + return CGRect() + } + + let localRect = CGRect(origin: CGPoint(x: 0.0, y: strongSelf.navigationBar.frame.maxY), size: CGSize(width: strongSelf.bounds.width, height: strongSelf.bounds.height - strongSelf.navigationBar.frame.maxY)) + return strongSelf.view.convert(localRect, to: nil) + }) + controller.window?.presentInGlobalOverlay(pinchController)*/ + let _ = self + }, pinchPreviewFinished: { [weak self] itemNode in + /*guard let strongSelf = self else { + return + } + for (_, listItemNode) in strongSelf.visibleItemsWithNodes { + if let listItemNode = listItemNode as? InstantPagePeerReferenceNode { + if listItemNode.frame.intersects(itemNode.frame) && listItemNode.frame.maxY <= itemNode.frame.maxY + 2.0 { + listItemNode.layer.animateAlpha(from: 0.0, to: listItemNode.alpha, duration: 0.25) + break + } + } + }*/ + let _ = self + }, openPeer: { [weak self] peerId in + let _ = self + //self?.openPeer(peerId) + }, openUrl: { [weak self] url in + let _ = self + //self?.openUrl(url) + }, updateWebEmbedHeight: { [weak self] height in + let _ = self + //self?.updateWebEmbedHeight(embedIndex, height) + }, updateDetailsExpanded: { [weak self] expanded in + let _ = self + //self?.updateDetailsExpanded(detailsIndex, expanded) + }, currentExpandedDetails: self.currentExpandedDetails, getPreloadedResource: { _ in return nil }) { + newNode.frame = itemFrame + newNode.updateLayout(size: itemFrame.size, transition: transition) + if let topNode = topNode { + self.containerNode.insertSubnode(newNode, aboveSubnode: topNode) + } else { + self.containerNode.insertSubnode(newNode, at: 0) + } + topNode = newNode + self.visibleItemsWithNodes[itemIndex] = newNode + itemNode = newNode + + if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.requestLayoutUpdate = { [weak self] animated in + let _ = self + /*if let strongSelf = self { + strongSelf.updateVisibleItems(visibleBounds: strongSelf.scrollNode.view.bounds, animated: animated) + }*/ + } + + if let previousDetailsNode = previousDetailsNode { + if itemNode.frame.minY - previousDetailsNode.frame.maxY < 1.0 { + itemNode.previousNode = previousDetailsNode + } + } + previousDetailsNode = itemNode + } + } + } else { + if let itemNode = itemNode, itemNode.frame != itemFrame { + transition.updateFrame(node: itemNode, frame: itemFrame) + itemNode.updateLayout(size: itemFrame.size, transition: transition) + } + } + + if let itemNode = itemNode as? InstantPageDetailsNode { + itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated) + } + } + } + + topNode = topTileNode + + var tileIndex = -1 + for tile in self.currentLayoutTiles { + tileIndex += 1 + + let tileFrame = effectiveFrameForTile(tile) + var tileVisibleFrame = tileFrame + tileVisibleFrame.origin.y -= 400.0 + tileVisibleFrame.size.height += 400.0 * 2.0 + if tileVisibleFrame.intersects(visibleBounds) { + visibleTileIndices.insert(tileIndex) + + if self.visibleTiles[tileIndex] == nil { + let tileNode = InstantPageTileNode(tile: tile, backgroundColor: .clear) + tileNode.frame = tileFrame + if let topNode = topNode { + self.containerNode.insertSubnode(tileNode, aboveSubnode: topNode) + } else { + self.containerNode.insertSubnode(tileNode, at: 0) + } + topNode = tileNode + self.visibleTiles[tileIndex] = tileNode + } else { + if let tileNode = self.visibleTiles[tileIndex] { + tileNode.update(tile: tile, backgroundColor: .clear) + if tileNode.frame != tileFrame { + transition.updateFrame(node: tileNode, frame: tileFrame) + } + } + } + } + } + + var removeTileIndices: [Int] = [] + for (index, tileNode) in self.visibleTiles { + if !visibleTileIndices.contains(index) { + removeTileIndices.append(index) + tileNode.removeFromSupernode() + } + } + for index in removeTileIndices { + self.visibleTiles.removeValue(forKey: index) + } + + var removeItemIndices: [Int] = [] + for (index, itemNode) in self.visibleItemsWithNodes { + if !visibleItemIndices.contains(index) { + removeItemIndices.append(index) + itemNode.removeFromSupernode() + } else { + var itemFrame = itemNode.frame + let itemThreshold: CGFloat = 200.0 + itemFrame.origin.y -= itemThreshold + itemFrame.size.height += itemThreshold * 2.0 + itemNode.updateIsVisible(visibleBounds.intersects(itemFrame)) + } + } + for index in removeItemIndices { + self.visibleItemsWithNodes.removeValue(forKey: index) + } + } + + override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { + /*self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if let statusNode = self.statusNode, statusNode.alpha != 0.0 { + statusNode.layer.animateAlpha(from: 0.0, to: statusNode.alpha, duration: 0.2) + }*/ + } + + override public func animateAdded(_ currentTimestamp: Double, duration: Double) { + /*self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if let statusNode = self.statusNode, statusNode.alpha != 0.0 { + statusNode.layer.animateAlpha(from: 0.0, to: statusNode.alpha, duration: 0.2) + }*/ + } + + override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { + /*self.textNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + if let statusNode = self.statusNode, statusNode.alpha != 0.0 { + statusNode.layer.animateAlpha(from: statusNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + }*/ + } + + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + if case .tap = gesture { + } else { + if let item = self.item, let subject = item.associatedData.subject, case .messageOptions = subject { + return ChatMessageBubbleContentTapAction(content: .none) + } + } + + /*func makeActivate(_ urlRange: NSRange?) -> (() -> Promise?)? { + return { [weak self] in + guard let self else { + return nil + } + + let promise = Promise() + + self.linkProgressDisposable?.dispose() + + if self.linkProgressRange != nil { + self.linkProgressRange = nil + self.updateLinkProgressState() + } + + self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in + guard let self else { + return + } + let updatedRange: NSRange? = value ? urlRange : nil + if self.linkProgressRange != updatedRange { + self.linkProgressRange = updatedRange + self.updateLinkProgressState() + } + }) + + return promise + } + }*/ + + return ChatMessageBubbleContentTapAction(content: .none) + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + + override public func updateTouchesAtPoint(_ point: CGPoint?) { + } + + override public func updateSearchTextHighlightState(text: String?, messages: [MessageIndex]?) { + } + + override public func willUpdateIsExtractedToContextPreview(_ value: Bool) { + } + + override public func updateIsExtractedToContextPreview(_ value: Bool) { + } + + override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { + /*if let statusNode = self.statusNode, !statusNode.isHidden { + return statusNode.reactionView(value: value) + }*/ + return nil + } + + override public func messageEffectTargetView() -> UIView? { + /*if let statusNode = self.statusNode, !statusNode.isHidden { + return statusNode.messageEffectTargetView() + }*/ + return nil + } + + override public func getStatusNode() -> ASDisplayNode? { + return nil + //return self.statusNode + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift index 6047a3099b..5a90c21e7f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift @@ -202,7 +202,7 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { private let reactionOverlayContainer: ChatMessageSelectionInputPanelNodeViewForOverlayContent - private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool, isMediaInputExpanded: Bool)? + private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool, isMediaInputExpanded: Bool, deviceMetrics: DeviceMetrics)? private var presentationInterfaceState: ChatPresentationInterfaceState? private var actions: ChatAvailableMessageActions? @@ -288,8 +288,8 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { if self.selectedMessages.isEmpty { self.actions = nil - if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded) = self.validLayout, let interfaceState = self.presentationInterfaceState { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) + if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded, deviceMetrics) = self.validLayout, let interfaceState = self.presentationInterfaceState { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, deviceMetrics: deviceMetrics, isMediaInputExpanded: isMediaInputExpanded) } self.canDeleteMessagesDisposable.set(nil) } else if let context = self.context { @@ -297,8 +297,8 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { |> deliverOnMainQueue).startStrict(next: { [weak self] actions in if let strongSelf = self { strongSelf.actions = actions - if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight: maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout, let interfaceState = strongSelf.presentationInterfaceState { - let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) + if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight: maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded, deviceMetrics) = strongSelf.validLayout, let interfaceState = strongSelf.presentationInterfaceState { + let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, deviceMetrics: deviceMetrics, isMediaInputExpanded: isMediaInputExpanded) } } })) @@ -456,21 +456,20 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { } private func update(transition: ContainedViewLayoutTransition) { - if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded) = self.validLayout, let interfaceState = self.presentationInterfaceState { - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) + if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded, deviceMetrics) = self.validLayout, let interfaceState = self.presentationInterfaceState { + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, deviceMetrics: deviceMetrics, isMediaInputExpanded: isMediaInputExpanded) } } - override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { - self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded) + override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) -> CGFloat { + self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded, deviceMetrics) var leftInset = leftInset + 8.0 var rightInset = rightInset + 8.0 - if bottomInset <= 32.0 { - leftInset += 18.0 - rightInset += 18.0 - } + let compactBottomSideInset = self.compactBottomSideInset(bottomInset: bottomInset, deviceMetrics: deviceMetrics) + leftInset += compactBottomSideInset + rightInset += compactBottomSideInset let panelHeight = defaultHeight(metrics: metrics) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode/BUILD index 703390463f..ea7eeb3c7e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/SSignalKit/SwiftSignalKit", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/TelegramPresentationData", "//submodules/TelegramUIPreferences", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode/Sources/ChatMessageSuggestedPostInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode/Sources/ChatMessageSuggestedPostInfoNode.swift index 4d7b205375..afc62706e1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode/Sources/ChatMessageSuggestedPostInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSuggestedPostInfoNode/Sources/ChatMessageSuggestedPostInfoNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 931ccd6ec2..a8797855fe 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -197,7 +197,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { self.expandedBlockIds.insert(blockId) } - item.controllerInteraction.requestMessageUpdate(item.message.id, false) + item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) } self.textNode.textNode.requestDisplayContentsUnderSpoilers = { [weak self] location in guard let self else { @@ -249,11 +249,10 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let currentMaxGlyphCount: Int? if let textRevealAnimationState = self.textRevealAnimationState { currentMaxGlyphCount = textRevealAnimationState.glyphCount(timestamp: CACurrentMediaTime()) - //print("currentMaxGlyphCount(\(textRevealAnimationState.fromCount) -> \(textRevealAnimationState.toCount)) fraction: \(textRevealAnimationState.fraction(timestamp: CACurrentMediaTime()))") } else { currentMaxGlyphCount = nil } - let previousGlyphCount = self.textNode.textNode.getGlyphCount() + let previousGlyphCount = self.textNode.textNode.cachedLayout?.attributedString?.length ?? 0 return { item, layoutConstants, _, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) @@ -699,6 +698,15 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } + var hasDraft = false + if item.message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) { + hasDraft = true + } + var hadDraft = false + if let previousItem, previousItem.message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) { + hadDraft = true + } + let textInsets = UIEdgeInsets(top: 2.0, left: 2.0, bottom: 5.0, right: 2.0) let (textLayout, textApply) = textLayout(InteractiveTextNodeLayoutArguments( attributedString: attributedText, @@ -712,25 +720,18 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { lineColor: messageTheme.accentControlColor, displayContentsUnderSpoilers: displayContentsUnderSpoilers.value, customTruncationToken: customTruncationToken, - expandedBlocks: expandedBlockIds + expandedBlocks: expandedBlockIds, + computeCharacterRects: true, + minWidth: (attributedText.string.isEmpty && hasDraft) ? 40.0 : nil )) - var hasDraft = false - if item.message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) { - hasDraft = true - } - var hadDraft = false - if let previousItem, previousItem.message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) { - hadDraft = true - } - var maxGlyphCount = currentMaxGlyphCount if maxGlyphCount == nil && (hasDraft || hadDraft) { maxGlyphCount = previousGlyphCount } var clippedGlyphCountLayout: TextNodeLayout.LayoutInfo? if let maxGlyphCount { - clippedGlyphCountLayout = textLayout.layoutForGlyphCount(glyphCount: maxGlyphCount) + clippedGlyphCountLayout = textLayout.layoutForCharacterCount(characterCount: maxGlyphCount) } var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))? @@ -792,14 +793,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if let clippedGlyphCountLayout { textFrame.size = clippedGlyphCountLayout.size - //print("currentMaxGlyphCount: \(currentMaxGlyphCount), size: \(textFrame.size.height)") textFrameWithoutInsets.size = CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: textFrame.height - textInsets.top - textInsets.bottom) } textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset) var suggestedBoundingWidth: CGFloat = textFrameWithoutInsets.width - if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { + if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue, !hasDraft { suggestedBoundingWidth = max(suggestedBoundingWidth, statusSuggestedWidthAndContinue.0) } let sideInsets = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right @@ -811,7 +811,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - sideInsets) boundingSize = textFrameWithoutInsets.size - if let statusSizeAndApply = statusSizeAndApply { + if let statusSizeAndApply = statusSizeAndApply, !hasDraft { boundingSize.height += statusSizeAndApply.0.height } @@ -828,7 +828,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var previousAnimateGlyphCount: Int? if hasDraft || hadDraft { - previousAnimateGlyphCount = strongSelf.textNode.textNode.getGlyphCount() + previousAnimateGlyphCount = strongSelf.textNode.textNode.cachedLayout?.attributedString?.length ?? 0 } strongSelf.textNode.textNode.displaysAsynchronously = !item.presentationData.isPreview @@ -841,7 +841,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.relativeDateTimer = nil } strongSelf.relativeDateTimer = (SwiftSignalKit.Timer(timeout: Double(formattedDateUpdatePeriod), repeat: true, completion: { [weak self] in - self?.requestFullUpdate?() + self?.requestFullUpdate?(ControlledTransition(duration: 0.15, curve: .easeInOut, interactive: false)) }, queue: Queue.mainQueue()), formattedDateUpdatePeriod) strongSelf.relativeDateTimer?.timer.start() } else if let (timer, _) = strongSelf.relativeDateTimer { @@ -1053,7 +1053,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { codeHighlightState.disposable.dispose() } - if previousAnimateGlyphCount != nil || strongSelf.textRevealAnimationState != nil || hadDraft { + if previousAnimateGlyphCount != nil || strongSelf.textRevealAnimationState != nil || hasDraft || hadDraft { + if strongSelf.textNode.textNode.revealCharacterCount == nil { + if hasDraft { + strongSelf.statusNode?.alpha = 0.0 + } + strongSelf.textNode.textNode.updateRevealCharacterCount(count: previousAnimateGlyphCount ?? 0, animated: false) + } strongSelf.updateTextRevealAnimation(previousGlyphCount: previousAnimateGlyphCount ?? 0) } } @@ -1065,7 +1071,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private func updateTextRevealAnimation(previousGlyphCount: Int) { var fromCount = previousGlyphCount - let toCount = self.textNode.textNode.getGlyphCount() + let toCount = self.textNode.textNode.cachedLayout?.attributedString?.length ?? 0 let timestamp = CACurrentMediaTime() if let textRevealAnimationState = self.textRevealAnimationState { if textRevealAnimationState.toCount == toCount { @@ -1077,13 +1083,13 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { if self.textRevealAnimationState != nil { self.textRevealAnimationState = nil self.textRevealLink = nil - self.textNode.textNode.updateRevealGlyphCount(count: nil) + self.textNode.textNode.updateRevealCharacterCount(count: nil, animated: false) } return } - var duration: Double = Double(toCount - fromCount) / 20.0 - duration = max(0.1, min(duration, 5.0)) + var duration: Double = Double(toCount - fromCount) / 40.0 + duration = max(0.1, min(duration, 1.0)) self.textRevealAnimationState = TextRevealAnimationState( fromCount: fromCount, @@ -1092,9 +1098,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { duration: duration ) if self.textRevealLink == nil, self.textRevealAnimationState != nil { - var lastLineUpdateTimestamp = timestamp self.textRevealLink = SharedDisplayLinkDriver.shared.add { [weak self] _ in - guard let self else { + guard let self, let item = self.item else { return } guard let textRevealAnimationState = self.textRevealAnimationState else { @@ -1106,25 +1111,33 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { self.textRevealAnimationState = nil self.textRevealLink = nil - self.textNode.textNode.updateRevealGlyphCount(count: nil) - self.requestFullUpdate?() - } else { - let lineUpdateTimeout = timestamp - lastLineUpdateTimestamp + self.textNode.textNode.updateRevealCharacterCount(count: nil, animated: false) + if let statusNode = self.statusNode, !item.message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) { + ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut).updateAlpha(node: statusNode, alpha: 1.0) + } + + self.requestFullUpdate?(ControlledTransition(duration: 0.15, curve: .easeInOut, interactive: false)) + } else { 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 { - if lineUpdateTimeout >= 0.0 { - lastLineUpdateTimestamp = timestamp + if let previousRevealGlyphCount = self.textNode.textNode.revealCharacterCount, previousRevealGlyphCount != glyphCount { + if let cachedLayout = self.textNode.textNode.cachedLayout { + if cachedLayout.sizeForCharacterCount(characterCount: previousRevealGlyphCount) != cachedLayout.sizeForCharacterCount(characterCount: glyphCount) { requestUpdate = true } + } else { + requestUpdate = true } + if requestUpdate { + //print("glyphCount: request update") + } + + self.textNode.textNode.updateRevealCharacterCount(count: glyphCount, animated: true) } - self.textNode.textNode.updateRevealGlyphCount(count: glyphCount) if requestUpdate { - self.requestFullUpdate?() + self.requestFullUpdate?(ControlledTransition(duration: 0.15, curve: .easeInOut, interactive: false)) } } } @@ -1133,17 +1146,23 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.statusNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if let statusNode = self.statusNode, statusNode.alpha != 0.0 { + statusNode.layer.animateAlpha(from: 0.0, to: statusNode.alpha, duration: 0.2) + } } override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.statusNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + if let statusNode = self.statusNode, statusNode.alpha != 0.0 { + statusNode.layer.animateAlpha(from: 0.0, to: statusNode.alpha, duration: 0.2) + } } override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.textNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.statusNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + if let statusNode = self.statusNode, statusNode.alpha != 0.0 { + statusNode.layer.animateAlpha(from: statusNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + } } override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { @@ -1718,7 +1737,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } self.displayContentsUnderSpoilers = (value, location) if let item = self.item { - item.controllerInteraction.requestMessageUpdate(item.message.id, false) + item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) } } @@ -1799,7 +1818,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { guard let textSelectionNode = self.textSelectionNode else { return nil } - guard let range = customRange ?? textSelectionNode.getSelection() else { + guard let rawRange = customRange ?? textSelectionNode.getSelection() else { return nil } guard let item = self.item else { @@ -1809,6 +1828,17 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return nil } + func normalizedSelectionRange(_ range: NSRange, length: Int) -> NSRange { + let location = min(max(range.location, 0), length) + let upperBound = min(max(location, range.location + range.length), length) + return NSRange(location: location, length: upperBound - location) + } + + let range = normalizedSelectionRange(rawRange, length: string.length) + guard range.length > 0 else { + return nil + } + let nsString = string.string as NSString let substring = nsString.substring(with: range) let offset = range.location diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift index 92bd0fa3b8..f4df239aa5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTodoBubbleContentNode/Sources/ChatMessageTodoBubbleContentNode.swift @@ -769,10 +769,10 @@ private final class ChatMessageTodoItemNode: ASDisplayNode { linkColor: messageTheme.linkTextColor, baseFont: presentationData.messageFont, linkFont: presentationData.messageFont, - boldFont: presentationData.messageFont, - italicFont: presentationData.messageFont, - boldItalicFont: presentationData.messageFont, - fixedFont: presentationData.messageFont, + boldFont: presentationData.messageBoldFont, + italicFont: presentationData.messageItalicFont, + boldItalicFont: presentationData.messageBoldItalicFont, + fixedFont: presentationData.messageFixedFont, blockQuoteFont: presentationData.messageFont, underlineLinks: underlineLinks, message: message diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/BUILD index 87d31d072a..0b47a88771 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/BUILD @@ -10,7 +10,6 @@ swift_library( "-warnings-as-errors", ], deps = [ - "//submodules/Postbox", "//submodules/Display", "//submodules/AsyncDisplayKit", "//submodules/SSignalKit/SwiftSignalKit", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/Sources/ChatMessageUnsupportedBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/Sources/ChatMessageUnsupportedBubbleContentNode.swift index eb8e110712..588d57296b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/Sources/ChatMessageUnsupportedBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode/Sources/ChatMessageUnsupportedBubbleContentNode.swift @@ -1,6 +1,5 @@ import Foundation import UIKit -import Postbox import Display import AsyncDisplayKit import SwiftSignalKit diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 636399f4ef..c0da699d9b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -152,7 +152,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent } self.contentNode.requestUpdateLayout = { [weak self] in if let strongSelf = self, let item = strongSelf.item { - let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false) + let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false, nil) } } self.contentNode.defaultContentAction = { [weak self] in @@ -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) @@ -265,19 +270,19 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent if case .full = automaticDownload { automaticPlayback = true } else { - automaticPlayback = item.context.account.postbox.mediaBox.completedResourcePath(file.resource) != nil + automaticPlayback = item.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.resource.id)) != nil } } switch type { - case .instagram, .twitter: - if automaticPlayback { - mainMedia = webpage.story ?? webpage.file ?? webpage.image - } else { - mainMedia = webpage.story ?? webpage.image ?? webpage.file - } - default: + case .instagram, .twitter: + if automaticPlayback { mainMedia = webpage.story ?? webpage.file ?? webpage.image + } else { + mainMedia = webpage.story ?? webpage.image ?? webpage.file + } + default: + mainMedia = webpage.story ?? webpage.file ?? webpage.image } let themeMimeType = "application/x-tgtheme-ios" @@ -517,6 +522,16 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent actionTitle = item.presentationData.strings.Chat_ContactChannel case "telegram_newbot": actionTitle = item.presentationData.strings.Chat_CreateBotLink + case "telegram_aicomposetone": + actionTitle = "VIEW STYLE" + + for attribute in webpage.attributes { + if case let .aiTextStyle(aiTextStyle) = attribute { + if let file = item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: aiTextStyle.emojiFileId)] { + mediaAndFlags = ([file], [.preferMediaInline]) + } + } + } default: break } diff --git a/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/BUILD b/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/BUILD index ee2fec7f98..2f4bd201dd 100644 --- a/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/Display", "//submodules/AsyncDisplayKit", "//submodules/SSignalKit/SwiftSignalKit", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/TelegramPresentationData", "//submodules/TextFormat", diff --git a/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift index 9870951c4e..b821e104da 100644 --- a/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatNewThreadInfoItem/Sources/ChatNewThreadInfoItem.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TextFormat @@ -80,7 +79,7 @@ public final class ChatNewThreadInfoItemNode: ListViewItemNode, ASGestureRecogni public let offsetContainer: ASDisplayNode public let titleNode: TextNode public let subtitleNode: TextNode - let arrowView: UIImageView + var arrowView: UIImageView? let iconBackground: SimpleLayer var icon = ComponentView() @@ -106,8 +105,6 @@ public final class ChatNewThreadInfoItemNode: ListViewItemNode, ASGestureRecogni self.subtitleNode.isUserInteractionEnabled = false self.subtitleNode.displaysAsynchronously = false - self.arrowView = UIImageView() - super.init(layerBacked: false, rotated: true) self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) @@ -231,8 +228,14 @@ public final class ChatNewThreadInfoItemNode: ListViewItemNode, ASGestureRecogni if strongSelf.iconBackground.superlayer == nil { strongSelf.offsetContainer.layer.addSublayer(strongSelf.iconBackground) } - if strongSelf.arrowView.superview == nil { - strongSelf.offsetContainer.view.addSubview(strongSelf.arrowView) + + let arrowView: UIImageView + if let current = self?.arrowView { + arrowView = current + } else { + arrowView = UIImageView() + strongSelf.arrowView = arrowView + strongSelf.offsetContainer.view.addSubview(arrowView) } let iconComponent = AnyComponent(BundleIconComponent( @@ -265,15 +268,15 @@ public final class ChatNewThreadInfoItemNode: ListViewItemNode, ASGestureRecogni contentOriginY += subtitleLayout.size.height contentOriginY += 20.0 - if strongSelf.arrowView.image == nil { - strongSelf.arrowView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: .white)?.withRenderingMode(.alwaysTemplate) + if arrowView.image == nil { + arrowView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Search/DownButton"), color: .white)?.withRenderingMode(.alwaysTemplate) } - strongSelf.arrowView.tintColor = primaryTextColor.withMultipliedAlpha(0.5) - if let image = strongSelf.arrowView.image { + arrowView.tintColor = primaryTextColor.withMultipliedAlpha(0.5) + if let image = arrowView.image { let scaleFactor: CGFloat = 0.8 let imageSize = CGSize(width: floor(image.size.width * scaleFactor), height: floor(image.size.height * scaleFactor)) let arrowFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - imageSize.width) / 2.0), y: backgroundFrame.minY + backgroundFrame.height - 8.0 - imageSize.height), size: imageSize) - strongSelf.arrowView.frame = arrowFrame + arrowView.frame = arrowFrame } if strongSelf.backgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { diff --git a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD index b8e4d8a129..f489c8f76f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/BUILD @@ -14,7 +14,6 @@ swift_library( "//submodules/ComponentFlow", "//submodules/Display", "//submodules/TelegramCore", - "//submodules/Postbox", "//submodules/AccountContext", "//submodules/AvatarNode", "//submodules/TextFormat", diff --git a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift index d522989b4d..58ab8e009c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift +++ b/submodules/TelegramUI/Components/Chat/ChatOverscrollControl/Sources/ChatOverscrollControl.swift @@ -3,7 +3,6 @@ import ComponentFlow import Display import AsyncDisplayKit import TelegramCore -import Postbox import AccountContext import AvatarNode import TextFormat diff --git a/submodules/TelegramUI/Components/Chat/ChatPanelsComponent/BUILD b/submodules/TelegramUI/Components/Chat/ChatPanelsComponent/BUILD index 49c5e938e3..c3e82286e8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatPanelsComponent/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatPanelsComponent/BUILD @@ -10,7 +10,6 @@ swift_library( "-warnings-as-errors", ], deps = [ - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/Display", "//submodules/AsyncDisplayKit", diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift index f09dfe4ae6..b9efa55c34 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 @@ -511,7 +511,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { } strongSelf.animatedStickerNode = animatedStickerNode strongSelf.emojiContainerNode.insertSubnode(animatedStickerNode, belowSubnode: strongSelf.placeholderNode) - let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let pathPrefix = item.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.resource.id)) animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix)) animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) @@ -519,7 +519,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { animatedStickerNode.autoplay = true animatedStickerNode.visibility = strongSelf.visibilityStatus - strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).startStrict()) + strongSelf.stickerFetchedDisposable.set(item.context.engine.resources.fetch(reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource), userLocation: .other, userContentType: .sticker).startStrict()) let thumbnailDimensions = PixelDimensions(width: 512, height: 512) strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: thumbnailDimensions.cgSize) @@ -571,7 +571,7 @@ public final class ChatQrCodeScreenImpl: ViewController, ChatQrCodeScreen { public static let themeCrossfadeDelay: Double = 0.05 public enum Subject { - case peer(peer: Peer, threadId: Int64?, temporary: Bool) + case peer(peer: EnginePeer, threadId: Int64?, temporary: Bool) case messages([Message]) public var fileName: String { @@ -580,7 +580,7 @@ public final class ChatQrCodeScreenImpl: ViewController, ChatQrCodeScreen { var result: String if let addressName = peer.addressName, !addressName.isEmpty { result = "t_me-\(peer.addressName ?? "")" - } else if let peer = peer as? TelegramUser { + } else if case let .user(peer) = peer { result = "t_me-\(peer.phone ?? "")" } else { result = "t_me-\(Int32.random(in: 0 ..< Int32.max))" @@ -1576,7 +1576,7 @@ private protocol ContentNode: ASDisplayNode { private class QrContentNode: ASDisplayNode, ContentNode { private let context: AccountContext - private let peer: Peer + private let peer: EnginePeer private let threadId: Int64? private let isStatic: Bool private let temporary: Bool @@ -1618,7 +1618,7 @@ private class QrContentNode: ASDisplayNode, ContentNode { private var tokenUpdated = false var requestNextToken: () -> Void = {} - init(context: AccountContext, peer: Peer, threadId: Int64?, isStatic: Bool = false, temporary: Bool) { + init(context: AccountContext, peer: EnginePeer, threadId: Int64?, isStatic: Bool = false, temporary: Bool) { self.context = context self.peer = peer self.threadId = threadId @@ -1706,7 +1706,7 @@ private class QrContentNode: ASDisplayNode, ContentNode { self.avatarNode = ImageNode() self.avatarNode.displaysAsynchronously = false - self.avatarNode.setSignal(peerAvatarCompleteImage(account: context.account, peer: EnginePeer(peer), size: CGSize(width: 180.0, height: 180.0), font: avatarPlaceholderFont(size: 78.0), fullSize: true)) + self.avatarNode.setSignal(peerAvatarCompleteImage(account: context.account, peer: peer, size: CGSize(width: 180.0, height: 180.0), font: avatarPlaceholderFont(size: 78.0), fullSize: true)) super.init() @@ -1739,9 +1739,9 @@ private class QrContentNode: ASDisplayNode, ContentNode { var codeLink: String if let addressName = peer.addressName, !addressName.isEmpty { codeLink = "https://t.me/\(peer.addressName ?? "")" - } else if let peer = peer as? TelegramUser { + } else if case let .user(peer) = peer { codeLink = "https://t.me/+\(peer.phone ?? "")" - } else if let _ = peer as? TelegramChannel { + } else if case .channel = peer { codeLink = "https://t.me/c/\(peer.id.id._internalGetInt64Value())" } else { codeLink = "" @@ -2502,9 +2502,9 @@ private enum RenderVideoResult { } private func renderVideo(context: AccountContext, backgroundImage: UIImage, userLocation: MediaResourceUserLocation, media: TelegramMediaFile, videoFrame: CGRect, completion: @escaping (URL?) -> Void) { - let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: userLocation, mediaReference: AnyMediaReference.standalone(media: media)) + let _ = (fetchMediaData(context: context, userLocation: userLocation, mediaReference: AnyMediaReference.standalone(media: media)) |> deliverOnMainQueue).startStandalone(next: { value, isImage in - guard case let .data(data) = value, data.complete else { + guard case let .data(data) = value, data.isComplete else { return } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift index 21f7c80be9..b315078b7e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift @@ -20,7 +20,7 @@ public final class ChatRecentActionsController: TelegramBaseController { } private let context: AccountContext - private let peer: Peer + private let peer: EnginePeer private let initialAdminPeerId: PeerId? let starsState: StarsRevenueStats? @@ -39,7 +39,7 @@ public final class ChatRecentActionsController: TelegramBaseController { private var adminsDisposable: Disposable? - public init(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) { + public init(context: AccountContext, peer: EnginePeer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) { self.context = context self.peer = peer self.initialAdminPeerId = adminPeerId @@ -197,7 +197,7 @@ public final class ChatRecentActionsController: TelegramBaseController { let rightBarButton = ChatNavigationButton(action: .search(hasTags: false), buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch))) self.rightBarButton = rightBarButton - self.titleView.title = CounterControllerTitle(title: EnginePeer(peer).compactDisplayTitle, counter: self.presentationData.strings.Channel_AdminLog_TitleAllEvents) + self.titleView.title = CounterControllerTitle(title: peer.compactDisplayTitle, counter: self.presentationData.strings.Channel_AdminLog_TitleAllEvents) let chatTheme = self.context.account.postbox.peerView(id: peer.id) |> map { view -> ChatTheme? in @@ -274,7 +274,7 @@ public final class ChatRecentActionsController: TelegramBaseController { } override public func loadDisplayNode() { - self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer, presentationData: self.presentationData, pushController: { [weak self] c in + self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer._asPeer(), presentationData: self.presentationData, pushController: { [weak self] c in (self?.navigationController as? NavigationController)?.pushViewController(c) }, presentController: { [weak self] c, t, a in self?.present(c, in: t, with: a, blockInteraction: true) @@ -356,12 +356,12 @@ public final class ChatRecentActionsController: TelegramBaseController { var adminPeers: [EnginePeer] = [] if let result { for participant in result { - adminPeers.append(EnginePeer(participant.peer)) + adminPeers.append(participant.peer) } } let controller = RecentActionsSettingsSheet( context: self.context, - peer: EnginePeer(self.peer), + peer: self.peer, adminPeers: adminPeers, initialValue: RecentActionsSettingsSheet.Value( events: self.controllerNode.filter.events, @@ -380,7 +380,7 @@ public final class ChatRecentActionsController: TelegramBaseController { } private func updateTitle() { - let title = EnginePeer(self.peer).compactDisplayTitle + let title = self.peer.compactDisplayTitle let subtitle: String if self.controllerNode.filter.isEmpty { subtitle = self.presentationData.strings.Channel_AdminLog_TitleAllEvents diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index f21df27321..e1408ee47d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -357,8 +357,15 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { return .single(peer?._asPeer()) } } else { - resolveSignal = context.account.postbox.loadedPeerWithId(strongSelf.peer.id) - |> map(Optional.init) + resolveSignal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.peer.id)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + |> map { Optional($0._asPeer()) } } strongSelf.resolvePeerByNameDisposable.set((resolveSignal |> deliverOnMainQueue).startStrict(next: { peer in @@ -646,7 +653,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, sendGift: { _ in }, openUniqueGift: { _ in }, openMessageFeeException: { - }, requestMessageUpdate: { _, _ in + }, requestMessageUpdate: { _, _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { }, scrollToMessageId: { _ in @@ -659,7 +666,10 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, requestToggleTodoMessageItem: { _, _, _ in }, displayTodoToggleUnavailable: { _ in }, openStarsPurchase: { _ in - }, openRankInfo: { _, _, _ in }, openSetPeerAvatar: {}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, + }, openRankInfo: { _, _, _ in + }, openSetPeerAvatar: { + }, displayPollRestrictedToast: { _ in + }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: self.backgroundNode)) self.controllerInteraction = controllerInteraction @@ -1001,7 +1011,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if peer is TelegramChannel, let navigationController = strongSelf.getNavigationController() { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), peekData: peekData, animated: true)) } else { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: EnginePeer(peer), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { strongSelf.pushController(infoController) } } @@ -1021,7 +1031,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { |> deliverOnMainQueue).startStrict(next: { [weak self] peer in if let strongSelf = self { if let peer = peer { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { strongSelf.pushController(infoController) } } @@ -1472,7 +1482,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case .sendGift: break - case .chats, .contacts, .compose, .postStory, .settings, .unknownDeepLink, .oauth, .createBot: + case .chats, .contacts, .compose, .postStory, .settings, .unknownDeepLink, .oauth, .createBot, .textStyle: break } } @@ -1480,7 +1490,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } private func presentAutoremoveSetup() { - /*let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peer.id, style: .default, mode: .autoremove, currentTime: currentValue, dismissByTapOutside: true, completion: { [weak self] value in + /*let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peer.id, style: .default, mode: .autoremove, currentTime: currentValue, completion: { [weak self] value in guard let strongSelf = self else { return } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift index 7a597f83b0..cc001bf972 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift @@ -213,7 +213,7 @@ private enum ChatRecentActionsFilterEntry: ItemListNodeEntry { peerText = strings.ChatAdmins_AdminLabel.lowercased() } } - return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(participant.peer), presence: nil, text: .text(peerText, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: checked, style: .check), enabled: true, selectable: true, sectionId: self.section, action: { + return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: nil, text: .text(peerText, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: checked, style: .check), enabled: true, selectable: true, sectionId: self.section, action: { arguments.toggleAdmin(participant.peer.id) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }) @@ -442,7 +442,7 @@ public func channelRecentActionsFilterController(context: AccountContext, update antiSpamBotPeerPromise.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: antiSpamBotId)) |> map { peer in if let peer = peer, case let .user(user) = peer { - return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: user) + return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer) } else { return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index ce0cecf39f..929b524704 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -670,9 +670,9 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } peers[peer.id] = peer for (_, peer) in participant.peers { - peers[peer.id] = peer + peers[peer.id] = peer._asPeer() } - peers[participant.peer.id] = participant.peer + peers[participant.peer.id] = participant.peer._asPeer() let action: TelegramMediaActionType action = TelegramMediaActionType.addedMembers(peerIds: [participant.peer.id]) @@ -716,7 +716,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } if (prevBanInfo == nil || !prevBanInfo!.rights.flags.contains(.banReadMessages)) && newFlags.contains(.banReadMessages) { - appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageKickedName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageKickedNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageKickedName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageKickedNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in var result: [MessageTextEntityType] = [] if index == 0 { result.append(.TextMention(peerId: new.peer.id)) @@ -727,7 +727,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { }, to: &text, entities: &entities) text += "\n" } else if isBroadcast, newBanInfo == nil, prevBanInfo != nil { - appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageUnkickedName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageUnkickedNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageUnkickedName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageUnkickedNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in var result: [MessageTextEntityType] = [] if index == 0 { result.append(.TextMention(peerId: new.peer.id)) @@ -737,7 +737,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return result }, to: &text, entities: &entities) } else { - appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRestrictedName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageRestrictedNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRestrictedName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageRestrictedNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in var result: [MessageTextEntityType] = [] if index == 0 { result.append(.TextMention(peerId: new.peer.id)) @@ -776,6 +776,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { (.banSendStickers, self.presentationData.strings.Channel_AdminLog_BanSendStickersAndGifs), (.banEmbedLinks, self.presentationData.strings.Channel_AdminLog_BanEmbedLinks), (.banSendPolls, self.presentationData.strings.Channel_AdminLog_SendPolls), + (.banSendReactions, self.presentationData.strings.Channel_AdminLog_SendReactions), (.banAddMembers, self.presentationData.strings.Channel_AdminLog_AddMembers), (.banEditRank, self.presentationData.strings.Channel_AdminLog_EditRankOwn), (.banPinMessages, self.presentationData.strings.Channel_AdminLog_PinMessages), @@ -825,7 +826,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { var entities: [MessageTextEntity] = [] if case .member = prev.participant, case .creator = new.participant { - appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageTransferedName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageTransferedNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageTransferedName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageTransferedNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in var result: [MessageTextEntityType] = [] if index == 0 { result.append(.TextMention(peerId: new.peer.id)) @@ -839,7 +840,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if case let .creator(_, prevAdminInfo, prevRank) = prev.participant, case let .creator(_, newAdminInfo, newRank) = new.participant, (prevRank != newRank || prevAdminInfo?.rights.rights.contains(.canBeAnonymous) != newAdminInfo?.rights.rights.contains(.canBeAnonymous)) { if prevRank != newRank { - appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRankNameNew(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), newRank ?? "") : self.presentationData.strings.Channel_AdminLog_MessageRankUsernameNew(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!, newRank ?? ""), generateEntities: { index in + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRankNameNew(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), newRank ?? "") : self.presentationData.strings.Channel_AdminLog_MessageRankUsernameNew(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!, newRank ?? ""), generateEntities: { index in var result: [MessageTextEntityType] = [] if index == 0 { result.append(.TextMention(peerId: new.peer.id)) @@ -867,7 +868,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if prevAdminInfo?.rights.rights.contains(flag) != newAdminInfo?.rights.rights.contains(flag) { if !appendedRightsHeader { appendedRightsHeader = true - appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessagePromotedName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessagePromotedNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessagePromotedName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessagePromotedNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in var result: [MessageTextEntityType] = [] if index == 0 { result.append(.TextMention(peerId: new.peer.id)) @@ -940,7 +941,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if !appendedRightsHeader { appendedRightsHeader = true if prevAdminRights == nil { - appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageAddedAdminName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageAddedAdminNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageAddedAdminName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageAddedAdminNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in var result: [MessageTextEntityType] = [] if index == 0 { result.append(.TextMention(peerId: new.peer.id)) @@ -952,7 +953,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return result }, to: &text, entities: &entities) } else { - appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in var result: [MessageTextEntityType] = [] if index == 0 { result.append(.TextMention(peerId: new.peer.id)) @@ -970,7 +971,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if !prevFlags.isEmpty && newFlags.isEmpty { if !appendedRightsHeader { appendedRightsHeader = true - appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in var result: [MessageTextEntityType] = [] if index == 0 { result.append(.TextMention(peerId: new.peer.id)) @@ -987,7 +988,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { if prevFlags.contains(flag) != newFlags.contains(flag) { if !appendedRightsHeader { appendedRightsHeader = true - appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessagePromotedName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessagePromotedNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessagePromotedName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessagePromotedNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in var result: [MessageTextEntityType] = [] if index == 0 { result.append(.TextMention(peerId: new.peer.id)) @@ -1023,7 +1024,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return result }, to: &text, entities: &entities) } else { - appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRankNameNew(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), newRank ?? "") : self.presentationData.strings.Channel_AdminLog_MessageRankUsernameNew(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!, newRank ?? ""), generateEntities: { index in + appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRankNameNew(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), newRank ?? "") : self.presentationData.strings.Channel_AdminLog_MessageRankUsernameNew(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!, newRank ?? ""), generateEntities: { index in var result: [MessageTextEntityType] = [] if index == 0 { result.append(.TextMention(peerId: new.peer.id)) @@ -1141,6 +1142,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { (.banSendStickers, self.presentationData.strings.Channel_AdminLog_BanSendStickersAndGifs), (.banEmbedLinks, self.presentationData.strings.Channel_AdminLog_BanEmbedLinks), (.banSendPolls, self.presentationData.strings.Channel_AdminLog_SendPolls), + (.banSendReactions, self.presentationData.strings.Channel_AdminLog_SendReactions), (.banAddMembers, self.presentationData.strings.Channel_AdminLog_AddMembers), (.banEditRank, self.presentationData.strings.Channel_AdminLog_EditRank), (.banPinMessages, self.presentationData.strings.Channel_AdminLog_PinMessages), @@ -2270,9 +2272,9 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { } peers[peer.id] = peer for (_, peer) in new.peers { - peers[peer.id] = peer + peers[peer.id] = peer._asPeer() } - peers[new.peer.id] = new.peer + peers[new.peer.id] = new.peer._asPeer() var text: String = "" var entities: [MessageTextEntity] = [] @@ -2367,6 +2369,8 @@ private let deletedMessagesDisplayedLimit = 4 func chatRecentActionsEntries(entries: [ChannelAdminEventLogEntry], presentationData: ChatPresentationData, expandedDeletedMessages: Set, currentDeletedHeaderMessages: inout Set) -> [ChatRecentActionsEntry] { var result: [ChatRecentActionsEntry] = [] var deleteMessageEntries: [ChannelAdminEventLogEntry] = [] + let previousDeletedHeaderMessages = currentDeletedHeaderMessages + currentDeletedHeaderMessages = Set() func appendCurrentDeleteEntries() { if !deleteMessageEntries.isEmpty, let lastEntry = deleteMessageEntries.last, let lastMessageId = lastEntry.event.action.messageId { @@ -2394,9 +2398,16 @@ func chatRecentActionsEntries(entries: [ChannelAdminEventLogEntry], presentation var skipAppendingGeneralEntry = false if case let .deleteMessage(message) = entry.event.action { var skipAppendingDeletionEntry = false - if currentDeleteMessageEvent == nil || (currentDeleteMessageEvent!.peerId == entry.event.peerId && abs(currentDeleteMessageEvent!.date - entry.event.date) < 5 && !currentDeletedHeaderMessages.contains(message.id)) { + let belongsToCurrentDeleteGroup: Bool + if let currentDeleteMessageEvent { + belongsToCurrentDeleteGroup = currentDeleteMessageEvent.peerId == entry.event.peerId && abs(currentDeleteMessageEvent.date - entry.event.date) < 5 } else { - if currentDeletedHeaderMessages.contains(message.id) { + belongsToCurrentDeleteGroup = true + } + let wasPreviousHeaderMessage = previousDeletedHeaderMessages.contains(message.id) + if currentDeleteMessageEvent == nil || (belongsToCurrentDeleteGroup && !wasPreviousHeaderMessage) { + } else { + if belongsToCurrentDeleteGroup && wasPreviousHeaderMessage { deleteMessageEntries.append(entry) skipAppendingDeletionEntry = true } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsSearchNavigationContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsSearchNavigationContentNode.swift index 6c6b603669..4f21496d02 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsSearchNavigationContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsSearchNavigationContentNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import SearchBarNode diff --git a/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/BUILD index 8db571013a..1597e34a7d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/TelegramCore", - "//submodules/Postbox", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramPresentationData", "//submodules/MediaPlayer:UniversalMediaPlayer", diff --git a/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/Sources/ChatRecordingPreviewInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/Sources/ChatRecordingPreviewInputPanelNode.swift index f8c3ce8074..56ca3620bf 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/Sources/ChatRecordingPreviewInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecordingPreviewInputPanelNode/Sources/ChatRecordingPreviewInputPanelNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import TelegramCore -import Postbox import SwiftSignalKit import TelegramPresentationData import UniversalMediaPlayer @@ -392,7 +391,7 @@ public final class ChatRecordingPreviewInputPanelNodeImpl: ChatInputPanelNode { })*/ } - override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) -> CGFloat { let waveformBackgroundFrame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: CGSize(width: width - 3.0 * 2.0, height: 40.0 - 3.0 * 2.0)) if self.presentationInterfaceState != interfaceState { diff --git a/submodules/TelegramUI/Components/Chat/ChatSearchNavigationContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatSearchNavigationContentNode/BUILD index 69f3988816..aba5e2f5f4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSearchNavigationContentNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatSearchNavigationContentNode/BUILD @@ -14,7 +14,6 @@ swift_library( "//submodules/Display", "//submodules/TelegramPresentationData", "//submodules/ChatPresentationInterfaceState", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SearchBarNode", "//submodules/LocalizedPeerData", diff --git a/submodules/TelegramUI/Components/Chat/ChatSearchNavigationContentNode/Sources/ChatSearchNavigationContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatSearchNavigationContentNode/Sources/ChatSearchNavigationContentNode.swift index 20c1e1b8fe..5b255bed37 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSearchNavigationContentNode/Sources/ChatSearchNavigationContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSearchNavigationContentNode/Sources/ChatSearchNavigationContentNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import SearchBarNode diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift index a0552e2033..d1b611b2b7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAsContextMenu/Sources/ChatSendAsPeerListContextItem.swift @@ -70,8 +70,8 @@ private final class ChatSendAsPeerListContextItemNode: ASDisplayNode, ContextMen if peer.peer.id.namespace == Namespaces.Peer.CloudUser { subtitle = presentationData.strings.VoiceChat_PersonalAccount } else if let subscribers = peer.subscribers { - if let peer = peer.peer as? TelegramChannel { - if case .broadcast = peer.info { + if case let .channel(channel) = peer.peer { + if case .broadcast = channel.info { subtitle = presentationData.strings.Conversation_StatusSubscribers(subscribers) } else { subtitle = presentationData.strings.VoiceChat_DiscussionGroup @@ -86,7 +86,7 @@ private final class ChatSendAsPeerListContextItemNode: ASDisplayNode, ContextMen selectedItemIndex = i } let extendedAvatarSize = CGSize(width: 35.0, height: 35.0) - let avatarSignal = peerAvatarCompleteImage(account: item.context.account, peer: EnginePeer(peer.peer), size: avatarSize) + let avatarSignal = peerAvatarCompleteImage(account: item.context.account, peer: peer.peer, size: avatarSize) |> map { image -> UIImage? in if isSelected, let image = image { return generateImage(extendedAvatarSize, rotatedContext: { size, context in @@ -107,18 +107,18 @@ private final class ChatSendAsPeerListContextItemNode: ASDisplayNode, ContextMen } } - let action = ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), textIcon: { theme in + let action = ContextMenuActionItem(text: peer.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), textIcon: { theme in return !item.isPremium && peer.isPremiumRequired ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.badgeInactiveFillColor) : nil }, action: { _, f in f(.default) if !item.isPremium && peer.isPremiumRequired { - item.presentToast(EnginePeer(peer.peer)) + item.presentToast(peer.peer) return } if peer.peer.id != item.selectedPeerId { - item.action(EnginePeer(peer.peer)) + item.action(peer.peer) } }) let actionNode = ContextActionNode(presentationData: presentationData, action: action, getController: getController, actionSelected: actionSelected, requestLayout: {}, requestUpdateAction: { _, _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 07a25bb7cd..f5069b7768 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -86,7 +86,7 @@ public final class ChatSendContactMessageContextPreview: UIView, ChatSendMessage for peer in self.contactPeers { switch peer { case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + guard case let .user(contact) = contact, let phoneNumber = contact.phone else { continue } let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") @@ -500,7 +500,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, sendGift: { _ in }, openUniqueGift: { _ in }, openMessageFeeException: { - }, requestMessageUpdate: { _, _ in + }, requestMessageUpdate: { _, _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { }, scrollToMessageId: { _ in @@ -513,7 +513,10 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, requestToggleTodoMessageItem: { _, _, _ in }, displayTodoToggleUnavailable: { _ in }, openStarsPurchase: { _ in - }, openRankInfo: { _, _, _ in }, openSetPeerAvatar: {}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, + }, openRankInfo: { _, _, _ in + }, openSetPeerAvatar: { + }, displayPollRestrictedToast: { _ in + }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: self.context, backgroundNode: self.wallpaperBackgroundNode)) let associatedData = ChatMessageItemAssociatedData( @@ -530,7 +533,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess accountPeer: nil ) - let entryAttributes = ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false) + let entryAttributes = ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false) let items = self.messages.map { message -> ChatMessageBubbleContentItem in return ChatMessageBubbleContentItem( diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index 8942eafd60..329f038e4c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -2293,7 +2293,7 @@ private final class ChatSendStarsScreenComponent: Component { if let peerInfoController = context.sharedContext.makePeerInfoController( context: context, updatedPresentationData: nil, - peer: peer._asPeer(), + peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, diff --git a/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/BUILD b/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/BUILD index c70d395426..3933f52171 100644 --- a/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/BUILD @@ -14,7 +14,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", "//submodules/TelegramCore", - "//submodules/Postbox", "//submodules/TelegramPresentationData", "//submodules/AccountContext", "//submodules/UndoUI", diff --git a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift index 19fa39ba65..6de9dc2e22 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSideTopicsPanel/Sources/ChatSideTopicsPanel.swift @@ -941,8 +941,8 @@ public final class ChatSideTopicsPanel: Component { } let size = CGSize(width: contentSize, height: availableSize.height) - let iconFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - iconSize.height) * 0.5)), size: iconSize) - let titleFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + spacing, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) + let iconFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - iconSize.height) * 0.5) - 1.0), size: iconSize) + let titleFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + spacing, y: floor((size.height - titleSize.height) * 0.5) - 1.0), size: titleSize) if let icon = self.icon { if let avatarNode = self.avatarNode { @@ -1422,7 +1422,7 @@ public final class ChatSideTopicsPanel: Component { let contentSize: CGFloat = leftInset + rightInset + titleSize.width let size = CGSize(width: contentSize, height: availableSize.height) - let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - titleSize.height) * 0.5) - 1.0), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { @@ -1502,7 +1502,6 @@ public final class ChatSideTopicsPanel: Component { self.scrollContainerView = UIView() self.scrollViewMask = UIImageView() self.scrollContainerView.mask = self.scrollViewMask - //self.scrollContainerView.addSubview(self.scrollViewMask) super.init(frame: frame) @@ -1599,6 +1598,20 @@ public final class ChatSideTopicsPanel: Component { } } if isPinned { + if !seenPinnedItems { + switch component.location { + case .side: + beforePinnedItemsPosition = item.frame.minY + case .top, .bottom: + beforePinnedItemsPosition = item.frame.minX + } + switch component.location { + case .side: + afterPinnedItemsPosition = item.frame.maxY + case .top, .bottom: + afterPinnedItemsPosition = item.frame.maxX + } + } seenPinnedItems = true } else { if !seenPinnedItems { @@ -1885,7 +1898,7 @@ public final class ChatSideTopicsPanel: Component { itemFrame = CGRect(origin: CGPoint(x: 8.0 + 4.0, y: directionContainerInset + 6.0), size: itemSize) directionContainerInset += itemSize.height case .top, .bottom: - itemFrame = CGRect(origin: CGPoint(x: 12.0, y: 6.0), size: itemSize) + itemFrame = CGRect(origin: CGPoint(x: 12.0, y: 5.0), size: itemSize) directionContainerInset += itemSize.width - 20.0 } diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift index 0a453ead98..1569743128 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputActionButtonsNode/Sources/ChatTextInputActionButtonsNode.swift @@ -173,6 +173,10 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag public var customSendColor: UIColor? public var isSendDisabled: Bool = false + private var slowmodeProgressTimestamp: (duration: Int32, timestamp: Int32)? + private var slowmodeProgressTimer: Foundation.Timer? + private var slowmodeProgressLayer: SimpleShapeLayer? + public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { self.context = context self.presentationContext = presentationContext @@ -255,6 +259,10 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag } } + deinit { + self.slowmodeProgressTimer?.invalidate() + } + override public func didLoad() { super.didLoad() @@ -357,8 +365,72 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag transition.updateFrame(layer: self.micButton.layer, frame: CGRect(origin: CGPoint(), size: size)) self.micButton.layoutItems() + var sendSlowmodeTimerTimestamp: (duration: Int32, timestamp: Int32)? + if let slowmodeState = interfaceState.slowmodeState { + switch slowmodeState.variant { + case .pendingMessages: + break + case let .timestamp(timeoutTimestamp): + sendSlowmodeTimerTimestamp = (slowmodeState.timeout, timeoutTimestamp) + } + } + let sendButtonBackgroundFrame = CGRect(origin: CGPoint(), size: innerSize).insetBy(dx: 3.0, dy: 3.0) - transition.updateFrame(view: self.sendButtonBackgroundView, frame: sendButtonBackgroundFrame) + + let slowmodeInset: CGFloat = 4.0 + + self.slowmodeProgressTimestamp = sendSlowmodeTimerTimestamp + if sendSlowmodeTimerTimestamp != nil { + let slowmodeProgressLayer: SimpleShapeLayer + var slowmodeProgressTransition = transition + if let current = self.slowmodeProgressLayer { + slowmodeProgressLayer = current + } else { + slowmodeProgressTransition = .immediate + slowmodeProgressLayer = SimpleShapeLayer() + self.slowmodeProgressLayer = slowmodeProgressLayer + self.sendButtonBackgroundView.layer.superlayer?.insertSublayer(slowmodeProgressLayer, below: self.sendButtonBackgroundView.layer) + + slowmodeProgressLayer.fillColor = nil + slowmodeProgressLayer.lineWidth = 2.0 + slowmodeProgressLayer.lineCap = .round + } + + slowmodeProgressLayer.strokeColor = (self.customSendColor ?? interfaceState.theme.chat.inputPanel.panelControlAccentColor).cgColor + + if slowmodeProgressLayer.bounds.size != sendButtonBackgroundFrame.size { + let pathFrame = CGRect(origin: CGPoint(), size: sendButtonBackgroundFrame.size).insetBy(dx: 2.0, dy: 2.0) + slowmodeProgressLayer.path = UIBezierPath(roundedRect: pathFrame, cornerRadius: pathFrame.height * 0.5).cgPath + } + slowmodeProgressTransition.updateFrame(layer: slowmodeProgressLayer, frame: sendButtonBackgroundFrame) + + if self.slowmodeProgressTimer == nil { + self.slowmodeProgressTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 60.0, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.updateSlowmodeProgress() + }) + } + self.updateSlowmodeProgress() + } else { + if let slowmodeProgressLayer = self.slowmodeProgressLayer { + self.slowmodeProgressLayer = nil + slowmodeProgressLayer.removeFromSuperlayer() + } + if let slowmodeProgressTimer = self.slowmodeProgressTimer { + self.slowmodeProgressTimer = nil + slowmodeProgressTimer.invalidate() + } + } + + ComponentTransition(transition).setPosition(view: self.sendButtonBackgroundView, position: sendButtonBackgroundFrame.center) + ComponentTransition(transition).setBounds(view: self.sendButtonBackgroundView, bounds: CGRect(origin: CGPoint(), size: sendButtonBackgroundFrame.size)) + var sendButtonBackgroundScale: CGFloat = 1.0 + if sendSlowmodeTimerTimestamp != nil, let image = self.sendButtonBackgroundView.image { + sendButtonBackgroundScale = (image.size.height - slowmodeInset * 2.0) / image.size.height + } + transition.updateTransformScale(layer: self.sendButtonBackgroundView.layer, scale: sendButtonBackgroundScale) if self.isSendDisabled { transition.updateTintColor(view: self.sendButtonBackgroundView, color: interfaceState.theme.chat.inputPanel.panelControlAccentColor.withMultiplied(hue: 1.0, saturation: 0.0, brightness: 0.5).withMultipliedAlpha(0.25)) @@ -441,6 +513,18 @@ public final class ChatTextInputActionButtonsNode: ASDisplayNode, ChatSendMessag return innerSize } + private func updateSlowmodeProgress() { + guard let slowmodeProgressLayer = self.slowmodeProgressLayer, let slowmodeProgressTimestamp = self.slowmodeProgressTimestamp else { + return + } + + let timestamp = Date().timeIntervalSince1970 + let timeout = max(0.0, Double(slowmodeProgressTimestamp.timestamp) - timestamp) + let fraction = timeout / max(0.1, Double(slowmodeProgressTimestamp.duration)) + + slowmodeProgressLayer.strokeEnd = CGFloat(fraction) + } + public func updateAccessibility() { self.accessibilityTraits = .button if !self.micButton.alpha.isZero { diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift index a9434330d9..c73df15c44 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelComponent.swift @@ -487,7 +487,6 @@ public final class ChatTextInputPanelComponent: Component { mode: .standard(.default), chatLocation: .peer(id: component.chatPeerId), subject: nil, - peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, @@ -799,7 +798,6 @@ public final class ChatTextInputPanelComponent: Component { mode: .standard(.default), chatLocation: .peer(id: component.chatPeerId), subject: nil, - peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, @@ -845,7 +843,7 @@ public final class ChatTextInputPanelComponent: Component { if let sendAsConfiguration = component.sendAsConfiguration { presentationInterfaceState = presentationInterfaceState.updatedSendAsPeers([SendAsPeer( - peer: sendAsConfiguration.currentPeer._asPeer(), + peer: sendAsConfiguration.currentPeer, subscribers: sendAsConfiguration.subscriberCount.flatMap(Int32.init(clamping:)), isPremiumRequired: sendAsConfiguration.isPremiumLocked )]).updatedShowSendAsPeers(sendAsConfiguration.isSelecting).updatedCurrentSendAsPeerId(sendAsConfiguration.currentPeer.id) @@ -1052,6 +1050,7 @@ public final class ChatTextInputPanelComponent: Component { transition: transition.containedViewLayoutTransition, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), + deviceMetrics: DeviceMetrics.iPhone16Pro, isMediaInputExpanded: false ) diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index f0465c0afe..fcb5d7a3cb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -300,7 +300,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = [] - private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, CGFloat, LayoutMetrics, Bool, Bool)? + private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, CGFloat, LayoutMetrics, Bool, Bool, DeviceMetrics)? private var leftMenuInset: CGFloat = 0.0 private var rightSlowModeInset: CGFloat = 0.0 private var currentTextInputBackgroundWidthOffset: CGFloat = 0.0 @@ -872,15 +872,15 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } self.mediaActionButtons.micButton.offsetRecordingControls = { [weak self] in if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState { - if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout { - let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) + if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded, deviceMetrics) = strongSelf.validLayout { + let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, deviceMetrics: deviceMetrics, isMediaInputExpanded: isMediaInputExpanded) } } } self.mediaActionButtons.micButton.updateCancelTranslation = { [weak self] in if let strongSelf = self, let presentationInterfaceState = strongSelf.presentationInterfaceState { - if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout { - let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) + if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded, deviceMetrics) = strongSelf.validLayout { + let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: presentationInterfaceState, metrics: metrics, deviceMetrics: deviceMetrics, isMediaInputExpanded: isMediaInputExpanded) } } } @@ -1412,10 +1412,10 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } public func requestLayout(transition: ContainedViewLayoutTransition = .immediate) { - guard let presentationInterfaceState = self.presentationInterfaceState, let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded) = self.validLayout else { + guard let presentationInterfaceState = self.presentationInterfaceState, let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded, deviceMetrics) = self.validLayout else { return } - let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: transition, interfaceState: presentationInterfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) + let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, transition: transition, interfaceState: presentationInterfaceState, metrics: metrics, deviceMetrics: deviceMetrics, isMediaInputExpanded: isMediaInputExpanded) } override public func updateLayout( @@ -1430,12 +1430,13 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, + deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool ) -> CGFloat { let isFirstTime = self.validLayout == nil let previousAdditionalSideInsets = self.validLayout?.4 - self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded) + self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, maxOverlayHeight, metrics, isSecondary, isMediaInputExpanded, deviceMetrics) let defaultGlassTintColor: GlassBackgroundView.TintColor let defaultGlassTintWithInnerColor: GlassBackgroundView.TintColor @@ -1450,10 +1451,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg var leftInset = leftInset var rightInset = rightInset - if bottomInset <= 32.0 { - leftInset += 18.0 - rightInset += 18.0 - } + let compactBottomSideInset = self.compactBottomSideInset(bottomInset: bottomInset, deviceMetrics: deviceMetrics) + leftInset += compactBottomSideInset + rightInset += compactBottomSideInset let placeholderColor: UIColor = interfaceState.theme.chat.inputPanel.inputPlaceholderColor @@ -1622,7 +1622,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg currentPeer = sendAsPeers.first?.peer } if let context = self.context, let peer = currentPeer { - self.sendAsAvatarNode.setPeer(context: context, theme: interfaceState.theme, peer: EnginePeer(peer), emptyColor: interfaceState.theme.list.mediaPlaceholderColor) + self.sendAsAvatarNode.setPeer(context: context, theme: interfaceState.theme, peer: peer, emptyColor: interfaceState.theme.list.mediaPlaceholderColor) } } else if let peer = interfaceState.renderedPeer?.peer as? TelegramUser, let _ = peer.botInfo, shouldDisplayMenuButton && interfaceState.editMessageState == nil { hasMenuButton = true @@ -2308,6 +2308,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } } } + sendActionButtonsSize = self.sendActionButtons.updateLayout(size: CGSize(width: 40.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, showTitle: showTitle, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState) mediaActionButtonsSize = self.mediaActionButtons.updateLayout(size: CGSize(width: 40.0, height: minimalHeight), isMediaInputExpanded: isMediaInputExpanded, showTitle: false, currentMessageEffectId: presentationInterfaceState.interfaceState.sendMessageEffect, transition: transition, interfaceState: presentationInterfaceState) } @@ -2897,7 +2898,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg transition.updateAlpha(layer: mediaPreviewPanelNode.tintMaskView.layer, alpha: 1.0) transition.updateFrame(view: mediaPreviewPanelNode.tintMaskView, frame: mediaPreviewPanelFrame) - let _ = mediaPreviewPanelNode.updateLayout(width: mediaPreviewPanelFrame.width, leftInset: 0.0, rightInset: 0.0, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: 40.0, maxOverlayHeight: 40.0, isSecondary: false, transition: mediaPreviewPanelTransition, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: false) + let _ = mediaPreviewPanelNode.updateLayout(width: mediaPreviewPanelFrame.width, leftInset: 0.0, rightInset: 0.0, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: 40.0, maxOverlayHeight: 40.0, isSecondary: false, transition: mediaPreviewPanelTransition, interfaceState: interfaceState, metrics: metrics, deviceMetrics: deviceMetrics, isMediaInputExpanded: false) } else if let mediaPreviewPanelNode = self.mediaPreviewPanelNode { self.mediaPreviewPanelNode = nil let mediaPreviewPanelView = mediaPreviewPanelNode.view @@ -3103,17 +3104,18 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg self.textPlaceholderNode.view.setMonochromaticEffect(tintColor: placeholderColor) self.textInputNode?.textView.accessibilityHint = currentPlaceholder + + if transition.isAnimated, let snapshotView = self.textPlaceholderNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.textPlaceholderNode.frame + self.textPlaceholderNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.textPlaceholderNode.view) + snapshotView.layer.animateAlpha(from: snapshotView.alpha, to: 0.0, duration: 0.22, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + self.textPlaceholderNode.layer.animateAlpha(from: 0.0, to: self.textPlaceholderNode.alpha, duration: 0.18) + } let placeholderSize = self.textPlaceholderNode.updateLayout(CGSize(width: textPlaceholderMaxWidth, height: CGFloat.greatestFiniteMagnitude)) - if transition.isAnimated, let snapshotLayer = self.textPlaceholderNode.layer.snapshotContentTree() { - self.textPlaceholderNode.supernode?.layer.insertSublayer(snapshotLayer, above: self.textPlaceholderNode.layer) - snapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.22, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in - snapshotLayer?.removeFromSuperlayer() - }) - self.textPlaceholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) - } - textPlaceholderSize = placeholderSize } else { textPlaceholderSize = self.textPlaceholderNode.updateLayout(CGSize(width: textPlaceholderMaxWidth, height: CGFloat.greatestFiniteMagnitude)) @@ -3425,6 +3427,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } } if let mediaDraftState = interfaceState.interfaceState.mediaDraftState, case .audio = mediaDraftState.contentType { + viewOnceIsVisible = true recordMoreIsVisible = true } @@ -3436,13 +3439,29 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg transition.updateSublayerTransformOffset(layer: self.clippingNode.layer, offset: CGPoint(x: 0.0, y: clippingDelta))*/ let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme) - let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 50.0 - UIScreenPixel, y: -152.0), size: viewOnceSize) + + var viewOnceButtonY: CGFloat = -105.0 + if isRecording { + if accessoryPanel == nil { + viewOnceButtonY -= 49.0 + } + } + + let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 50.0 - UIScreenPixel, y: viewOnceButtonY), size: viewOnceSize) self.viewOnceButton.bounds = CGRect(origin: .zero, size: viewOnceButtonFrame.size) transition.updatePosition(node: self.viewOnceButton, position: viewOnceButtonFrame.center) if self.viewOnceButton.alpha.isZero && viewOnceIsVisible { self.viewOnceButton.update(isSelected: self.viewOnce, animated: false) } + + transition.updateAlpha(node: self.viewOnceButton, alpha: viewOnceIsVisible ? 1.0 : 0.0) + transition.updateTransformScale(node: self.viewOnceButton, scale: viewOnceIsVisible ? 1.0 : 0.01) + if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.id != interfaceState.accountPeerId && user.botInfo == nil && interfaceState.sendPaidMessageStars == nil { + self.viewOnceButton.isHidden = false + } else { + self.viewOnceButton.isHidden = true + } let recordMoreSize = self.recordMoreButton.update(theme: interfaceState.theme) let recordMoreButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 50.0 - UIScreenPixel, y: -52.0), size: recordMoreSize) @@ -3453,14 +3472,6 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg self.recordMoreButton.update(isSelected: false, animated: false) } - transition.updateAlpha(node: self.viewOnceButton, alpha: viewOnceIsVisible ? 1.0 : 0.0) - transition.updateTransformScale(node: self.viewOnceButton, scale: viewOnceIsVisible ? 1.0 : 0.01) - if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.id != interfaceState.accountPeerId && user.botInfo == nil && interfaceState.sendPaidMessageStars == nil { - self.viewOnceButton.isHidden = false - } else { - self.viewOnceButton.isHidden = true - } - transition.updateAlpha(node: self.recordMoreButton, alpha: recordMoreIsVisible ? 1.0 : 0.0) transition.updateTransformScale(node: self.recordMoreButton, scale: recordMoreIsVisible ? 1.0 : 0.01) @@ -4653,13 +4664,12 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } private func updateTextHeight(animated: Bool) { - if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, _, metrics, _, _) = self.validLayout, let interfaceState = self.presentationInterfaceState { + if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, _, metrics, _, _, deviceMetrics) = self.validLayout, let interfaceState = self.presentationInterfaceState { var leftInset = leftInset var rightInset = rightInset - if bottomInset <= 32.0 { - leftInset += 18.0 - rightInset += 18.0 - } + let compactBottomSideInset = self.compactBottomSideInset(bottomInset: bottomInset, deviceMetrics: deviceMetrics) + leftInset += compactBottomSideInset + rightInset += compactBottomSideInset let baseWidth = width - leftInset - self.leftMenuInset - rightInset - self.rightSlowModeInset + self.currentTextInputBackgroundWidthOffset - additionalSideInsets.right let (_, textFieldHeight, _) = self.calculateTextFieldMetrics(width: baseWidth, sendActionControlsWidth: self.sendActionButtons.bounds.width, maxHeight: maxHeight, metrics: metrics, bottomInset: bottomInset, interfaceState: interfaceState) @@ -5600,12 +5610,8 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } public func frameForInputActionButton() -> CGRect? { - if !self.mediaActionButtons.alpha.isZero { - if self.mediaActionButtons.micButton.alpha.isZero { - return self.mediaActionButtons.frame.insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: -3.0, dy: 0.0) - } else { - return self.mediaActionButtons.frame.insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: -3.0, dy: 0.0) - } + if !self.mediaActionButtons.alpha.isZero && self.mediaActionButtons.frame.minX < self.bounds.width { + return self.mediaActionButtons.frame.insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: -3.0, dy: 0.0) } return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift index 3b80c51c1f..8a664f562a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatUserInfoItem/Sources/ChatUserInfoItem.swift @@ -114,7 +114,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe private var groupsInCommonContext: GroupsInCommonContext? private var groupsInCommonDisposable: Disposable? - private var groupsInCommon: [Peer] = [] + private var groupsInCommon: [EnginePeer] = [] private let disclaimerTextNode: TextNodeWithEntities @@ -436,7 +436,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe guard let self, let item = self.item else { return } - self.groupsInCommon = Array(state.peers.compactMap { $0.peer }.prefix(3)) + self.groupsInCommon = Array(state.peers.compactMap { $0.peer }.prefix(3)).map(EnginePeer.init) self.groupsAvatarsNode.update(context: item.context, peers: self.groupsInCommon, synchronousLoad: true, imageSize: avatarImageSize, imageSpacing: avatarSpacing, borderWidth: avatarBorder) }) } diff --git a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/BUILD b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/BUILD index 09c44b1067..217d311fe4 100644 --- a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/BUILD +++ b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift index fb4c4a6c0f..d3d836cb6c 100644 --- a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift @@ -3,7 +3,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import AccountContext diff --git a/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/BUILD b/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/BUILD index a06f08dcc1..dc29ccd348 100644 --- a/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/Display", "//submodules/AsyncDisplayKit", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AccountContext", diff --git a/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift b/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift index d6e4efc807..ddfe1640ca 100644 --- a/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift +++ b/submodules/TelegramUI/Components/Chat/ManagedDiceAnimationNode/Sources/ManagedDiceAnimationNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import AccountContext @@ -141,9 +140,9 @@ public final class ManagedDiceAnimationNode: ManagedAnimationNode { self.context = context self.emoji = emoji - self.configuration.set(self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + self.configuration.set(self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) |> map { preferencesView -> InteractiveEmojiConfiguration? in - let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue + let appConfiguration: AppConfiguration = preferencesView?.get(AppConfiguration.self) ?? .defaultValue return InteractiveEmojiConfiguration.with(appConfiguration: appConfiguration) }) self.emojis.set(context.engine.stickers.loadedStickerPack(reference: .dice(emoji), forceActualized: false) diff --git a/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/Sources/MergedAvatarsNode.swift b/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/Sources/MergedAvatarsNode.swift index 111a1d8640..3aa48b63ad 100644 --- a/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/Sources/MergedAvatarsNode.swift +++ b/submodules/TelegramUI/Components/Chat/MergedAvatarsNode/Sources/MergedAvatarsNode.swift @@ -24,7 +24,7 @@ private enum PeerAvatarReference: Equatable { } private extension PeerAvatarReference { - init(peer: Peer) { + init(peer: EnginePeer) { if let photo = peer.smallProfileImage, let peerReference = PeerReference(peer) { self = .image(peerReference, photo) } else { @@ -97,7 +97,7 @@ public final class MergedAvatarsNode: ASDisplayNode { self.buttonNode.frame = CGRect(origin: CGPoint(), size: size) } - public func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool, imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat, avatarFontSize: CGFloat = 8.0) { + public func update(context: AccountContext, peers: [EnginePeer], synchronousLoad: Bool, imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat, avatarFontSize: CGFloat = 8.0) { self.imageSize = imageSize self.imageSpacing = imageSpacing self.borderWidthValue = borderWidth diff --git a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/BUILD b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/BUILD index beec526121..e610b4df40 100644 --- a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/BUILD +++ b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/Display", "//submodules/Components/HierarchyTrackingLayer", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", diff --git a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift index 7408d13b04..fc5366cc2b 100644 --- a/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift +++ b/submodules/TelegramUI/Components/Chat/MessageInlineBlockBackgroundView/Sources/MessageInlineBlockBackgroundView.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import HierarchyTrackingLayer -import Postbox import TelegramCore import AnimationCache import MultiAnimationRenderer @@ -506,6 +505,8 @@ public final class MessageInlineBlockBackgroundView: UIView { thirdColor: params.thirdColor, backgroundColor: params.backgroundColor, pattern: params.pattern, + patternTopRightPosition: params.patternTopRightPosition, + patternAlpha: params.patternAlpha, animation: .None ) } diff --git a/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift b/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift index 723007aa78..baafd5d89b 100644 --- a/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift +++ b/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift @@ -254,16 +254,19 @@ private final class QuickShareToastScreenComponent: Component { let contentSize = self.content.update( transition: transition, - component: AnyComponent(MultilineTextComponent(text: .markdown( - text: tooltipText, - attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white), - bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: environment.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0)), - link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white), - linkAttribute: { _ in return nil }) - ))), + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: tooltipText, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white), + bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: environment.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0)), + link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white), + linkAttribute: { _ in return nil }) + ), + maximumNumberOfLines: 2 + )), environment: {}, - containerSize: CGSize(width: availableContentSize.width - contentInsets.left - contentInsets.right - spacing - iconSize.width - actionButtonSize.width - 16.0 - 4.0, height: availableContentSize.height) + containerSize: CGSize(width: availableContentSize.width - contentInsets.left - contentInsets.right - spacing - iconSize.width - actionButtonSize.width - 16.0 - 8.0, height: availableContentSize.height) ) var contentHeight: CGFloat = 0.0 @@ -313,7 +316,7 @@ private final class QuickShareToastScreenComponent: Component { self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: transition.containedViewLayoutTransition) - self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 14.0, transition: transition.containedViewLayoutTransition) + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.height * 0.5, transition: transition.containedViewLayoutTransition) transition.setFrame(view: self.backgroundView, frame: backgroundFrame) transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: backgroundFrame.size)) diff --git a/submodules/TelegramUI/Components/Chat/SavedTagNameAlertController/BUILD b/submodules/TelegramUI/Components/Chat/SavedTagNameAlertController/BUILD index 24344ea62d..6ef0604ffa 100644 --- a/submodules/TelegramUI/Components/Chat/SavedTagNameAlertController/BUILD +++ b/submodules/TelegramUI/Components/Chat/SavedTagNameAlertController/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/Chat/SavedTagNameAlertController/Sources/SavedTagNameAlertController.swift b/submodules/TelegramUI/Components/Chat/SavedTagNameAlertController/Sources/SavedTagNameAlertController.swift index 23f7b9bf18..23a4c4e5bc 100644 --- a/submodules/TelegramUI/Components/Chat/SavedTagNameAlertController/Sources/SavedTagNameAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/SavedTagNameAlertController/Sources/SavedTagNameAlertController.swift @@ -3,7 +3,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import AccountContext diff --git a/submodules/TelegramUI/Components/Chat/SuggestPostAccessoryPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/SuggestPostAccessoryPanelNode/BUILD index 885c055cd0..b1c21063e1 100644 --- a/submodules/TelegramUI/Components/Chat/SuggestPostAccessoryPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/SuggestPostAccessoryPanelNode/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/TelegramCore", - "//submodules/Postbox", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", "//submodules/TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/Chat/SuggestPostAccessoryPanelNode/Sources/SuggestPostAccessoryPanelNode.swift b/submodules/TelegramUI/Components/Chat/SuggestPostAccessoryPanelNode/Sources/SuggestPostAccessoryPanelNode.swift index dfe08a10b5..6a5973c86a 100644 --- a/submodules/TelegramUI/Components/Chat/SuggestPostAccessoryPanelNode/Sources/SuggestPostAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/SuggestPostAccessoryPanelNode/Sources/SuggestPostAccessoryPanelNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import TelegramCore -import Postbox import SwiftSignalKit import Display import TelegramPresentationData diff --git a/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift b/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift index 0f549ecfeb..bb3f83e615 100644 --- a/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift +++ b/submodules/TelegramUI/Components/Chat/TopMessageReactions/Sources/TopMessageReactions.swift @@ -10,7 +10,7 @@ public enum AllowedReactions { case all } -public func peerMessageAllowedReactions(context: AccountContext, message: Message) -> Signal<(allowedReactions: AllowedReactions?, areStarsEnabled: Bool), NoError> { +public func peerMessageAllowedReactions(context: AccountContext, message: Message, ignoreDefault: Bool = false) -> Signal<(allowedReactions: AllowedReactions?, areStarsEnabled: Bool), NoError> { if message.id.peerId == context.account.peerId { return .single((.all, false)) } @@ -41,6 +41,10 @@ public func peerMessageAllowedReactions(context: AccountContext, message: Messag areStarsEnabled = value } + if let peer, !canSendReactionsToPeer(peer, ignoreDefault: ignoreDefault) { + return (nil, areStarsEnabled) + } + if let effectiveReactions = message.effectiveReactions(isTags: message.areReactionsTags(accountPeerId: context.account.peerId)), effectiveReactions.count >= maxReactionCount { return (.set(Set(effectiveReactions.map(\.value))), areStarsEnabled) } @@ -256,7 +260,7 @@ public func tagMessageReactions(context: AccountContext, subPeerId: EnginePeer.I } } -public func topMessageReactions(context: AccountContext, message: Message, subPeerId: EnginePeer.Id?) -> Signal<[ReactionItem], NoError> { +public func topMessageReactions(context: AccountContext, message: Message, subPeerId: EnginePeer.Id?, ignoreDefault: Bool = false) -> Signal<[ReactionItem], NoError> { if message.id.peerId == context.account.peerId { var loadTags = false if let effectiveReactionsAttribute = message.effectiveReactionsAttribute(isTags: message.areReactionsTags(accountPeerId: context.account.peerId)) { @@ -286,7 +290,7 @@ public func topMessageReactions(context: AccountContext, message: Message, subPe } } - let allowedReactionsWithFiles: Signal<(reactions: AllowedReactions, files: [Int64: TelegramMediaFile], areStarsEnabled: Bool)?, NoError> = peerMessageAllowedReactions(context: context, message: message) + let allowedReactionsWithFiles: Signal<(reactions: AllowedReactions, files: [Int64: TelegramMediaFile], areStarsEnabled: Bool)?, NoError> = peerMessageAllowedReactions(context: context, message: message, ignoreDefault: ignoreDefault) |> mapToSignal { allowedReactions, areStarsEnabled -> Signal<(reactions: AllowedReactions, files: [Int64: TelegramMediaFile], areStarsEnabled: Bool)?, NoError> in guard let allowedReactions = allowedReactions else { return .single(nil) diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index d200d532ca..168605725f 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -1,9 +1,7 @@ import Foundation import UIKit -import Postbox import SwiftSignalKit import AsyncDisplayKit -import Postbox import TelegramCore import Display import TelegramUIPreferences @@ -18,6 +16,7 @@ import TextFormat import WallpaperBackgroundNode import AnimationCache import MultiAnimationRenderer +import Postbox public struct ChatInterfaceHighlightedState: Equatable { public struct Quote: Equatable { @@ -164,12 +163,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 } } @@ -287,7 +288,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let sendGift: (EnginePeer.Id) -> Void public let openUniqueGift: (String) -> Void public let openMessageFeeException: () -> Void - public let requestMessageUpdate: (MessageId, Bool) -> Void + public let requestMessageUpdate: (MessageId, Bool, ControlledTransition?) -> Void public let cancelInteractiveKeyboardGestures: () -> Void public let dismissTextInput: () -> Void public let scrollToMessageId: (MessageIndex) -> Void @@ -302,6 +303,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let openStarsPurchase: (Int64?) -> Void public let openRankInfo: (EnginePeer, ChatRankInfoScreenRole, String) -> Void public let openSetPeerAvatar: () -> Void + public let displayPollRestrictedToast: (EngineMessage.Id) -> Void public var canPlayMedia: Bool = false public var hiddenMedia: [MessageId: [Media]] = [:] @@ -464,7 +466,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol sendGift: @escaping (EnginePeer.Id) -> Void, openUniqueGift: @escaping (String) -> Void, openMessageFeeException: @escaping () -> Void, - requestMessageUpdate: @escaping (MessageId, Bool) -> Void, + requestMessageUpdate: @escaping (MessageId, Bool, ControlledTransition?) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, dismissTextInput: @escaping () -> Void, scrollToMessageId: @escaping (MessageIndex) -> Void, @@ -479,6 +481,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol openStarsPurchase: @escaping (Int64?) -> Void, openRankInfo: @escaping (EnginePeer, ChatRankInfoScreenRole, String) -> Void, openSetPeerAvatar: @escaping () -> Void, + displayPollRestrictedToast: @escaping (EngineMessage.Id) -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings, @@ -609,6 +612,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.openStarsPurchase = openStarsPurchase self.openRankInfo = openRankInfo self.openSetPeerAvatar = openSetPeerAvatar + self.displayPollRestrictedToast = displayPollRestrictedToast self.automaticMediaDownloadSettings = automaticMediaDownloadSettings diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD index 6c8e0d451e..85a17851ad 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/BUILD @@ -17,13 +17,18 @@ swift_library( "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState", + "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/ComponentFlow:ComponentFlow", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TextFormat:TextFormat", "//submodules/AppBundle:AppBundle", + "//submodules/SearchBarNode:SearchBarNode", "//submodules/TelegramUI/Components/ChatInputNode:ChatInputNode", "//submodules/Components/PagerComponent:PagerComponent", "//submodules/PremiumUI:PremiumUI", @@ -38,10 +43,12 @@ swift_library( "//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction", "//submodules/FeaturedStickersScreen:FeaturedStickersScreen", "//submodules/TelegramUI/Components/EntityKeyboardGifContent:EntityKeyboardGifContent", + "//submodules/TelegramUI/Components/GlassControls", "//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView:LegacyMessageInputPanelInputView", - "//submodules/AttachmentTextInputPanelNode", "//submodules/TelegramUI/Components/BatchVideoRendering", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/SubcodecAnimationCacheImpl:SubcodecAnimationCacheImpl", + "//submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl:SubcodecMultiAnimationRendererImpl", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index a15805b502..5f4d8181d2 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -30,7 +30,6 @@ import FeaturedStickersScreen import Pasteboard import EntityKeyboardGifContent import LegacyMessageInputPanelInputView -import AttachmentTextInputPanelNode import GlassBackgroundComponent private let keyboardCornerRadius: CGFloat = 30.0 @@ -64,7 +63,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let getNavigationController: () -> NavigationController? let requestLayout: (ContainedViewLayoutTransition) -> Void public var forceTheme: PresentationTheme? - + public init( sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView?, CGRect?, CALayer?, [ItemCollectionId]) -> Bool, sendEmoji: @escaping (String, ChatTextInputTextCustomEmojiAttribute, Bool) -> Void, @@ -98,7 +97,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.getNavigationController = getNavigationController self.requestLayout = requestLayout } - + public init(chatControllerInteraction: ChatControllerInteraction, panelInteraction: ChatPanelInterfaceInteraction) { self.sendSticker = chatControllerInteraction.sendSticker self.sendEmoji = chatControllerInteraction.sendEmoji @@ -121,13 +120,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.requestLayout = panelInteraction.requestLayout } } - + public struct InputData: Equatable { public var emoji: EmojiPagerContentComponent? public var stickers: EmojiPagerContentComponent? public var gifs: EntityKeyboardGifContent? public var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] - + public init( emoji: EmojiPagerContentComponent?, stickers: EmojiPagerContentComponent?, @@ -140,14 +139,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.availableGifSearchEmojies = availableGifSearchEmojies } } - + public final class StateContext { let emojiState = EmojiPagerContentComponent.StateContext() - + public init() { } } - + public static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal { let hasPremium: Signal if premiumIfSavedMessages, let chatPeerId = chatPeerId, chatPeerId == context.account.peerId { @@ -164,7 +163,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } return hasPremium } - + public static func inputData( context: AccountContext, chatPeerId: PeerId?, @@ -181,7 +180,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { ) -> Signal { let animationCache = context.animationCache let animationRenderer = context.animationRenderer - + let emojiItems = EmojiPagerContentComponent.emojiInputData( context: context, animationCache: animationCache, @@ -198,12 +197,12 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { hideBackground: hideBackground, maskEdge: maskEdge ) - + let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers] - + let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings - + let stickerItems: Signal if hasStickers { stickerItems = EmojiPagerContentComponent.stickerInputData( @@ -226,20 +225,20 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } else { stickerItems = .single(nil) } - + let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()) |> map { appConfiguration -> [String] in let defaultReactions: [String] = ["👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒"] - + guard let data = appConfiguration.data, let emojis = data["gif_search_emojies"] as? [String] else { return defaultReactions } return emojis } |> distinctUntilChanged - + let animatedEmojiStickers: Signal<[String: [StickerPackItem]], NoError> - + if hasGifs { animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) |> map { animatedEmoji -> [String: [StickerPackItem]] in @@ -263,7 +262,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } else { animatedEmojiStickers = .single([:]) } - + let gifInputInteraction = GifPagerContentComponent.InputInteraction( performItemAction: { item, view, rect in if let sendGif { @@ -281,7 +280,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { hideBackground: hideBackground, hasSearch: hasSearch ) - + // We are going to subscribe to the actual data when the view is loaded let gifItems: Signal if hasGifs { @@ -304,7 +303,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } else { gifItems = .single(nil) } - + return combineLatest(queue: .mainQueue(), emojiItems, stickerItems, @@ -341,15 +340,15 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { default: break } - + guard let title = title else { continue } - + availableGifSearchEmojies.append(EntityKeyboardComponent.GifSearchEmoji(emoji: reaction, file: file._parse(), title: title)) } } - + return InputData( emoji: emoji, stickers: stickers, @@ -358,11 +357,11 @@ 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]] = [:] private var currentInputData: InputData @@ -371,32 +370,34 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { private let opaqueTopPanelBackground: Bool private let useOpaqueTheme: Bool private let displayBottomPanel: Bool - + private struct EmojiSearchResult { var groups: [EmojiPagerContentComponent.ItemGroup] var id: AnyHashable var version: Int var isPreset: Bool + var canLoadMore: Bool } - + private struct EmojiSearchState { var result: EmojiSearchResult? var isSearching: Bool - + init(result: EmojiSearchResult?, isSearching: Bool) { self.result = result self.isSearching = isSearching } } - + private let emojiSearchDisposable = MetaDisposable() + private var emojiSearchContext: EmojiSearchContext? private let emojiSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) { didSet { self.emojiSearchState.set(.single(self.emojiSearchStateValue)) } } - + private let stickerSearchDisposable = MetaDisposable() private let stickerSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) private var stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) { @@ -404,27 +405,27 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.stickerSearchState.set(.single(self.stickerSearchStateValue)) } } - + private let interaction: ChatEntityKeyboardInputNode.Interaction? private var inputNodeInteraction: ChatMediaInputNodeInteraction? - + private let trendingGifsPromise = Promise(nil) - + private var isMarkInputCollapsed: Bool = false - + private var isEmojiSearchActive: Bool = false { didSet { self.followsDefaultHeight = !self.isEmojiSearchActive } } - + public var clipContentToTopPanel: Bool = false - + public var externalTopPanelContainerImpl: PagerExternalTopPanelContainer? public override var externalTopPanelContainer: UIView? { return self.externalTopPanelContainerImpl } - + private let clippingView: UIView private var backgroundView: BlurredBackgroundView? private var backgroundTintView: UIImageView? @@ -432,14 +433,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { private var backgroundTintMaskView: UIView? private var backgroundTintMaskContentView: UIView? private var externalBackground: EmojiPagerContentComponent.ExternalBackground? - + public var switchToTextInput: (() -> Void)? - + private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)? - + private var scheduledContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? private var scheduledInnerTransition: ComponentTransition? - + private var gifMode: GifPagerContentComponent.Subject? { didSet { if let gifMode = self.gifMode, gifMode != oldValue { @@ -447,18 +448,18 @@ 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 } } return true } - + public var useExternalSearchContainer: Bool = false - + private var gifContext: GifContext? { didSet { if let gifContext = self.gifContext { @@ -468,12 +469,12 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } private let gifComponent = Promise() private var gifInputInteraction: GifPagerContentComponent.InputInteraction? - + fileprivate var emojiInputInteraction: EmojiPagerContentComponent.InputInteraction? private var stickerInputInteraction: EmojiPagerContentComponent.InputInteraction? - + private weak var currentUndoOverlayController: UndoOverlayController? - + private var choosingStickerDisposable: Disposable? private var scrollingStickersGridPromise = Promise(false) private var previewingStickersPromise = ValuePromise(false) @@ -484,7 +485,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } |> distinctUntilChanged } - + public init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal, defaultToEmojiTab: Bool, opaqueTopPanelBackground: Bool = false, useOpaqueTheme: Bool = false, interaction: ChatEntityKeyboardInputNode.Interaction?, chatPeerId: PeerId?, stateContext: StateContext?, forceHasPremium: Bool = false, displayBottomPanel: Bool = true) { self.context = context self.currentInputData = currentInputData @@ -493,31 +494,31 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.useOpaqueTheme = useOpaqueTheme self.stateContext = stateContext self.displayBottomPanel = displayBottomPanel - + 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() - + self.currentInputData = self.processInputData(inputData: self.currentInputData) - + self.topBackgroundExtension = 34.0 self.followsDefaultHeight = true - + if "".isEmpty { let backgroundView = BlurredBackgroundView(color: .black, enableBlur: true) self.backgroundView = backgroundView self.view.addSubview(backgroundView) - + let backgroundTintView = UIImageView() self.backgroundTintView = backgroundTintView self.view.addSubview(backgroundTintView) - + let backgroundTintMaskView = UIView() backgroundTintMaskView.backgroundColor = .white self.backgroundTintMaskView = backgroundTintMaskView @@ -525,32 +526,31 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { backgroundTintMaskView.layer.filters = [filter] } backgroundTintView.mask = backgroundTintMaskView - + let backgroundTintMaskContentView = UIView() backgroundTintMaskView.addSubview(backgroundTintMaskContentView) self.backgroundTintMaskContentView = backgroundTintMaskContentView - + let backgroundChromeView = UIImageView() self.backgroundChromeView = backgroundChromeView - + self.externalBackground = EmojiPagerContentComponent.ExternalBackground( effectContainerView: backgroundTintMaskContentView ) } - - self.clippingView.addSubview(self.entityKeyboardView) + self.view.addSubview(self.clippingView) - + if let backgroundChromeView = self.backgroundChromeView { self.view.addSubview(backgroundChromeView) } - + self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer() - + var stickerPeekBehavior: EmojiContentPeekBehaviorImpl? if let interaction { let context = self.context - + stickerPeekBehavior = EmojiContentPeekBehaviorImpl( context: self.context, interaction: EmojiContentPeekBehaviorImpl.Interaction( @@ -562,7 +562,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { switch attribute { case let .CustomEmoji(_, _, displayText, stickerPackReference): text = displayText - + var packId: ItemCollectionId? if case let .id(id, _) = stickerPackReference { packId = ItemCollectionId(namespace: Namespaces.ItemCollection.CloudEmojiPacks, id: id) @@ -573,7 +573,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { break } } - + if let emojiAttribute { interaction.sendEmoji(text, emojiAttribute, true) } @@ -583,16 +583,16 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return } let _ = strongSelf.context.engine.accountData.setEmojiStatus(file: file, expirationDate: nil).start() - + var animateInAsReplacement = false if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation() strongSelf.currentUndoOverlayController = nil animateInAsReplacement = true } - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiStatus_AppliedText, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false }) strongSelf.currentUndoOverlayController = controller interaction.presentController(controller, nil) @@ -601,33 +601,33 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let strongSelf = self else { return } - + var text = "." var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? loop: for attribute in file.attributes { switch attribute { case let .CustomEmoji(_, _, displayText, _): text = displayText - + emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file) break loop default: break } } - + if let _ = emojiAttribute { storeMessageTextInPasteboard(text, entities: [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .CustomEmoji(stickerPack: nil, fileId: file.fileId.id))]) - + var animateInAsReplacement = false if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { currentUndoOverlayController.dismissWithCommitActionAndReplacementAnimation() strongSelf.currentUndoOverlayController = nil animateInAsReplacement = true } - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Conversation_EmojiCopied, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false }) strongSelf.currentUndoOverlayController = controller interaction.presentController(controller, nil) @@ -646,7 +646,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } ) } - + var premiumToastCounter = 0 self.emojiInputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self, weak interaction] groupId, item, _, _, _, _ in @@ -660,7 +660,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let strongSelf = self, let interaction else { return } - + if groupId == AnyHashable("featuredTop"), let file = item.itemFile { let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) let _ = (combineLatest( @@ -678,21 +678,21 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let self else { return } - + let _ = interaction - + var installedCollectionIds = Set() for (id, _, _) in emojiPacksView.collectionInfos { installedCollectionIds.insert(id) } - + let stickerPacks = view.items.map({ $0.contents.get(FeaturedStickerPackItem.self)! }).filter({ !installedCollectionIds.contains($0.info.id) }) - + 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, @@ -714,7 +714,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { switch attribute { case let .CustomEmoji(_, _, displayText, _): text = displayText - + var packId: ItemCollectionId? if let id = groupId.base as? ItemCollectionId { packId = id @@ -725,7 +725,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { break } } - + if file.isPremiumEmoji && !hasPremium && groupId != AnyHashable("peerSpecific") && !forceHasPremium { var animateInAsReplacement = false if let currentUndoOverlayController = strongSelf.currentUndoOverlayController { @@ -733,9 +733,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { strongSelf.currentUndoOverlayController = nil animateInAsReplacement = true } - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + premiumToastCounter += 1 var suggestSavedMessages = premiumToastCounter % 2 == 0 if chatPeerId == nil { @@ -750,19 +750,19 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { text = presentationData.strings.EmojiInput_PremiumEmojiToast_Text actionTitle = presentationData.strings.EmojiInput_PremiumEmojiToast_Action } - + let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: text, undoText: actionTitle, customAction: { [weak interaction] in guard let interaction else { return } - + if suggestSavedMessages { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> deliverOnMainQueue).start(next: { peer in guard let peer = peer, let navigationController = interaction.getNavigationController() else { return } - + context.sharedContext.navigateToChatController(NavigateToChatControllerParams( navigationController: navigationController, chatController: nil, @@ -792,14 +792,16 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { interaction.presentController(controller, nil) return } - + if let emojiAttribute = emojiAttribute { AudioServicesPlaySystemSound(0x450) interaction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) + (strongSelf.entityKeyboardView.view as? EntityKeyboardComponent.View)?.revealHiddenPanels() } } else if case let .staticEmoji(staticEmoji) = item.content { AudioServicesPlaySystemSound(0x450) interaction.insertText(NSAttributedString(string: staticEmoji, attributes: [:])) + (strongSelf.entityKeyboardView.view as? EntityKeyboardComponent.View)?.revealHiddenPanels() } }) }, @@ -818,7 +820,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let interaction, let collectionId = groupId.base as? ItemCollectionId else { return } - + if isPremiumLocked { var replaceImpl: ((ViewController) -> Void)? let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: { @@ -829,10 +831,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { controller?.replace(with: c) } interaction.getNavigationController()?.pushViewController(controller) - + return } - + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) let _ = (context.account.postbox.combinedView(keys: [viewKey]) |> take(1) @@ -846,7 +848,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { strongSelf.scheduledContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupInstalled(id: collectionId, scrollToGroup: scrollToGroup)) } let _ = context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info._parse(), items: featuredEmojiPack.topItems).start() - + break } } @@ -924,20 +926,23 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let self = self else { return } - + switch query { case .none: + self.emojiSearchContext = nil self.emojiSearchDisposable.set(nil) - self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) case let .text(rawQuery, languageCode): let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) - + if query.isEmpty { + self.emojiSearchContext = nil self.emojiSearchDisposable.set(nil) - self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) } else { let context = self.context - + self.emojiSearchContext = nil + var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) if !languageCode.lowercased().hasPrefix("en") { signal = signal @@ -951,7 +956,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { ) } } - + let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> map { peer -> Bool in guard case let .user(user) = peer else { @@ -960,43 +965,46 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return user.isPremium } |> distinctUntilChanged - + let resultSignal = combineLatest( signal, hasPremium ) - |> mapToSignal { keywords, hasPremium -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + |> mapToSignal { keywords, hasPremium -> Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?), NoError> in var allEmoticons: [String: String] = [:] for keyword in keywords { for emoticon in keyword.emoticons { allEmoticons[emoticon] = keyword.keyword } } - let remoteSignal: Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError> - let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> + + let emojiSearchContext: EmojiSearchContext? + let remoteSignal: Signal + let remotePacksSignal: Signal if hasPremium { - remoteSignal = context.engine.stickers.searchEmoji(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode) + let currentEmojiSearchContext = context.engine.stickers.emojiSearchContext(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode) + emojiSearchContext = currentEmojiSearchContext + remoteSignal = currentEmojiSearchContext.state remotePacksSignal = context.engine.stickers.searchEmojiSets(query: query) |> mapToSignal { localResult in - return .single((localResult, false)) + return .single(localResult) |> then( context.engine.stickers.searchEmojiSetsRemotely(query: query) |> map { remoteResult in - return (localResult.merge(with: remoteResult), true) + return localResult.merge(with: remoteResult) } ) } } else { - remoteSignal = .single(([], true)) - remotePacksSignal = .single((FoundStickerSets(), true)) + emojiSearchContext = nil + remoteSignal = .single(EmojiSearchContext.State(items: [], canLoadMore: false, isLoadingMore: false)) + remotePacksSignal = .single(FoundStickerSets()) } + return combineLatest(remoteSignal, remotePacksSignal) - |> mapToSignal { foundEmoji, foundPacks -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in - if foundEmoji.items.isEmpty && !foundEmoji.isFinalResult { - return .complete() - } + |> map { foundEmoji, foundPacks -> (groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?) in var items: [EmojiPagerContentComponent.Item] = [] - + let appendUnicodeEmoji = { for (_, list) in EmojiPagerContentComponent.staticEmojiMapping { for emojiString in list { @@ -1014,11 +1022,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } } - + if !hasPremium { appendUnicodeEmoji() } - + var existingIds = Set() for itemFile in foundEmoji.items { if existingIds.contains(itemFile.fileId) { @@ -1039,11 +1047,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { ) items.append(item) } - + if hasPremium { appendUnicodeEmoji() } - + var resultGroups: [EmojiPagerContentComponent.ItemGroup] = [] resultGroups.append(EmojiPagerContentComponent.ItemGroup( supergroupId: "search", @@ -1063,25 +1071,25 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { fillWithLoadingPlaceholders: false, items: items )) - - for (collectionId, info, _, _) in foundPacks.sets.infos { + + for (collectionId, info, _, _) in foundPacks.infos { if let info = info as? StickerPackCollectionInfo { var topItems: [StickerPackItem] = [] - for e in foundPacks.sets.entries { + for e in foundPacks.entries { if let item = e.item as? StickerPackItem { if e.index.collectionId == collectionId { topItems.append(item) } } } - + var groupItems: [EmojiPagerContentComponent.Item] = [] for item in topItems { var tintMode: EmojiPagerContentComponent.Item.TintMode = .none if item.file.isCustomTemplateEmoji { tintMode = .primary } - + let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, @@ -1091,10 +1099,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { icon: .none, tintMode: tintMode ) - + groupItems.append(resultItem) } - + resultGroups.append(EmojiPagerContentComponent.ItemGroup( supergroupId: AnyHashable(info.id), groupId: AnyHashable(info.id), @@ -1115,29 +1123,31 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { )) } } - - return .single(resultGroups) + + return (resultGroups, foundEmoji.canLoadMore, foundEmoji.items.isEmpty && foundEmoji.isLoadingMore, emojiSearchContext) } } - + var version = 0 self.emojiSearchStateValue.isSearching = true self.emojiSearchDisposable.set((resultSignal - |> delay(0.15, queue: .mainQueue()) + |> delay(0.25, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } - - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + + self.emojiSearchContext = result.searchContext + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.groups, id: AnyHashable(query), version: version, isPreset: false, canLoadMore: result.canLoadMore), isSearching: result.isSearching) version += 1 })) } case let .category(value): + self.emojiSearchContext = nil let resultSignal = self.context.engine.stickers.searchEmoji(category: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] - + var existingIds = Set() for itemFile in files { if existingIds.contains(itemFile.fileId) { @@ -1155,7 +1165,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { ) items.append(item) } - + return .single(([EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", @@ -1175,14 +1185,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { items: items )], isFinalResult)) } - + var version = 0 self.emojiSearchDisposable.set((resultSignal |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } - + guard let group = result.items.first else { return } @@ -1206,10 +1216,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) version += 1 })) } @@ -1217,6 +1227,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { updateScrollingToItemGroup: { }, onScroll: {}, + loadMore: { [weak self] in + self?.emojiSearchContext?.loadMore() + }, chatPeerId: chatPeerId, peekBehavior: stickerPeekBehavior, customLayout: nil, @@ -1228,7 +1241,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { stateContext: self.stateContext?.emojiState, addImage: nil ) - + self.stickerInputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak interaction] groupId, item, view, rect, layer, _ in let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in @@ -1241,7 +1254,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } return } - + if groupId == AnyHashable("featuredTop") { let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) let _ = (context.account.postbox.combinedView(keys: [viewKey]) @@ -1266,7 +1279,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return interaction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, []) } )) - + break } } @@ -1275,14 +1288,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { if file.isPremiumSticker && !hasPremium { let controller = PremiumIntroScreen(context: context, source: .stickers) interaction.getNavigationController()?.pushViewController(controller) - + return } var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] if let id = groupId.base as? ItemCollectionId, context.sharedContext.currentStickerSettings.with({ $0 }).dynamicPackOrder { bubbleUpEmojiOrStickersets.append(id) } - + let reference: FileMediaReference if groupId == AnyHashable("saved") { reference = .savedSticker(media: file) @@ -1326,7 +1339,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() } }, @@ -1334,14 +1347,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let interaction, let collectionId = groupId.base as? ItemCollectionId else { return } - + if isPremiumLocked { let controller = PremiumIntroScreen(context: context, source: .stickers) interaction.getNavigationController()?.pushViewController(controller) - + return } - + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) let _ = (context.account.postbox.combinedView(keys: [viewKey]) |> take(1) @@ -1358,7 +1371,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { if installed { return .complete() } else { - return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) + return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) |> map { _ in return Void() } } case .fetching: break @@ -1369,7 +1382,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } |> deliverOnMainQueue).start(completed: { }) - + break } } @@ -1471,7 +1484,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let strongSelf = self else { return } - + switch query { case .none: strongSelf.stickerSearchDisposable.set(nil) @@ -1483,7 +1496,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let resultSignal = strongSelf.context.engine.stickers.searchStickers(category: value, scope: [.installed, .remote]) |> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] - + var existingIds = Set() for item in files.items { let itemFile = item.file @@ -1502,7 +1515,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { ) items.append(item) } - + return .single(([EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", @@ -1522,7 +1535,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { items: items )], files.isFinalResult)) } - + var version = 0 strongSelf.stickerSearchDisposable.set((resultSignal |> deliverOnMainQueue).start(next: { result in @@ -1553,10 +1566,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) return } - strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) version += 1 })) } @@ -1575,7 +1588,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { stateContext: nil, addImage: nil ) - + self.inputDataDisposable = (combineLatest(queue: .mainQueue(), updatedInputData, .single(self.currentInputData.gifs) |> then(self.gifComponent.get() |> map(Optional.init)), @@ -1588,12 +1601,12 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } var inputData = inputData inputData.gifs = gifs - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } - + if let emojiSearchResult = emojiSearchState.result { var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? - if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { + if !emojiSearchState.isSearching && !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( text: presentationData.strings.EmojiSearch_SearchEmojiEmptyResult, iconFile: nil @@ -1601,14 +1614,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } if let emoji = inputData.emoji { let defaultSearchState: EmojiPagerContentComponent.SearchState = emojiSearchResult.isPreset ? .active : .empty(hasResults: true) - inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState) + inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState, canLoadMore: emojiSearchResult.canLoadMore) } } else if emojiSearchState.isSearching { if let emoji = inputData.emoji { inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emoji.contentItemGroups, itemContentUniqueId: emoji.itemContentUniqueId, emptySearchResults: emoji.emptySearchResults, searchState: .searching) } } - + if let stickerSearchResult = stickerSearchState.result { var stickerSearchResults: EmojiPagerContentComponent.EmptySearchResults? if !stickerSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { @@ -1619,25 +1632,25 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } if let stickers = inputData.stickers { let defaultSearchState: EmojiPagerContentComponent.SearchState = stickerSearchResult.isPreset ? .active : .empty(hasResults: true) - inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: stickerSearchState.isSearching ? .searching : defaultSearchState) + inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: stickerSearchState.isSearching ? .searching : defaultSearchState, canLoadMore: stickerSearchResult.canLoadMore) } } else if stickerSearchState.isSearching { if let stickers = inputData.stickers { inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickers.contentItemGroups, itemContentUniqueId: stickers.itemContentUniqueId, emptySearchResults: stickers.emptySearchResults, searchState: .searching) } } - + 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 { useAnimation = strongSelf.currentInputData.stickers != inputData.stickers } } - + if useAnimation { let contentAnimation: EmojiPagerContentComponent.ContentAnimation if let scheduledContentAnimationHint = strongSelf.scheduledContentAnimationHint { @@ -1651,7 +1664,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { strongSelf.currentInputData = strongSelf.processInputData(inputData: inputData) strongSelf.performLayout(transition: transition) }) - + self.inputNodeInteraction = ChatMediaInputNodeInteraction( navigateToCollectionId: { _ in }, @@ -1674,7 +1687,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { clearRecentlyUsedStickers: { } ) - + self.trendingGifsPromise.set(.single(nil)) self.trendingGifsPromise.set(paneGifSearchForQuery(context: context, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil) |> map { items -> ChatMediaInputGifPaneTrendingState? in @@ -1684,13 +1697,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return nil } }) - + self.gifInputInteraction = GifPagerContentComponent.InputInteraction( performItemAction: { [weak interaction] item, view, rect in guard let interaction else { return } - + if let (collection, result) = item.contextResult { let _ = interaction.sendBotContextResultAsGif(collection, result, view, rect, false, false) } else { @@ -1710,7 +1723,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() } }, @@ -1727,25 +1740,25 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { hideBackground: currentInputData.gifs?.component.hideBackground ?? false, hasSearch: currentInputData.gifs?.component.inputInteraction.hasSearch ?? false ) - + self.switchToTextInput = { [weak self] in if let self { self.interaction?.switchToTextInput() } } - + if self.currentInputData.gifs != nil { let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) |> map { savedGifs -> Bool in return !savedGifs.isEmpty } - + self.hasRecentGifsDisposable = (hasRecentGifs |> deliverOnMainQueue).start(next: { [weak self] hasRecentGifs in guard let strongSelf = self else { return } - + if let gifMode = strongSelf.gifMode { if !hasRecentGifs, case .recent = gifMode { strongSelf.gifMode = .trending @@ -1755,7 +1768,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } }) } - + self.choosingStickerDisposable = (self.choosingSticker |> deliverOnMainQueue).start(next: { [weak self] value in if let self { @@ -1763,7 +1776,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } }) } - + deinit { self.inputDataDisposable?.dispose() self.hasRecentGifsDisposable?.dispose() @@ -1771,17 +1784,17 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.stickerSearchDisposable.dispose() self.choosingStickerDisposable?.dispose() } - + private func reloadGifContext() { if let gifInputInteraction = self.gifInputInteraction, let gifMode = self.gifMode { self.gifContext = GifContext(context: self.context, subject: gifMode, gifInputInteraction: gifInputInteraction, trendingGifs: self.trendingGifsPromise.get()) } } - + public func markInputCollapsed() { self.isMarkInputCollapsed = true } - + private func performLayout(transition: ComponentTransition) { guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, layoutMetrics, deviceMetrics, isVisible, isExpanded) = self.currentState else { return @@ -1789,19 +1802,24 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.scheduledInnerTransition = transition let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, layoutMetrics: layoutMetrics, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) } - + public func simulateUpdateLayout(isVisible: Bool) { guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, layoutMetrics, deviceMetrics, _, isExpanded) = self.currentState else { return } let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, layoutMetrics: layoutMetrics, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded) } - + 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) { for subview in self.view.subviews.reversed() { if let result = subview.hitTest(self.view.convert(point, to: subview), with: event) { @@ -1809,13 +1827,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } } - + return nil } - + public override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, layoutMetrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool) -> (CGFloat, CGFloat) { self.currentState = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, layoutMetrics, deviceMetrics, isVisible, isExpanded) - + let innerTransition: ComponentTransition if let scheduledInnerTransition = self.scheduledInnerTransition { self.scheduledInnerTransition = nil @@ -1823,40 +1841,40 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } else { innerTransition = ComponentTransition(transition) } - + let wasMarkedInputCollapsed = self.isMarkInputCollapsed self.isMarkInputCollapsed = false - + var expandedHeight = standardInputHeight if self.isEmojiSearchActive && !isExpanded { expandedHeight += 118.0 } - + var hiddenInputHeight: CGFloat = 0.0 if self.hideInput && !self.adjustLayoutForHiddenInput { hiddenInputHeight = inputPanelHeight } - + let context = self.context let interaction = self.interaction let inputNodeInteraction = self.inputNodeInteraction! let trendingGifsPromise = self.trendingGifsPromise - + var mappedTransition = innerTransition - + if wasMarkedInputCollapsed || !isExpanded { mappedTransition = mappedTransition.withUserData(EntityKeyboardComponent.MarkInputCollapsed()) } - + var emojiContent: EmojiPagerContentComponent? = self.currentInputData.emoji var stickerContent: EmojiPagerContentComponent? = self.currentInputData.stickers var gifContent: EntityKeyboardGifContent? = self.currentInputData.gifs - + var stickersEnabled = true var emojiEnabled = true if let peer = interfaceState.renderedPeer?.peer as? TelegramChannel { if let boostsToUnrestrict = interfaceState.boostsToUnrestrict, boostsToUnrestrict > 0 { - + } else { if peer.hasBannedPermission(.banSendStickers) != nil { stickersEnabled = false @@ -1873,7 +1891,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { emojiEnabled = false } } - + if !stickersEnabled || interfaceState.interfaceState.editMessage != nil { stickerContent = nil gifContent = nil @@ -1892,16 +1910,16 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { gifContent = nil } } - + stickerContent?.inputInteractionHolder.inputInteraction = self.stickerInputInteraction self.currentInputData.emoji?.inputInteractionHolder.inputInteraction = self.emojiInputInteraction - + if let stickerInputInteraction = self.stickerInputInteraction { self.scrollingStickersGridPromise.set(stickerInputInteraction.scrollingStickersGridPromise.get()) } - + let startTime = CFAbsoluteTimeGetCurrent() - + var keyboardBottomInset = bottomInset if case .regular = layoutMetrics.widthClass, inputHeight > 0.0 && inputHeight < 100.0 { keyboardBottomInset = inputHeight + 15.0 @@ -1979,7 +1997,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { case .gifs: mappedMode = .gif } - + let searchContainerNode = PaneSearchContainerNode( context: context, theme: interfaceState.theme, @@ -1999,7 +2017,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } self.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceNode.view, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) } - + return searchContainerNode }, contentIdUpdated: { _ in }, @@ -2014,21 +2032,26 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { environment: {}, containerSize: CGSize(width: width, height: expandedHeight) ) - + var clippingFrame = CGRect(origin: CGPoint(), size: entityKeyboardSize) clippingFrame.size.height += 32.0 - + var entityKeyboardSizeFrame = CGRect(origin: CGPoint(), size: entityKeyboardSize) if self.hideInput { clippingFrame.size.height += self.topBackgroundExtension clippingFrame.origin.y -= self.topBackgroundExtension 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) - + if let backgroundView = self.backgroundView, let backgroundTintView = self.backgroundTintView, let backgroundTintMaskView = self.backgroundTintMaskView, let backgroundTintMaskContentView = self.backgroundTintMaskContentView, let backgroundChromeView = self.backgroundChromeView { var backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: entityKeyboardSize) if self.hideInput { @@ -2036,7 +2059,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { backgroundFrame.origin.y -= self.topBackgroundExtension } backgroundFrame.size.height += 32.0 - + if backgroundChromeView.image == nil { backgroundChromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: keyboardCornerRadius * 2.0, height: keyboardCornerRadius * 2.0), isDark: interfaceState.theme.overallDarkAppearance, fillColor: .clear) } @@ -2044,33 +2067,33 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { backgroundTintView.image = generateStretchableFilledCircleImage(diameter: keyboardCornerRadius * 2.0, color: .white)?.withRenderingMode(.alwaysTemplate) } backgroundTintView.tintColor = interfaceState.theme.chat.inputMediaPanel.backgroundColor - + transition.updateFrame(view: backgroundView, frame: backgroundFrame) backgroundView.updateColor(color: .clear, forceKeepBlur: true, transition: .immediate) backgroundView.update(size: backgroundFrame.size, cornerRadius: keyboardCornerRadius, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition) - + transition.updateFrame(view: backgroundChromeView, frame: backgroundFrame.insetBy(dx: -1.0, dy: 0.0)) - + var backgroundTintMaskContentFrame = CGRect(origin: CGPoint(), size: backgroundFrame.size) if self.hideInput { backgroundTintMaskContentFrame.origin.y += self.topBackgroundExtension } transition.updateFrame(view: backgroundTintView, frame: backgroundFrame) - + transition.updateFrame(view: backgroundTintMaskView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) transition.updateFrame(view: backgroundTintMaskContentView, frame: backgroundTintMaskContentFrame) } - + let layoutTime = CFAbsoluteTimeGetCurrent() - startTime if layoutTime > 0.1 { #if DEBUG print("EntityKeyboard layout in \(layoutTime * 1000.0) ms") #endif } - + return (expandedHeight, 0.0) } - + private func processStableItemGroupList(category: EntityKeyboardComponent.ReorderCategory, itemGroups: [EmojiPagerContentComponent.ItemGroup]) -> [EmojiPagerContentComponent.ItemGroup] { let nextIds: [ItemCollectionId] = itemGroups.compactMap { group -> ItemCollectionId? in if group.isEmbedded { @@ -2085,11 +2108,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return nil } } - + let stableOrder = self.stableReorderableGroupOrder[category] ?? nextIds - + var updatedGroups: [EmojiPagerContentComponent.ItemGroup] = [] - + var staticIsFirst = false let topStaticGroups: [String] = [ "static", @@ -2111,7 +2134,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { break } } - + for group in itemGroups { if !(group.groupId.base is ItemCollectionId) { if group.groupId != AnyHashable("static") || staticIsFirst { @@ -2139,7 +2162,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { updatedGroups.append(group) } } - + let updatedIds = updatedGroups.compactMap { group -> ItemCollectionId? in if group.isEmbedded { return nil @@ -2155,10 +2178,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } self.stableReorderableGroupOrder[category] = updatedIds - + return updatedGroups } - + private func processInputData(inputData: InputData) -> InputData { return InputData( emoji: inputData.emoji.flatMap { emoji in @@ -2171,7 +2194,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { availableGifSearchEmojies: inputData.availableGifSearchEmojies ) } - + private func reorderItems(category: EntityKeyboardComponent.ReorderCategory, items: [EntityKeyboardTopPanelComponent.Item]) { var currentIds: [ItemCollectionId] = [] for item in items { @@ -2189,9 +2212,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { case .masks: namespace = Namespaces.ItemCollection.CloudMaskPacks } - + self.stableReorderableGroupOrder.removeValue(forKey: category) - + let _ = (self.context.engine.stickers.reorderStickerPacks(namespace: namespace, itemIds: currentIds) |> deliverOnMainQueue).start(completed: { [weak self] in guard let strongSelf = self else { @@ -2199,19 +2222,19 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } strongSelf.performLayout(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) }) - + if self.context.sharedContext.currentStickerSettings.with({ $0 }).dynamicPackOrder { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.interaction?.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_reorder", scale: 0.05, colors: [:], title: presentationData.strings.StickerPacksSettings_DynamicOrderOff, text: presentationData.strings.StickerPacksSettings_DynamicOrderOffInfo, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in return false }), nil) - + let _ = updateStickerSettingsInteractively(accountManager: self.context.sharedContext.accountManager, { return $0.withUpdatedDynamicPackOrder(false) }).start() } } - + private func openGifContextMenu(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { let canSaveGif: Bool if file.media.fileId.namespace == Namespaces.Media.CloudFile { @@ -2219,7 +2242,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } else { canSaveGif = false } - + let _ = (self.context.engine.stickers.isGifSaved(id: file.media.fileId) |> deliverOnMainQueue).start(next: { [weak self] isGifSaved in guard let strongSelf = self else { @@ -2230,11 +2253,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { isGifSaved = false } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - + let gallery = strongSelf.context.sharedContext.makeGalleryController(context: strongSelf.context, source: .standaloneMessage(message, nil), streamSingleVideo: true, isPreview: true) - + var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Send, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor) @@ -2248,10 +2271,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } }))) - + if let currentState = strongSelf.currentState { let interfaceState = currentState.interfaceState - + var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true @@ -2272,7 +2295,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } }))) } - + if isSaved && interfaceState.sendPaidMessageStars == nil { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) @@ -2286,7 +2309,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { } } } - + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_Gif_AddCaption, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddCaption"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in @@ -2295,7 +2318,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let _ = self.interaction?.editGif(file, true) } }))) - + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_Gif_Edit, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Draw"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in @@ -2304,13 +2327,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let _ = self.interaction?.editGif(file, false) } }))) - + if isSaved || isGifSaved { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) - + if let self { let _ = removeSavedGif(postbox: self.context.account.postbox, mediaId: file.media.fileId).start() } @@ -2320,11 +2343,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) - + guard let strongSelf = self else { return } - + let context = strongSelf.context let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (toggleGifSaved(account: context.account, fileReference: file, saved: true) @@ -2347,7 +2370,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { guard let strongSelf = self else { return false } - + if case .info = action { let controller = PremiumIntroScreen(context: context, source: .savedGifs) strongSelf.interaction?.getNavigationController()?.pushViewController(controller) @@ -2359,14 +2382,14 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { }) }))) } - + let contextController = makeContextController(presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) strongSelf.interaction?.presentGlobalOverlayController(contextController, nil) }) } - + 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) } } @@ -2376,17 +2399,17 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent let controller: ViewController weak var sourceView: UIView? let sourceRect: CGRect - + let navigationController: NavigationController? = nil - + let passthroughTouches: Bool = false - + init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect) { self.controller = controller self.sourceView = sourceView self.sourceRect = sourceRect } - + func transitionInfo() -> ContextControllerTakeControllerInfo? { let sourceView = self.sourceView let sourceRect = self.sourceRect @@ -2398,7 +2421,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent } }) } - + func animatedIn() { if let controller = self.controller as? GalleryControllerProtocol { controller.viewDidAppear(false) @@ -2406,19 +2429,19 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent } } -public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputView, LegacyMessageInputPanelInputView, UIInputViewAudioFeedback { +public final class EntityInputView: UIInputView, LegacyMessageInputPanelInputView, UIInputViewAudioFeedback { private let context: AccountContext - + public var insertText: ((NSAttributedString) -> Void)? public var deleteBackwards: (() -> Void)? public var switchToKeyboard: (() -> Void)? public var presentController: ((ViewController) -> Void)? - + private var presentationData: PresentationData private var inputNode: ChatEntityKeyboardInputNode? private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer - + public init( context: AccountContext, isDark: Bool, @@ -2427,20 +2450,20 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi forceHasPremium: Bool = false ) { self.context = context - + self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer - + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } if isDark { self.presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme) } - + super.init(frame: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), inputViewStyle: .default) - + self.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.clipsToBounds = true - + let inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] groupId, item, _, _, _, _ in let hasPremium: Signal @@ -2453,7 +2476,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi guard let strongSelf = self else { return } - + if groupId == AnyHashable("featuredTop") { } else { if let file = item.itemFile?._parse() { @@ -2473,14 +2496,14 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi break } } - + if file.isPremiumEmoji && !hasPremium { let presentationData = context.sharedContext.currentPresentationData.with { $0 } strongSelf.presentController?(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: { guard let strongSelf = self else { return } - + var replaceImpl: ((ViewController) -> Void)? let controller = PremiumDemoScreen(context: strongSelf.context, subject: .animatedEmoji, action: { let controller = PremiumIntroScreen(context: strongSelf.context, source: .animatedEmoji) @@ -2494,7 +2517,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi controller.replace(with: c) } else { controller.dismiss() - + if let self { self.presentController?(c) } @@ -2504,7 +2527,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi }), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) return } - + if let emojiAttribute = emojiAttribute { AudioServicesPlaySystemSound(0x450) strongSelf.insertText?(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) @@ -2579,7 +2602,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi stateContext: nil, addImage: nil ) - + let semaphore = DispatchSemaphore(value: 0) var emojiComponent: EmojiPagerContentComponent? let _ = EmojiPagerContentComponent.emojiInputData( @@ -2588,7 +2611,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi animationRenderer: self.animationRenderer, isStandalone: true, subject: .generic, - hasTrending: false, + hasTrending: false, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, @@ -2599,7 +2622,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi semaphore.signal() }) semaphore.wait() - + if let emojiComponent = emojiComponent { let inputNode = ChatEntityKeyboardInputNode( context: self.context, @@ -2616,7 +2639,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi isStandalone: true, subject: .generic, hasTrending: false, - topReactionItems: [], + topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil, @@ -2649,31 +2672,31 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi self.addSubnode(inputNode) } } - + required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + public override func layoutSubviews() { super.layoutSubviews() - + guard let inputNode = self.inputNode else { return } - + for view in self.subviews { if view !== inputNode.view { view.isHidden = true } } - + let bottomInset: CGFloat if #available(iOS 11.0, *) { bottomInset = max(0.0, UIScreen.main.bounds.height - (self.window?.safeAreaLayoutGuide.layoutFrame.maxY ?? 10000.0)) } else { bottomInset = 0.0 } - + let presentationInterfaceState = ChatPresentationInterfaceState( chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, @@ -2688,7 +2711,6 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi mode: .standard(.default), chatLocation: .peer(id: self.context.account.peerId), subject: nil, - peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, @@ -2730,7 +2752,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { public let presentGlobalOverlayController: (ViewController, Any?) -> Void public let navigationController: () -> NavigationController? public let updateIsPreviewing: (Bool) -> Void - + public init(sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView?, CGRect?, CALayer?, [ItemCollectionId]) -> Bool, sendEmoji: @escaping (TelegramMediaFile) -> Void, setStatus: @escaping (TelegramMediaFile) -> Void, copyEmoji: @escaping (TelegramMediaFile) -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, updateIsPreviewing: @escaping (Bool) -> Void) { self.sendSticker = sendSticker self.sendEmoji = sendEmoji @@ -2742,26 +2764,26 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { self.updateIsPreviewing = updateIsPreviewing } } - + private final class ViewRecord { weak var view: UIView? let peekRecognizer: PeekControllerGestureRecognizer - + init(view: UIView, peekRecognizer: PeekControllerGestureRecognizer) { self.view = view self.peekRecognizer = peekRecognizer } } - + private let context: AccountContext private let forceTheme: PresentationTheme? private let interaction: Interaction? private let chatPeerId: EnginePeer.Id? private let present: (ViewController, Any?) -> Void - + private var viewRecords: [ViewRecord] = [] private weak var peekController: PeekController? - + public init(context: AccountContext, forceTheme: PresentationTheme? = nil, interaction: Interaction?, chatPeerId: EnginePeer.Id?, present: @escaping (ViewController, Any?) -> Void) { self.context = context self.forceTheme = forceTheme @@ -2769,12 +2791,12 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { self.chatPeerId = chatPeerId self.present = present } - + public func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, CALayer, TelegramMediaFile)?) { self.viewRecords = self.viewRecords.filter({ $0.view != nil }) - + let viewRecord = self.viewRecords.first(where: { $0.view === view }) - + if let viewRecord = viewRecord { viewRecord.peekRecognizer.isEnabled = isEnabled } else { @@ -2785,19 +2807,19 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { guard let (groupId, itemLayer, file) = itemAtPoint(point) else { return nil } - + let context = strongSelf.context - + var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] if let id = groupId.base as? ItemCollectionId { if file.isCustomEmoji || context.sharedContext.currentStickerSettings.with({ $0 }).dynamicPackOrder { bubbleUpEmojiOrStickersets.append(id) } } - + let accountPeerId = context.account.peerId let chatPeerId = strongSelf.chatPeerId - + if file.isCustomEmoji { return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId)) |> map { peer -> Bool in var hasPremium = false @@ -2811,9 +2833,9 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { guard let strongSelf = self, let itemLayer = itemLayer else { return nil } - + var menuItems: [ContextMenuItem] = [] - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } var isLocked = false if !hasPremium { @@ -2822,7 +2844,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { isLocked = false } } - + if let interaction = strongSelf.interaction { let sendEmoji: (TelegramMediaFile) -> Void = { file in interaction.sendEmoji(file) @@ -2833,13 +2855,13 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { let copyEmoji: (TelegramMediaFile) -> Void = { file in interaction.copyEmoji(file) } - + if let _ = strongSelf.chatPeerId { menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_SendEmoji, icon: { theme in if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) { return generateImage(image.size, rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - + if let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size)) } @@ -2851,16 +2873,16 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { sendEmoji(file) f(.default) }))) - + menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_SetAsStatus, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Smile"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) - + guard let strongSelf = self else { return } - + if hasPremium { setStatus(file) } else { @@ -2875,7 +2897,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { strongSelf.interaction?.navigationController()?.pushViewController(controller) } }))) - + menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.EmojiPreview_CopyEmoji, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in @@ -2884,14 +2906,14 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { }))) } } - + if menuItems.isEmpty { return nil } guard let view = view else { return nil } - + return (view, itemLayer.convert(itemLayer.bounds, to: view.layer), StickerPreviewPeekContent(context: context, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked, menu: menuItems, openPremiumIntro: { guard let strongSelf = self, let interaction = strongSelf.interaction else { return @@ -2907,7 +2929,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { } else { sendPaidMessageStars = .single(nil) } - + return combineLatest( context.engine.stickers.isStickerSaved(id: file.fileId), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId)) |> map { peer -> Bool in @@ -2925,15 +2947,15 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { return nil } var menuItems: [ContextMenuItem] = [] - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } let isLocked = file.isPremiumSticker && !hasPremium - + if let interaction = strongSelf.interaction { let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView?, CGRect?, CALayer?) -> Void = { fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer in let _ = interaction.sendSticker(fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets) } - + if let chatPeerId = strongSelf.chatPeerId, !isLocked { if chatPeerId != strongSelf.context.account.peerId && chatPeerId.namespace != Namespaces.Peer.SecretChat { menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in @@ -2949,7 +2971,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { f(.default) }))) } - + if sendPaidMessageStars == nil { menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) @@ -2965,11 +2987,11 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { }))) } } - + menuItems.append( .action(ContextMenuActionItem(text: isStarred ? presentationData.strings.Stickers_RemoveFromFavorites : presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) - + let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred) |> deliverOnMainQueue).start(next: { result in @@ -2996,18 +3018,18 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { }) })) ) - + if !file.isPremiumSticker { menuItems.append(.action(ContextMenuActionItem(text: presentationData.strings.Stickers_EditSticker, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Draw"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) - + var emoji: [String] = [] for attribute in file.attributes { if case let .Sticker(displayText, _, _) = attribute { emoji = [displayText] } } - + let controller = context.sharedContext.makeStickerEditorScreen( context: context, source: (file, emoji), @@ -3022,7 +3044,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { interaction.navigationController()?.pushViewController(controller) }))) } - + loop: for attribute in file.attributes { switch attribute { case let .CustomEmoji(_, _, _, packReference), let .Sticker(_, packReference, _): @@ -3032,16 +3054,16 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) - + guard let strongSelf = self else { return } - + let controller = strongSelf.context.sharedContext.makeStickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], loadedStickerPacks: [], actionTitle: nil, isEditing: false, expandIfNeeded: false, parentNavigationController: interaction.navigationController(), sendSticker: { file, sourceView, sourceRect in sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil) return true }, actionPerformed: nil) - + interaction.navigationController()?.view.window?.endEditing(true) interaction.presentController(controller, nil) })) @@ -3052,31 +3074,31 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { break } } - + if groupId == AnyHashable("recent") { menuItems.append( .action(ContextMenuActionItem(text: presentationData.strings.Stickers_RemoveFromRecent, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { _, f in f(.default) - + guard let strongSelf = self else { return } - + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Conversation_StickerRemovedFromRecent, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) - + strongSelf.context.engine.stickers.removeRecentlyUsedSticker(fileReference: .recentSticker(media: file)) })) ) } } - + guard let view = view else { return nil } - + return (view, itemLayer.convert(itemLayer.bounds, to: view.layer), StickerPreviewPeekContent(context: context, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: isLocked && !isStarred, menu: menuItems, openPremiumIntro: { guard let strongSelf = self, let interaction = strongSelf.interaction else { return @@ -3090,7 +3112,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { guard let strongSelf = self else { return nil } - + var presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } if let forceTheme = strongSelf.forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) @@ -3111,7 +3133,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { guard let strongSelf = self else { return } - + let _ = strongSelf }) self.viewRecords.append(ViewRecord(view: view, peekRecognizer: peekRecognizer)) diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchBarNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchBarNode.swift deleted file mode 100644 index e3e0272230..0000000000 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchBarNode.swift +++ /dev/null @@ -1,500 +0,0 @@ -import Foundation -import UIKit -import SwiftSignalKit -import AsyncDisplayKit -import Display -import TelegramPresentationData -import ActivityIndicator -import AppBundle -import FeaturedStickersScreen - -private func generateLoupeIcon(color: UIColor) -> UIImage? { - return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: color) -} - -private func generateClearIcon(color: UIColor) -> UIImage? { - return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color) -} - -private func generateBackground(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { - let diameter: CGFloat = 10.0 - return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in - context.setFillColor(backgroundColor.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(foregroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - }, opaque: true)?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) -} - -private class PaneSearchBarTextField: UITextField { - public var didDeleteBackwardWhileEmpty: (() -> Void)? - - let placeholderLabel: ImmediateTextNode - var placeholderString: NSAttributedString? { - didSet { - self.placeholderLabel.attributedText = self.placeholderString - } - } - - let prefixLabel: ASTextNode - var prefixString: NSAttributedString? { - didSet { - self.prefixLabel.attributedText = self.prefixString - } - } - - override init(frame: CGRect) { - self.placeholderLabel = ImmediateTextNode() - self.placeholderLabel.isUserInteractionEnabled = false - self.placeholderLabel.displaysAsynchronously = false - self.placeholderLabel.maximumNumberOfLines = 1 - - self.prefixLabel = ASTextNode() - self.prefixLabel.isUserInteractionEnabled = false - self.prefixLabel.displaysAsynchronously = false - - super.init(frame: frame) - - self.addSubnode(self.placeholderLabel) - self.addSubnode(self.prefixLabel) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override var keyboardAppearance: UIKeyboardAppearance { - get { - return super.keyboardAppearance - } - set { - let resigning = self.isFirstResponder - if resigning { - self.resignFirstResponder() - } - super.keyboardAppearance = newValue - if resigning { - self.becomeFirstResponder() - } - } - } - - override func textRect(forBounds bounds: CGRect) -> CGRect { - if bounds.size.width.isZero { - return CGRect(origin: CGPoint(), size: CGSize()) - } - var rect = bounds.insetBy(dx: 4.0, dy: 4.0) - - let prefixSize = self.prefixLabel.measure(bounds.size) - if !prefixSize.width.isZero { - let prefixOffset = prefixSize.width - rect.origin.x += prefixOffset - rect.size.width -= prefixOffset - } - return rect - } - - override func editingRect(forBounds bounds: CGRect) -> CGRect { - return self.textRect(forBounds: bounds) - } - - override func layoutSubviews() { - super.layoutSubviews() - - let bounds = self.bounds - if bounds.size.width.isZero { - return - } - - let constrainedSize = self.textRect(forBounds: self.bounds).size - let labelSize = self.placeholderLabel.updateLayout(constrainedSize) - self.placeholderLabel.frame = CGRect(origin: CGPoint(x: self.textRect(forBounds: bounds).minX, y: self.textRect(forBounds: bounds).minY + 4.0), size: labelSize) - - let prefixSize = self.prefixLabel.measure(constrainedSize) - let prefixBounds = bounds.insetBy(dx: 4.0, dy: 4.0) - self.prefixLabel.frame = CGRect(origin: CGPoint(x: prefixBounds.minX, y: prefixBounds.minY + 1.0), size: prefixSize) - } - - override func deleteBackward() { - if self.text == nil || self.text!.isEmpty { - self.didDeleteBackwardWhileEmpty?() - } - super.deleteBackward() - } -} - -class PaneSearchBarNode: ASDisplayNode, UITextFieldDelegate { - var cancel: (() -> Void)? - var textUpdated: ((String, String) -> Void)? - var clearPrefix: (() -> Void)? - - private let backgroundNode: ASDisplayNode - private let separatorNode: ASDisplayNode - private let textBackgroundNode: ASImageNode - private var activityIndicator: ActivityIndicator? - private let iconNode: ASImageNode - private let textField: PaneSearchBarTextField - private let clearButton: HighlightableButtonNode - private let cancelButton: HighlightableButtonNode - - var placeholderString: NSAttributedString? { - get { - return self.textField.placeholderString - } set(value) { - self.textField.placeholderString = value - } - } - - var prefixString: NSAttributedString? { - get { - return self.textField.prefixString - } set(value) { - let previous = self.prefixString - let updated: Bool - if let previous = previous, let value = value { - updated = !previous.isEqual(to: value) - } else { - updated = (previous != nil) != (value != nil) - } - if updated { - self.textField.prefixString = value - self.textField.setNeedsLayout() - self.updateIsEmpty() - } - } - } - - var text: String { - get { - return self.textField.text ?? "" - } set(value) { - if self.textField.text ?? "" != value { - self.textField.text = value - self.textFieldDidChange(self.textField) - } - } - } - - var activity: Bool = false { - didSet { - if self.activity != oldValue { - if self.activity { - if self.activityIndicator == nil, let theme = self.theme { - let activityIndicator = ActivityIndicator(type: .custom(theme.chat.inputMediaPanel.stickersSearchControlColor, 13.0, 1.0, false)) - self.activityIndicator = activityIndicator - self.addSubnode(activityIndicator) - if let (boundingSize, leftInset, rightInset) = self.validLayout { - self.updateLayout(boundingSize: boundingSize, leftInset: leftInset, rightInset: rightInset, transition: .immediate) - } - } - } else if let activityIndicator = self.activityIndicator { - self.activityIndicator = nil - activityIndicator.removeFromSupernode() - } - self.iconNode.isHidden = self.activity - } - } - } - - private var validLayout: (CGSize, CGFloat, CGFloat)? - private var theme: PresentationTheme? - - override init() { - self.backgroundNode = ASDisplayNode() - self.backgroundNode.isLayerBacked = true - - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - - self.textBackgroundNode = ASImageNode() - self.textBackgroundNode.isLayerBacked = false - self.textBackgroundNode.displaysAsynchronously = false - self.textBackgroundNode.displayWithoutProcessing = true - - self.iconNode = ASImageNode() - self.iconNode.isUserInteractionEnabled = false - self.iconNode.displaysAsynchronously = false - self.iconNode.displayWithoutProcessing = true - - self.textField = PaneSearchBarTextField() - self.textField.accessibilityTraits = .searchField - self.textField.autocorrectionType = .no - self.textField.returnKeyType = .search - self.textField.font = Font.regular(17.0) - - self.clearButton = HighlightableButtonNode() - self.clearButton.imageNode.displaysAsynchronously = false - self.clearButton.imageNode.displayWithoutProcessing = true - self.clearButton.displaysAsynchronously = false - self.clearButton.isHidden = true - - self.cancelButton = HighlightableButtonNode() - self.cancelButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) - self.cancelButton.displaysAsynchronously = false - - super.init() - - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) - - self.addSubnode(self.textBackgroundNode) - self.view.addSubview(self.textField) - self.addSubnode(self.iconNode) - self.addSubnode(self.clearButton) - self.addSubnode(self.cancelButton) - - self.textField.delegate = self - self.textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged) - - self.textField.didDeleteBackwardWhileEmpty = { [weak self] in - self?.clearPressed() - } - - self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) - self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside) - } - - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - self.theme = theme - - if let activityIndicator = self.activityIndicator { - activityIndicator.type = .custom(theme.chat.inputMediaPanel.stickersSearchControlColor, 13.0, 1.0, false) - } - self.separatorNode.backgroundColor = theme.chat.inputMediaPanel.panelSeparatorColor - self.textBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: theme.chat.inputMediaPanel.stickersSearchBackgroundColor) - self.textField.textColor = theme.chat.inputMediaPanel.stickersSearchPrimaryColor - self.iconNode.image = generateLoupeIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor) - self.clearButton.setImage(generateClearIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor), for: []) - self.cancelButton.setAttributedTitle(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: []) - self.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance - self.textField.tintColor = theme.list.itemAccentColor - - if let (boundingSize, leftInset, rightInset) = self.validLayout { - self.updateLayout(boundingSize: boundingSize, leftInset: leftInset, rightInset: rightInset, transition: .immediate) - } - } - - func updateLayout(boundingSize: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { - self.validLayout = (boundingSize, leftInset, rightInset) - - self.backgroundNode.frame = self.bounds - transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel))) - - let verticalOffset: CGFloat = -20.0 - - let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: boundingSize.width - leftInset - rightInset, height: boundingSize.height)) - - let cancelButtonSize = self.cancelButton.measure(CGSize(width: 100.0, height: CGFloat.infinity)) - transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: contentFrame.maxX - 8.0 - cancelButtonSize.width, y: verticalOffset + 36.0), size: cancelButtonSize)) - - let textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + 8.0, y: verticalOffset + 28.0), size: CGSize(width: contentFrame.width - 16.0 - cancelButtonSize.width - 11.0, height: 36.0)) - transition.updateFrame(node: self.textBackgroundNode, frame: textBackgroundFrame) - - let textFrame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 27.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 27.0 - 20.0), height: textBackgroundFrame.size.height)) - - if let iconImage = self.iconNode.image { - let iconSize = iconImage.size - transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 5.0, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - iconSize.height) / 2.0)), size: iconSize)) - } - - if let activityIndicator = self.activityIndicator { - let indicatorSize = activityIndicator.measure(CGSize(width: 32.0, height: 32.0)) - transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 11.0, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - indicatorSize.height) / 2.0)), size: indicatorSize)) - } - - let clearSize = self.clearButton.measure(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.maxX - 8.0 - clearSize.width, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - clearSize.height) / 2.0)), size: clearSize)) - - self.textField.frame = textFrame - self.textField.layoutSubviews() - } - - @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - if let cancel = self.cancel { - cancel() - } - } - } - - func activate() { - self.textField.becomeFirstResponder() - } - - func animateIn(from node: PaneSearchBarPlaceholderNode, duration: Double, timingFunction: String, completion: @escaping () -> Void) { - let initialTextBackgroundFrame = node.view.convert(node.backgroundNode.frame, to: self.view) - - var backgroundCompleted = false - var separatorCompleted = false - var textBackgroundCompleted = false - let intermediateCompletion: () -> Void = { - if backgroundCompleted && separatorCompleted && textBackgroundCompleted { - completion() - } - } - - let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.size.width, height: max(0.0, initialTextBackgroundFrame.maxY + 8.0))) - if let fromBackgroundColor = node.backgroundColor, let toBackgroundColor = self.backgroundNode.backgroundColor { - self.backgroundNode.layer.animate(from: fromBackgroundColor.cgColor, to: toBackgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: duration * 0.7) - } else { - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - } - self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: timingFunction, completion: { _ in - backgroundCompleted = true - intermediateCompletion() - }) - - let initialSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, initialTextBackgroundFrame.maxY + 8.0)), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel)) - self.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - self.separatorNode.layer.animateFrame(from: initialSeparatorFrame, to: self.separatorNode.frame, duration: duration, timingFunction: timingFunction, completion: { _ in - separatorCompleted = true - intermediateCompletion() - }) - - self.textBackgroundNode.layer.animateFrame(from: initialTextBackgroundFrame, to: self.textBackgroundNode.frame, duration: duration, timingFunction: timingFunction, completion: { _ in - textBackgroundCompleted = true - intermediateCompletion() - }) - - let labelFrame = self.textField.placeholderLabel.frame - let initialLabelNodeFrame = CGRect(origin: node.labelNode.view.convert(node.labelNode.bounds, to: self.textField.superview).origin, size: labelFrame.size) - self.textField.layer.animateFrame(from: CGRect(origin: initialLabelNodeFrame.origin.offsetBy(dx: -labelFrame.minX, dy: -labelFrame.minY), size: self.textField.frame.size), to: self.textField.frame, duration: duration, timingFunction: timingFunction) - - let iconFrame = self.iconNode.frame - let initialIconFrame = CGRect(origin: node.iconNode.view.convert(node.iconNode.bounds, to: self.iconNode.view.superview).origin, size: iconFrame.size) - self.iconNode.layer.animateFrame(from: initialIconFrame, to: self.iconNode.frame, duration: duration, timingFunction: timingFunction) - - let cancelButtonFrame = self.cancelButton.frame - self.cancelButton.layer.animatePosition(from: CGPoint(x: self.bounds.size.width + cancelButtonFrame.size.width / 2.0, y: initialTextBackgroundFrame.minY + 2.0 + cancelButtonFrame.size.height / 2.0), to: self.cancelButton.layer.position, duration: duration, timingFunction: timingFunction) - node.isHidden = true - } - - func deactivate(clear: Bool = true) { - self.textField.resignFirstResponder() - if clear { - self.textField.text = nil - self.textField.placeholderLabel.isHidden = false - } - } - - func transitionOut(to node: PaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { - let targetTextBackgroundFrame = node.view.convert(node.backgroundNode.view.frame, to: self.view) - - let duration: Double = 0.5 - let timingFunction = kCAMediaTimingFunctionSpring - - node.isHidden = true - self.clearButton.isHidden = true - self.textField.text = "" - - var backgroundCompleted = false - var separatorCompleted = false - var textBackgroundCompleted = false - let intermediateCompletion: () -> Void = { [weak node] in - if backgroundCompleted && separatorCompleted && textBackgroundCompleted { - completion() - node?.isHidden = false - } - } - - let targetBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.size.width, height: max(0.0, targetTextBackgroundFrame.maxY + 8.0))) - if let toBackgroundColor = node.backgroundColor, let fromBackgroundColor = self.backgroundNode.backgroundColor { - self.backgroundNode.layer.animate(from: fromBackgroundColor.cgColor, to: toBackgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: duration * 0.5, removeOnCompletion: false) - } else { - self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration / 2.0, removeOnCompletion: false) - } - self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: targetBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in - backgroundCompleted = true - intermediateCompletion() - }) - - let targetSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, targetTextBackgroundFrame.maxY + 8.0)), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel)) - self.separatorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration / 2.0, removeOnCompletion: false) - self.separatorNode.layer.animateFrame(from: self.separatorNode.frame, to: targetSeparatorFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in - separatorCompleted = true - intermediateCompletion() - }) - - self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in - textBackgroundCompleted = true - intermediateCompletion() - }) - - let transitionBackgroundNode = ASImageNode() - transitionBackgroundNode.isLayerBacked = true - transitionBackgroundNode.displaysAsynchronously = false - transitionBackgroundNode.displayWithoutProcessing = true - transitionBackgroundNode.image = node.backgroundNode.image - self.insertSubnode(transitionBackgroundNode, aboveSubnode: self.textBackgroundNode) - transitionBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0, removeOnCompletion: false) - transitionBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) - - let textFieldFrame = self.textField.frame - let targetLabelNodeFrame = CGRect(origin: node.labelNode.view.convert(node.labelNode.bounds, to: self.textField.superview).origin, size: textFieldFrame.size) - self.textField.layer.animateFrame(from: self.textField.frame, to: CGRect(origin: targetLabelNodeFrame.origin.offsetBy(dx: -self.textField.placeholderLabel.frame.minX, dy: -self.textField.placeholderLabel.frame.minY), size: self.textField.frame.size), duration: duration, timingFunction: timingFunction, removeOnCompletion: false) - if let snapshot = node.labelNode.layer.snapshotContentTree() { - snapshot.frame = CGRect(origin: self.textField.placeholderLabel.frame.origin, size: node.labelNode.frame.size) - self.textField.layer.addSublayer(snapshot) - snapshot.animateAlpha(from: 0.0, to: 1.0, duration: duration * 2.0 / 3.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue) - //self.textField.placeholderLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 3.0 / 2.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false) - - } - - let iconFrame = self.iconNode.frame - let targetIconFrame = CGRect(origin: node.iconNode.view.convert(node.iconNode.bounds, to: self.iconNode.view.superview).origin, size: iconFrame.size) - self.iconNode.image = node.iconNode.image - self.iconNode.layer.animateFrame(from: self.iconNode.frame, to: targetIconFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) - - let cancelButtonFrame = self.cancelButton.frame - self.cancelButton.layer.animatePosition(from: self.cancelButton.layer.position, to: CGPoint(x: self.bounds.size.width + cancelButtonFrame.size.width / 2.0, y: targetTextBackgroundFrame.minY + 4.0 + cancelButtonFrame.size.height / 2.0), duration: duration, timingFunction: timingFunction, removeOnCompletion: false) - } - - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - if string.range(of: "\n") != nil { - return false - } - return true - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - self.textField.resignFirstResponder() - return false - } - - @objc func textFieldDidChange(_ textField: UITextField) { - self.updateIsEmpty() - if let textUpdated = self.textUpdated { - textUpdated(textField.text ?? "", self.textField.textInputMode?.primaryLanguage ?? "") - } - } - - private func updateIsEmpty() { - let isEmpty = !(textField.text?.isEmpty ?? true) - if isEmpty != self.textField.placeholderLabel.isHidden { - self.textField.placeholderLabel.isHidden = isEmpty - } - self.clearButton.isHidden = !isEmpty && self.prefixString == nil - } - - @objc func cancelPressed() { - if let cancel = self.cancel { - cancel() - } - } - - @objc func clearPressed() { - if (self.textField.text?.isEmpty ?? true) { - if self.prefixString != nil { - self.clearPrefix?() - } - } else { - self.textField.text = "" - self.textFieldDidChange(self.textField) - } - } - - func updateQuery(_ query: String) { - self.textField.text = query - self.textFieldDidChange(self.textField) - } -} diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift index 759f4b04cf..b9255fdffd 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift @@ -2,6 +2,8 @@ import Foundation import UIKit import AsyncDisplayKit import Display +import ComponentFlow +import SearchBarNode import SwiftSignalKit import Postbox import TelegramCore @@ -9,27 +11,47 @@ import TelegramPresentationData import AccountContext import ChatPresentationInterfaceState import EntityKeyboard +import ContextUI +import GlassControls +import MultilineTextComponent import ChatControllerInteraction import MultiplexedVideoNode import FeaturedStickersScreen import StickerPeekUI import EntityKeyboardGifContent import BatchVideoRendering +import UndoUI -private let searchBarHeight: CGFloat = 52.0 +private let searchBarHeight: CGFloat = 76.0 +private let searchBarTopInset: CGFloat = 16.0 +private let searchBarFieldHeight: CGFloat = 44.0 + +private func paneSearchBarTheme(_ theme: PresentationTheme) -> SearchBarNodeTheme { + return SearchBarNodeTheme( + background: .clear, + separator: .clear, + inputFill: .clear, + primaryText: theme.chat.inputPanel.panelControlColor, + placeholder: theme.chat.inputPanel.inputPlaceholderColor, + inputIcon: theme.chat.inputPanel.inputControlColor, + inputClear: theme.chat.inputPanel.panelControlColor, + accent: theme.chat.inputPanel.panelControlAccentColor, + keyboard: theme.rootController.keyboardColor + ) +} public protocol PaneSearchContentNode { var ready: Signal { get } var deactivateSearchBar: (() -> Void)? { get set } var updateActivity: ((Bool) -> Void)? { get set } - + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) func updateText(_ text: String, languageCode: String?) func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) - + func animateIn(additivePosition: CGFloat, transition: ContainedViewLayoutTransition) func animateOut(transition: ContainedViewLayoutTransition) - + func updatePreviewing(animated: Bool) func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? } @@ -41,26 +63,34 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer private let interaction: ChatEntityKeyboardInputNode.Interaction private let inputNodeInteraction: ChatMediaInputNodeInteraction private let peekBehavior: EmojiContentPeekBehavior? - + private let backgroundNode: ASDisplayNode - private let searchBar: PaneSearchBarNode - - private var validLayout: CGSize? - + private let searchBar: SearchBarNode + private let navigationButtons = ComponentView() + private let selectedPackTitle = ComponentView() + + private var theme: PresentationTheme + private var strings: PresentationStrings + private var validLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics)? + private weak var animatedPlaceholder: PaneSearchBarPlaceholderNode? + private var selectedStickerPack: StickerPaneSearchSelectedPack? + public var onCancel: (() -> Void)? - + public var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? - + public var ready: Signal { return self.contentNode.ready } - + public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, interaction: ChatEntityKeyboardInputNode.Interaction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, batchVideoRenderingContext: BatchVideoRenderingContext?, stickerActionTitle: String? = nil, trendingGifsPromise: Promise, cancel: @escaping () -> Void, peekBehavior: EmojiContentPeekBehavior?) { self.context = context self.mode = mode self.interaction = interaction self.inputNodeInteraction = inputNodeInteraction self.peekBehavior = peekBehavior + self.theme = theme + self.strings = strings switch mode { case .gif: self.contentNode = GifPaneSearchContentNode(context: context, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction, batchVideoRenderingContext: batchVideoRenderingContext ?? BatchVideoRenderingContext(context: context), trendingPromise: trendingGifsPromise) @@ -68,38 +98,43 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer self.contentNode = StickerPaneSearchContentNode(context: context, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction, stickerActionTitle: stickerActionTitle) } self.backgroundNode = ASDisplayNode() - - self.searchBar = PaneSearchBarNode() - + + self.searchBar = SearchBarNode( + theme: paneSearchBarTheme(theme), + presentationTheme: theme, + strings: strings, + fieldStyle: .glass, + displayBackground: false + ) + super.init() - + self.clipsToBounds = true - + self.addSubnode(self.backgroundNode) self.addSubnode(self.contentNode) self.addSubnode(self.searchBar) - + self.contentNode.deactivateSearchBar = { [weak self] in self?.searchBar.deactivate(clear: false) } self.contentNode.updateActivity = { [weak self] active in self?.searchBar.activity = active } - + self.searchBar.cancel = { [weak self] in + self?.searchBar.deactivate(clear: false) cancel() - - self?.searchBar.view.endEditing(true) self?.onCancel?() } self.searchBar.activate() - + self.searchBar.textUpdated = { [weak self] text, languageCode in self?.contentNode.updateText(text, languageCode: languageCode) } - + self.updateThemeAndStrings(theme: theme, strings: strings) - + if let contentNode = self.contentNode as? GifPaneSearchContentNode { contentNode.requestUpdateQuery = { [weak self] query in self?.updateQuery(query) @@ -108,7 +143,20 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer self?.openGifContextMenu?(file, node, rect, gesture, isSaved) } } - + + if let contentNode = self.contentNode as? StickerPaneSearchContentNode { + contentNode.selectedPackUpdated = { [weak self] pack in + guard let self else { + return + } + self.selectedStickerPack = pack + if pack != nil { + self.searchBar.deactivate(clear: false) + } + self.requestLayout(transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } + if let contentNode = self.contentNode as? StickerPaneSearchContentNode, let peekBehavior = self.peekBehavior { peekBehavior.setGestureRecognizerEnabled(view: self.contentNode.view, isEnabled: true, itemAtPoint: { [weak contentNode] point in guard let contentNode else { @@ -117,7 +165,7 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer guard let (itemNode, item) = contentNode.itemAt(point: point) else { return nil } - + var maybeFile: TelegramMediaFile? if let item = item as? StickerPreviewPeekItem { switch item { @@ -132,7 +180,7 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer guard let file = maybeFile else { return nil } - + var groupId: AnyHashable = AnyHashable("search") for attribute in file.attributes { if case let .Sticker(_, packReference, _) = attribute { @@ -141,17 +189,19 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer } } } - + return (groupId, itemNode.layer, file) }) } } - + public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0) self.contentNode.updateThemeAndStrings(theme: theme, strings: strings) - self.searchBar.updateThemeAndStrings(theme: theme, strings: strings) - + self.searchBar.updateThemeAndStrings(theme: paneSearchBarTheme(theme), presentationTheme: theme, strings: strings) + let placeholder: String switch mode { case .gif: @@ -159,76 +209,329 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer case .sticker, .trending: placeholder = strings.Stickers_Search } - self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor) + self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor) } - + public func updateQuery(_ query: String) { - self.searchBar.updateQuery(query) + self.searchBar.text = query } - + public func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? { return self.contentNode.itemAt(point: CGPoint(x: point.x, y: point.y - searchBarHeight)) } - + + private func openMore() { + guard let selectedStickerPack = self.selectedStickerPack, let controlsView = self.navigationButtons.view as? GlassControlPanelComponent.View, let rightItemView = controlsView.rightItemView, let sourceView = rightItemView.itemView(id: AnyHashable("more")) else { + return + } + + let link = "https://t.me/addstickers/\(selectedStickerPack.info.shortName)" + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: self.theme) + let strings = self.strings + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: strings.StickerPack_Share, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Share"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + let shareController = self.context.sharedContext.makeShareController( + context: self.context, + params: ShareControllerParams( + subject: .url(link), + externalShare: false, + actionCompleted: { [weak self] in + guard let self else { + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: self.theme) + self.interaction.presentController(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in + return false + }), nil) + } + ) + ) + self.interaction.presentController(shareController, nil) + }))) + + items.append(.action(ContextMenuActionItem(text: strings.StickerPack_CopyLink, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + + UIPasteboard.general.string = link + guard let self else { + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: self.theme) + self.interaction.presentController(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in + return false + }), nil) + }))) + + if selectedStickerPack.installed { + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: strings.StickerPack_RemoveStickerSet, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + let info = selectedStickerPack.info + let context = self.context + let _ = (context.engine.stickers.removeStickerPackInteractively(id: info.id, option: .delete) + |> deliverOnMainQueue).start(next: { [weak self] indexAndItems in + guard let self, let (positionInList, items) = indexAndItems else { + return + } + + let stickerItems = items.compactMap { $0 as? StickerPackItem } + if let contentNode = self.contentNode as? StickerPaneSearchContentNode { + contentNode.setPackInstalledState(id: info.id, installed: false) + } + + var animateInAsReplacement = false + if let navigationController = self.interaction.getNavigationController() { + for controller in navigationController.overlayControllers { + if let controller = controller as? UndoOverlayController { + controller.dismissWithCommitActionAndReplacementAnimation() + animateInAsReplacement = true + } + } + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: self.theme) + let undoController = UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_RemovedTitle, text: presentationData.strings.StickerPackActionInfo_RemovedText(info.title).string, undo: true, info: info, topItem: stickerItems.first, context: context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { [weak self] action in + if case .undo = action { + let _ = context.engine.stickers.addStickerPackInteractively(info: info, items: stickerItems, positionInList: positionInList).start() + if let contentNode = self?.contentNode as? StickerPaneSearchContentNode { + contentNode.setPackInstalledState(id: info.id, installed: true) + } + } + return true + }) + if let navigationController = self.interaction.getNavigationController() { + navigationController.presentOverlay(controller: undoController) + } else { + self.interaction.presentController(undoController, nil) + } + }) + }))) + } + + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(StickerPaneSearchHeaderContextReferenceContentSource(sourceView: sourceView)), + items: .single(ContextController.Items(content: .list(items))), + gesture: nil + ) + self.interaction.presentGlobalOverlayController(contextController, nil) + } + + private func requestLayout(transition: ContainedViewLayoutTransition) { + guard let (size, leftInset, rightInset, bottomInset, inputHeight, deviceMetrics) = self.validLayout else { + return + } + self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: transition) + } + public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { - self.validLayout = size + self.validLayout = (size, leftInset, rightInset, bottomInset, inputHeight, deviceMetrics) transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + + let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: searchBarTopInset), size: CGSize(width: size.width, height: searchBarFieldHeight)) + transition.updateFrame(node: self.searchBar, frame: searchBarFrame) + self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition) + self.searchBar.isUserInteractionEnabled = self.selectedStickerPack == nil + transition.updateAlpha(node: self.searchBar, alpha: self.selectedStickerPack == nil ? 1.0 : 0.0) - transition.updateFrame(node: self.searchBar, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: searchBarHeight))) - self.searchBar.updateLayout(boundingSize: CGSize(width: size.width, height: searchBarHeight), leftInset: leftInset, rightInset: rightInset, transition: transition) + let componentTransition = ComponentTransition(transition) + let navigationButtonsFrame = CGRect(origin: CGPoint(x: leftInset + 16.0, y: searchBarTopInset), size: CGSize(width: max(1.0, size.width - leftInset - rightInset - 16.0 * 2.0), height: 48.0)) + + let navigationButtonsSize = self.navigationButtons.update( + transition: componentTransition, + component: AnyComponent(GlassControlPanelComponent( + theme: self.theme, + leftItem: self.selectedStickerPack == nil ? nil : GlassControlPanelComponent.Item( + items: [ + GlassControlGroupComponent.Item( + id: AnyHashable("back"), + content: .icon("Navigation/Back"), + action: { [weak self] in + guard let self, let contentNode = self.contentNode as? StickerPaneSearchContentNode else { + return + } + contentNode.clearSelectedPack() + } + ) + ], + background: .panel + ), + centralItem: nil, + rightItem: self.selectedStickerPack == nil ? nil : GlassControlPanelComponent.Item( + items: [ + GlassControlGroupComponent.Item( + id: AnyHashable("more"), + content: .animation("anim_morewide"), + action: { [weak self] in + self?.openMore() + } + ) + ], + background: .panel + ), + centerAlignmentIfPossible: true, + isDark: self.theme.overallDarkAppearance + )), + environment: {}, + containerSize: navigationButtonsFrame.size + ) + if let navigationButtons = self.navigationButtons.view { + if navigationButtons.superview == nil { + self.view.addSubview(navigationButtons) + } + navigationButtons.isUserInteractionEnabled = self.selectedStickerPack != nil + componentTransition.setFrame(view: navigationButtons, frame: CGRect(origin: navigationButtonsFrame.origin, size: navigationButtonsSize)) + //componentTransition.setAlpha(view: navigationButtons, alpha: self.selectedStickerPack != nil ? 1.0 : 0.0) + } + + let title = self.selectedStickerPack?.info.title ?? "" + let titleSize = self.selectedPackTitle.update( + transition: componentTransition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: self.theme.chat.inputPanel.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: max(1.0, size.width - leftInset - rightInset - 140.0), height: searchBarFieldHeight) + ) + if let titleView = self.selectedPackTitle.view { + if titleView.superview == nil { + self.view.addSubview(titleView) + } + titleView.isUserInteractionEnabled = false + let titleOrigin = CGPoint(x: leftInset + floor((size.width - leftInset - rightInset - titleSize.width) / 2.0), y: searchBarTopInset + floor((searchBarFieldHeight - titleSize.height) / 2.0)) + titleView.frame = CGRect(origin: titleOrigin, size: titleSize) + componentTransition.setAlpha(view: titleView, alpha: self.selectedStickerPack != nil ? 1.0 : 0.0) + } let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: searchBarHeight), size: CGSize(width: size.width - leftInset - rightInset, height: size.height - searchBarHeight)) - transition.updateFrame(node: self.contentNode, frame: contentFrame) self.contentNode.updateLayout(size: contentFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: transition) } - + public func deactivate() { + if let contentNode = self.contentNode as? StickerPaneSearchContentNode { + contentNode.clearSelectedPack() + } self.searchBar.deactivate(clear: true) } - + public func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { var verticalOrigin: CGFloat = anhorTopView.convert(anchorTop, to: self.view).y if let placeholder = placeholder { + self.animatedPlaceholder = placeholder + placeholder.isHidden = true + let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view) verticalOrigin = placeholderFrame.minY - 4.0 self.contentNode.animateIn(additivePosition: verticalOrigin, transition: transition) } else { self.contentNode.animateIn(additivePosition: 0.0, transition: transition) } - + + let searchBarFrame = self.searchBar.frame + let initialSearchBarFrame = CGRect(origin: CGPoint(x: searchBarFrame.minX, y: verticalOrigin), size: searchBarFrame.size) + switch transition { case let .animated(duration, curve): self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0) - if let placeholder = placeholder { - self.searchBar.animateIn(from: placeholder, duration: duration, timingFunction: curve.timingFunction, completion: completion) - } else { - self.searchBar.alpha = 0.0 - transition.updateAlpha(node: self.searchBar, alpha: 1.0) - } - if let size = self.validLayout { - let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin))) + + self.searchBar.alpha = 1.0 + self.searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction, completion: { _ in + completion() + }) + self.searchBar.layer.animateFrame(from: initialSearchBarFrame, to: searchBarFrame, duration: duration, timingFunction: curve.timingFunction) + + if let layout = self.validLayout { + let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: layout.size.width, height: max(0.0, layout.size.height - verticalOrigin))) self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: curve.timingFunction) } case .immediate: + completion() break } } - + public func animateOut(to placeholder: PaneSearchBarPlaceholderNode, animateOutSearchBar: Bool, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { - if case let .animated(duration, curve) = transition { - if let size = self.validLayout { - let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view) - let verticalOrigin = placeholderFrame.minY - 4.0 - self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin))), duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false) + let finish: () -> Void = { [weak self] in + placeholder.isHidden = false + if let self, self.animatedPlaceholder === placeholder { + self.animatedPlaceholder = nil } + completion() } - self.searchBar.transitionOut(to: placeholder, transition: transition, completion: completion) + + if case let .animated(duration, curve) = transition { + let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view) + let verticalOrigin = placeholderFrame.minY - 4.0 + let targetSearchBarFrame = CGRect(origin: CGPoint(x: self.searchBar.frame.minX, y: verticalOrigin), size: self.searchBar.frame.size) + + if let layout = self.validLayout { + self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: layout.size.width, height: max(0.0, layout.size.height - verticalOrigin))), duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false) + } + + self.searchBar.layer.animateFrame(from: self.searchBar.frame, to: targetSearchBarFrame, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false) + if animateOutSearchBar { + self.searchBar.alpha = 0.0 + self.searchBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in + finish() + }) + } else { + self.searchBar.layer.animateAlpha(from: self.searchBar.alpha, to: self.searchBar.alpha, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in + finish() + }) + } + } else { + if animateOutSearchBar { + self.searchBar.alpha = 0.0 + } + finish() + } + transition.updateAlpha(node: self.backgroundNode, alpha: 0.0) if animateOutSearchBar { transition.updateAlpha(node: self.searchBar, alpha: 0.0) } + let componentTransition = ComponentTransition(transition) + if let headerView = self.navigationButtons.view { + componentTransition.setAlpha(view: headerView, alpha: 0.0) + } + if let titleView = self.selectedPackTitle.view { + componentTransition.setAlpha(view: titleView, alpha: 0.0) + } self.contentNode.animateOut(transition: transition) self.deactivate() } } + +private final class StickerPaneSearchHeaderContextReferenceContentSource: ContextReferenceContentSource { + private weak var sourceView: UIView? + + init(sourceView: UIView) { + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + guard let sourceView = self.sourceView else { + return nil + } + return ContextControllerReferenceViewInfo(referenceView: sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift index f67543bd15..bb5eeb9b62 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/StickerPaneSearchContentNode.swift @@ -2,6 +2,9 @@ import Foundation import UIKit import AsyncDisplayKit import Display +import ButtonComponent +import ComponentFlow +import EdgeEffect import SwiftSignalKit import Postbox import TelegramCore @@ -19,93 +22,231 @@ import ChatControllerInteraction import FeaturedStickersScreen import ChatPresentationInterfaceState import StickerResources +import EntityKeyboard +import EmojiTextAttachmentView +import MultilineTextComponent +import TextFormat + +private let packPanelHeight: CGFloat = 76.0 +private let collapsedPackPanelHeight: CGFloat = 40.0 private enum StickerSearchEntryId: Equatable, Hashable { case sticker(String?, Int64) - case global(ItemCollectionId) } private enum StickerSearchEntry: Identifiable, Comparable { case sticker(index: Int, code: String?, stickerItem: FoundStickerItem, theme: PresentationTheme) - case global(index: Int, info: StickerPackCollectionInfo, topItems: [StickerPackItem], installed: Bool, topSeparator: Bool) - + var stableId: StickerSearchEntryId { switch self { case let .sticker(_, code, stickerItem, _): return .sticker(code, stickerItem.file.fileId.id) - case let .global(_, info, _, _, _): - return .global(info.id) } } - + static func ==(lhs: StickerSearchEntry, rhs: StickerSearchEntry) -> Bool { - switch lhs { - case let .sticker(lhsIndex, lhsCode, lhsStickerItem, lhsTheme): - if case let .sticker(rhsIndex, rhsCode, rhsStickerItem, rhsTheme) = rhs { - if lhsIndex != rhsIndex { - return false - } - if lhsCode != rhsCode { - return false - } - if lhsStickerItem != rhsStickerItem { - return false - } - if lhsTheme !== rhsTheme { - return false - } - return true - } else { + switch (lhs, rhs) { + case let (.sticker(lhsIndex, lhsCode, lhsStickerItem, lhsTheme), .sticker(rhsIndex, rhsCode, rhsStickerItem, rhsTheme)): + if lhsIndex != rhsIndex { return false } - case let .global(index, info, topItems, installed, topSeparator): - if case .global(index, info, topItems, installed, topSeparator) = rhs { - return true - } else { + if lhsCode != rhsCode { return false } + if lhsStickerItem != rhsStickerItem { + return false + } + if lhsTheme !== rhsTheme { + return false + } + return true } } - + static func <(lhs: StickerSearchEntry, rhs: StickerSearchEntry) -> Bool { - switch lhs { - case let .sticker(lhsIndex, _, _, _): - switch rhs { - case let .sticker(rhsIndex, _, _, _): - return lhsIndex < rhsIndex - default: - return true - } - case let .global(lhsIndex, _, _, _, _): - switch rhs { - case .sticker: - return false - case let .global(rhsIndex, _, _, _, _): - return lhsIndex < rhsIndex - } + switch (lhs, rhs) { + case let (.sticker(lhsIndex, _, _, _), .sticker(rhsIndex, _, _, _)): + return lhsIndex < rhsIndex } } - + func item(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, interaction: StickerPaneSearchInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> GridItem { switch self { case let .sticker(_, code, stickerItem, theme): return StickerPaneSearchStickerItem(context: context, theme: theme, code: code, stickerItem: stickerItem, inputNodeInteraction: inputNodeInteraction, selected: { node, layer, rect in interaction.sendSticker(.standalone(media: stickerItem.file), node.view, layer, rect) }) - case let .global(_, info, topItems, installed, topSeparator): - let itemContext = StickerPaneSearchGlobalItemContext() - itemContext.canPlayMedia = true - return StickerPaneSearchGlobalItem(context: context, theme: theme, strings: strings, listAppearance: false, info: StickerPackCollectionInfo.Accessor(info), topItems: topItems, topSeparator: topSeparator, regularInsets: false, installed: installed, unread: false, open: { - interaction.open(info) - }, install: { - interaction.install(info, topItems, !installed) - }, getItemIsPreviewed: { item in - return interaction.getItemIsPreviewed(item) - }, itemContext: itemContext) } } } +struct StickerPaneSearchSelectedPack { + let info: StickerPackCollectionInfo + let installed: Bool +} + +private struct StickerPaneSearchPack: Equatable { + let info: StickerPackCollectionInfo + let topItems: [StickerPackItem] + let installed: Bool +} + +private final class StickerSearchPackTopPanelItemComponent: Component { + typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment + + let context: AccountContext + let theme: PresentationTheme + let info: StickerPackCollectionInfo + let topItem: StickerPackItem? + let pressed: () -> Void + + init( + context: AccountContext, + theme: PresentationTheme, + info: StickerPackCollectionInfo, + topItem: StickerPackItem?, + pressed: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.info = info + self.topItem = topItem + self.pressed = pressed + } + + static func ==(lhs: StickerSearchPackTopPanelItemComponent, rhs: StickerSearchPackTopPanelItemComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.info != rhs.info { + return false + } + if lhs.topItem != rhs.topItem { + return false + } + return true + } + + final class View: UIView { + private var itemLayer: InlineStickerItemLayer? + private var itemFileId: MediaId? + private var titleView: ComponentView? + private var component: StickerSearchPackTopPanelItemComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + 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 { + self.component?.pressed() + } + } + + func update(component: StickerSearchPackTopPanelItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value + let file = component.topItem?.file._parse() + let fileId = file?.fileId + if self.itemFileId != fileId { + self.itemFileId = fileId + if let itemLayer = self.itemLayer { + self.itemLayer = nil + itemLayer.removeFromSuperlayer() + } + + if let file { + let itemDimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) + let displaySize = itemDimensions.aspectFitted(CGSize(width: 44.0, height: 44.0)) + let itemLayer = InlineStickerItemLayer( + context: component.context, + userLocation: .other, + attemptSynchronousLoad: false, + emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file), + file: file, + cache: component.context.animationCache, + renderer: component.context.animationRenderer, + placeholderColor: component.theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1), + pointSize: displaySize, + dynamicColor: .white + ) + self.itemLayer = itemLayer + self.layer.addSublayer(itemLayer) + } + } + + let iconFitSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 44.0, height: 44.0) : CGSize(width: 24.0, height: 24.0) + if let itemLayer = self.itemLayer, let file { + let itemDimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) + let iconSize = itemDimensions.aspectFitted(iconFitSize) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) / 2.0), y: floor((iconFitSize.height - iconSize.height) / 2.0)), size: iconSize) + transition.setPosition(layer: itemLayer, position: CGPoint(x: iconFrame.midX, y: iconFrame.midY)) + transition.setBounds(layer: itemLayer, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + itemLayer.isVisibleForAnimations = itemEnvironment.isContentInFocus && component.context.sharedContext.energyUsageSettings.loopStickers + } + + if itemEnvironment.isExpanded { + let titleView: ComponentView + if let current = self.titleView { + titleView = current + } else { + titleView = ComponentView() + self.titleView = titleView + } + let titleSize = titleView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.info.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor)), + insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + )), + environment: {}, + containerSize: CGSize(width: 62.0, height: 100.0) + ) + if let view = titleView.view { + if view.superview == nil { + view.alpha = 0.0 + self.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height - 1.0), size: titleSize) + transition.setAlpha(view: view, alpha: 1.0) + } + } else if let titleView = self.titleView { + self.titleView = nil + if let view = titleView.view { + if !transition.animation.isImmediate { + view.alpha = 0.0 + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + } + + 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) + } +} + private struct StickerPaneSearchGridTransition { let deletions: [Int] let insertions: [GridNodeInsertItem] @@ -114,23 +255,30 @@ private struct StickerPaneSearchGridTransition { let stationaryItems: GridNodeStationaryItems let scrollToItem: GridNodeScrollToItem? let animated: Bool + let crossfade: Bool } -private func preparedChatMediaInputGridEntryTransition(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [StickerSearchEntry], to toEntries: [StickerSearchEntry], interaction: StickerPaneSearchInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> StickerPaneSearchGridTransition { +private struct StickerPaneSearchStickerState { + let context: StickerSearchContext? + let items: [FoundStickerItem] + let isLoadingMore: Bool +} + +private func preparedChatMediaInputGridEntryTransition(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [StickerSearchEntry], to toEntries: [StickerSearchEntry], interaction: StickerPaneSearchInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, crossfade: Bool) -> StickerPaneSearchGridTransition { let stationaryItems: GridNodeStationaryItems = .none let scrollToItem: GridNodeScrollToItem? = nil var animated = false animated = true - + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - + let deletions = deleteIndices let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction), previousIndex: $0.2) } let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction)) } - + let firstIndexInSectionOffset = 0 - - return StickerPaneSearchGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, animated: animated) + + return StickerPaneSearchGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, animated: animated, crossfade: crossfade) } final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { @@ -138,92 +286,131 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { private let interaction: ChatEntityKeyboardInputNode.Interaction private let inputNodeInteraction: ChatMediaInputNodeInteraction private var searchInteraction: StickerPaneSearchInteraction? - + private var theme: PresentationTheme private var strings: PresentationStrings - + private let trendingPane: ChatMediaInputTrendingPane private let gridNode: GridNode private let notFoundNode: ASImageNode private let notFoundLabel: ImmediateTextNode - - private var validLayout: CGSize? - + private let packPanel = ComponentView() + private let topEdgeEffectView = EdgeEffectView() + private let bottomEdgeEffectView = EdgeEffectView() + private let selectedPackAddButton = ComponentView() + private let packPanelVisibilityFractionUpdated = ActionSlot<(CGFloat, ComponentTransition)>() + private let packPanelActiveItemUpdated = ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>() + + private var validLayout: (size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics)? + private var enqueuedTransitions: [StickerPaneSearchGridTransition] = [] - + private let searchDisposable = MetaDisposable() - + private let selectedPackDisposable = MetaDisposable() + private let queue = Queue() private let currentEntries = Atomic<[StickerSearchEntry]?>(value: nil) private let currentRemotePacks = Atomic(value: nil) - + private var currentSearchEntries: [StickerSearchEntry] = [] + private var currentPacks: [StickerPaneSearchPack] = [] + private var currentSearchIsFinal: Bool = false + private var searchIsActive: Bool = false + private var selectedPack: StickerPaneSearchPack? + private var isPackPanelExpanded: Bool = true + private var installedPackIds = Set() + private var stickerSearchContext: StickerSearchContext? + private var currentSearchStickerCount: Int = 0 + private var currentStickerCount: Int = 0 + private let _ready = Promise() var ready: Signal { return self._ready.get() } - + var deactivateSearchBar: (() -> Void)? var updateActivity: ((Bool) -> Void)? - + var selectedPackUpdated: ((StickerPaneSearchSelectedPack?) -> Void)? + private let installDisposable = MetaDisposable() - + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, interaction: ChatEntityKeyboardInputNode.Interaction, inputNodeInteraction: ChatMediaInputNodeInteraction, stickerActionTitle: String?) { self.context = context self.interaction = interaction self.inputNodeInteraction = inputNodeInteraction - + self.theme = theme self.strings = strings - + let trendingPaneInteraction = ChatMediaInputTrendingPane.Interaction( sendSticker: interaction.sendSticker, presentController: interaction.presentController, getNavigationController: interaction.getNavigationController ) - + self.trendingPane = ChatMediaInputTrendingPane(context: context, forceTheme: theme, interaction: trendingPaneInteraction, getItemIsPreviewed: { [weak inputNodeInteraction] item in return inputNodeInteraction?.previewedStickerPackItemFile?.id == item.file.id }, isPane: false) self.trendingPane.stickerActionTitle = stickerActionTitle - + self.gridNode = GridNode() - + self.notFoundNode = ASImageNode() self.notFoundNode.displayWithoutProcessing = true self.notFoundNode.displaysAsynchronously = false self.notFoundNode.clipsToBounds = false - + self.notFoundLabel = ImmediateTextNode() self.notFoundLabel.displaysAsynchronously = false self.notFoundLabel.isUserInteractionEnabled = false self.notFoundNode.addSubnode(self.notFoundLabel) - + self.gridNode.isHidden = true self.trendingPane.isHidden = false self.notFoundNode.isHidden = true - + self.topEdgeEffectView.isUserInteractionEnabled = false + self.bottomEdgeEffectView.isUserInteractionEnabled = false + self.bottomEdgeEffectView.alpha = 0.0 + super.init() - + self.addSubnode(self.trendingPane) self.addSubnode(self.gridNode) self.addSubnode(self.notFoundNode) - + self.view.addSubview(self.topEdgeEffectView) + self.view.addSubview(self.bottomEdgeEffectView) + self.gridNode.scrollView.alwaysBounceVertical = true self.gridNode.scrollingInitiated = { [weak self] in self?.deactivateSearchBar?() } - + self.gridNode.visibleItemsUpdated = { [weak self] visibleItems in + guard let self else { + return + } + self.updatePackPanelExpansionFromScroll() + + guard let (bottomVisible, _) = visibleItems.bottomVisible else { + return + } + guard self.selectedPack == nil, self.currentStickerCount != 0 else { + return + } + if bottomVisible >= max(0, self.currentStickerCount - 8) { + self.stickerSearchContext?.loadMore() + } + } + self.trendingPane.scrollingInitiated = { [weak self] in self?.deactivateSearchBar?() } - + self.searchInteraction = StickerPaneSearchInteraction(open: { [weak self] info in if let strongSelf = self { strongSelf.view.window?.endEditing(true) let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) - + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: theme) - + let controller = strongSelf.context.sharedContext.makeStickerPackScreen( context: strongSelf.context, updatedPresentationData: (presentationData, .single(presentationData)), @@ -278,7 +465,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { return .complete() } |> deliverOnMainQueue - + let context = strongSelf.context var cancelImpl: (() -> Void)? let progressSignal = Signal { subscriber in @@ -296,7 +483,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { |> runOn(Queue.mainQueue()) |> delay(0.12, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() - + installSignal = installSignal |> afterDisposed { Queue.mainQueue().async { @@ -306,24 +493,13 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { cancelImpl = { self?.installDisposable.set(nil) } - + strongSelf.installDisposable.set(installSignal.start(next: { info, items in guard let strongSelf = self else { return } - - var animateInAsReplacement = false - if let navigationController = strongSelf.interaction.getNavigationController() { - for controller in navigationController.overlayControllers { - if let controller = controller as? UndoOverlayController { - controller.dismissWithCommitActionAndReplacementAnimation() - animateInAsReplacement = true - } - } - } - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: theme) - strongSelf.interaction.getNavigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: strongSelf.context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in + strongSelf.interaction.getNavigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: strongSelf.context), elevatedLayout: false, action: { _ in return true })) })) @@ -340,73 +516,71 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { }, getItemIsPreviewed: { item in return inputNodeInteraction.previewedStickerPackItemFile?.id == item.file.id }) - + self._ready.set(self.trendingPane.ready) self.trendingPane.activate() - + self.updateThemeAndStrings(theme: theme, strings: strings) } - + deinit { self.searchDisposable.dispose() + self.selectedPackDisposable.dispose() self.installDisposable.dispose() } - + func updateText(_ text: String, languageCode: String?) { - let signal: Signal<([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)?, NoError> - if !text.isEmpty { + if self.selectedPack != nil { + self.clearSelectedPack(applySearchResults: false) + } + self.stickerSearchContext = nil + self.currentSearchStickerCount = 0 + self.currentStickerCount = 0 + self.isPackPanelExpanded = true + let _ = self.currentRemotePacks.swap(nil) + + let query = text.trimmingCharacters(in: .whitespacesAndNewlines) + let signal: Signal<(StickerPaneSearchStickerState, FoundStickerSets, Bool, FoundStickerSets?)?, NoError> + if query.count >= 2 { let context = self.context - let stickers: Signal<([(String?, FoundStickerItem)], Bool), NoError> = Signal { subscriber in - var signals: Signal<[Signal<(String?, [FoundStickerItem], Bool), NoError>], NoError> = .single([]) - - let query = text.trimmingCharacters(in: .whitespacesAndNewlines) - if query.isSingleEmoji { - signals = .single([context.engine.stickers.searchStickers(query: nil, emoticon: [text.basicEmoji.0]) - |> map { (nil, $0.items, $0.isFinalResult) }]) - } else if query.count > 1, let languageCode = languageCode, !languageCode.isEmpty && languageCode != "emoji" { - var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3) - if !languageCode.lowercased().hasPrefix("en") { - signal = signal - |> mapToSignal { keywords in - return .single(keywords) - |> then( - context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query.lowercased(), completeMatch: query.count < 3) - |> map { englishKeywords in - return keywords + englishKeywords - } - ) - } - } - signals = signal - |> map { keywords -> [Signal<(String?, [FoundStickerItem], Bool), NoError>] in - let emoticon = keywords.flatMap { $0.emoticons }.map { $0.basicEmoji.0 } - return [context.engine.stickers.searchStickers(query: query, emoticon: emoticon, inputLanguageCode: languageCode) - |> map { (nil, $0.items, $0.isFinalResult) }] + let stickers: Signal + if query.isSingleEmoji { + let searchContext = context.engine.stickers.stickerSearchContext(query: nil, emoticon: [query.basicEmoji.0]) + stickers = searchContext.state + |> map { state -> StickerPaneSearchStickerState in + return StickerPaneSearchStickerState(context: searchContext, items: state.items, isLoadingMore: state.isLoadingMore) + } + } else if query.count > 1, let languageCode = languageCode, !languageCode.isEmpty && languageCode != "emoji" { + var keywords = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3) + if !languageCode.lowercased().hasPrefix("en") { + keywords = keywords + |> mapToSignal { keywords in + return .single(keywords) + |> then( + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query.lowercased(), completeMatch: query.count < 3) + |> map { englishKeywords in + return keywords + englishKeywords + } + ) } } - - return (signals - |> mapToSignal { signals in - return combineLatest(signals) - }).start(next: { results in - var result: [(String?, FoundStickerItem)] = [] - var allAreFinal = true - for (emoji, stickers, isFinal) in results { - for sticker in stickers { - result.append((emoji, sticker)) - } - if !isFinal { - allAreFinal = false - } + stickers = .single(StickerPaneSearchStickerState(context: nil, items: [], isLoadingMore: true)) + |> then( + keywords + |> mapToSignal { keywords -> Signal in + let emoticon = keywords.flatMap { $0.emoticons }.map { $0.basicEmoji.0 } + let searchContext = context.engine.stickers.stickerSearchContext(query: query, emoticon: emoticon, inputLanguageCode: languageCode) + return searchContext.state + |> map { state -> StickerPaneSearchStickerState in + return StickerPaneSearchStickerState(context: searchContext, items: state.items, isLoadingMore: state.isLoadingMore) } - subscriber.putNext((result, allAreFinal)) - }, completed: { -// subscriber.putCompletion() }) + } else { + stickers = .single(StickerPaneSearchStickerState(context: nil, items: [], isLoadingMore: false)) } - - let local = context.engine.stickers.searchStickerSets(query: text) - let remote = context.engine.stickers.searchStickerSetsRemotely(query: text) + + let local = context.engine.stickers.searchStickerSets(query: query) + let remote = context.engine.stickers.searchStickerSetsRemotely(query: query) |> delay(0.2, queue: Queue.mainQueue()) let rawPacks = local |> mapToSignal { result -> Signal<(FoundStickerSets, Bool, FoundStickerSets?), NoError> in @@ -422,7 +596,7 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { } ) } - + let installedPackIds = context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])]) |> map { view -> Set in var installedPacks = Set() @@ -439,14 +613,14 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { let packs = combineLatest(rawPacks, installedPackIds) |> map { packs, installedPackIds -> (FoundStickerSets, Bool, FoundStickerSets?) in var (localPacks, completed, remotePacks) = packs - + for i in 0 ..< localPacks.infos.count { let installed = installedPackIds.contains(localPacks.infos[i].0) if installed != localPacks.infos[i].3 { localPacks.infos[i].3 = installed } } - + if remotePacks != nil { for i in 0 ..< remotePacks!.infos.count { let installed = installedPackIds.contains(remotePacks!.infos[i].0) @@ -455,121 +629,390 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { } } } - + return (localPacks, completed, remotePacks) } - + signal = combineLatest(stickers, packs) - |> map { stickers, packs -> ([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)? in - return (stickers.0, packs.0, packs.1 && stickers.1, packs.2) + |> map { stickers, packs -> (StickerPaneSearchStickerState, FoundStickerSets, Bool, FoundStickerSets?)? in + return (stickers, packs.0, packs.1 && !stickers.isLoadingMore, packs.2) } - self.updateActivity?(true) } else { signal = .single(nil) self.updateActivity?(false) } - + self.searchDisposable.set((signal |> deliverOn(self.queue)).start(next: { [weak self] result in Queue.mainQueue().async { guard let strongSelf = self, let interaction = strongSelf.searchInteraction else { return } - - var entries: [StickerSearchEntry] = [] + if let (stickers, packs, final, remote) = result { + strongSelf.stickerSearchContext = stickers.context + strongSelf.currentSearchStickerCount = stickers.items.count + strongSelf.currentStickerCount = stickers.items.count + strongSelf.updateActivity?(stickers.items.isEmpty && stickers.isLoadingMore) + if let remote = remote { let _ = strongSelf.currentRemotePacks.swap(remote) } strongSelf.gridNode.isHidden = false strongSelf.trendingPane.isHidden = true + + let previousPacks = strongSelf.currentPacks + let entries = strongSelf.entries(stickers: stickers.items) - if final { - strongSelf.updateActivity?(false) - } - - var index = 0 - var existingStickerIds = Set() - var previousCode: String? - for (code, sticker) in stickers { - if let id = sticker.file.id, !existingStickerIds.contains(id) { - entries.append(.sticker(index: index, code: code != previousCode ? code : nil, stickerItem: sticker, theme: strongSelf.theme)) - index += 1 - - previousCode = code - existingStickerIds.insert(id) - } - } - var isFirstGlobal = true - for (collectionId, info, _, installed) in packs.infos { - if let info = info as? StickerPackCollectionInfo { - var topItems: [StickerPackItem] = [] - for e in packs.entries { - if let item = e.item as? StickerPackItem { - if e.index.collectionId == collectionId { - topItems.append(item) - } - } + var packItems: [StickerPaneSearchPack] = strongSelf.packs(from: packs) + if !strongSelf.installedPackIds.isEmpty { + packItems = packItems.map { pack in + if strongSelf.installedPackIds.contains(pack.info.id) && !pack.installed { + return StickerPaneSearchPack(info: pack.info, topItems: pack.topItems, installed: true) + } else { + return pack } - entries.append(.global(index: index, info: info, topItems: topItems, installed: installed, topSeparator: !isFirstGlobal)) - isFirstGlobal = false - index += 1 } } - - if final || !entries.isEmpty { - strongSelf.notFoundNode.isHidden = !entries.isEmpty + + strongSelf.currentSearchEntries = entries + strongSelf.currentPacks = packItems + if let selectedPack = strongSelf.selectedPack, let updatedPack = packItems.first(where: { $0.info.id == selectedPack.info.id }), selectedPack.installed != updatedPack.installed { + strongSelf.setPackInstalledState(id: selectedPack.info.id, installed: updatedPack.installed, transition: .immediate) + } + strongSelf.currentSearchIsFinal = final + strongSelf.searchIsActive = true + + if strongSelf.selectedPack == nil { + strongSelf.enqueueEntries(entries, interaction: interaction) + strongSelf.updateNotFound() + } + if previousPacks != packItems { + strongSelf.requestLayout(transition: .immediate) } } else { let _ = strongSelf.currentRemotePacks.swap(nil) + strongSelf.stickerSearchContext = nil + strongSelf.currentSearchStickerCount = 0 + strongSelf.currentStickerCount = 0 + strongSelf.currentSearchEntries = [] + strongSelf.currentPacks = [] + strongSelf.currentSearchIsFinal = false + strongSelf.searchIsActive = false strongSelf.updateActivity?(false) strongSelf.gridNode.isHidden = true strongSelf.notFoundNode.isHidden = true strongSelf.trendingPane.isHidden = false + strongSelf.enqueueEntries([], interaction: interaction) + strongSelf.requestLayout(transition: .immediate) } - - let previousEntries = strongSelf.currentEntries.swap(entries) - let transition = preparedChatMediaInputGridEntryTransition(context: strongSelf.context, theme: strongSelf.theme, strings: strongSelf.strings, from: previousEntries ?? [], to: entries, interaction: interaction, inputNodeInteraction: strongSelf.inputNodeInteraction) - strongSelf.enqueueTransition(transition) } })) } + + private func entries(stickers: [FoundStickerItem]) -> [StickerSearchEntry] { + var entries: [StickerSearchEntry] = [] + var index = 0 + var existingStickerIds = Set() + for sticker in stickers { + if let id = sticker.file.id, !existingStickerIds.contains(id) { + entries.append(.sticker(index: index, code: nil, stickerItem: sticker, theme: self.theme)) + index += 1 + existingStickerIds.insert(id) + } + } + + return entries + } + + private func packs(from packs: FoundStickerSets) -> [StickerPaneSearchPack] { + var result: [StickerPaneSearchPack] = [] + var existingIds = Set() + for (collectionId, info, _, installed) in packs.infos { + guard !existingIds.contains(collectionId), let info = info as? StickerPackCollectionInfo else { + continue + } + existingIds.insert(collectionId) + + var topItems: [StickerPackItem] = [] + for entry in packs.entries { + if let item = entry.item as? StickerPackItem, entry.index.collectionId == collectionId { + topItems.append(item) + } + } + result.append(StickerPaneSearchPack(info: info, topItems: topItems, installed: installed)) + } + + return result + } + + private func entries(packItems: [StickerPackItem]) -> [StickerSearchEntry] { + var entries: [StickerSearchEntry] = [] + var existingStickerIds = Set() + var index = 0 + for item in packItems { + let file = item.file._parse() + if let id = file.id, !existingStickerIds.contains(id) { + entries.append(.sticker(index: index, code: nil, stickerItem: FoundStickerItem(file: file, stringRepresentations: item.getStringRepresentationsOfIndexKeys()), theme: self.theme)) + existingStickerIds.insert(id) + index += 1 + } + } + return entries + } + + private var shouldDisplayPackPanel: Bool { + return self.searchIsActive && !self.currentPacks.isEmpty + } + + private var currentPackPanelHeight: CGFloat { + guard self.shouldDisplayPackPanel else { + return 0.0 + } + return packPanelHeight + } + + private var currentVisiblePackPanelHeight: CGFloat { + guard self.shouldDisplayPackPanel else { + return 0.0 + } + return self.isPackPanelExpanded ? packPanelHeight : collapsedPackPanelHeight + } + + private var isInstallPackButtonVisible: Bool { + guard let selectedPack = self.selectedPack else { + return false + } + return !selectedPack.installed && !self.installedPackIds.contains(selectedPack.info.id) + } + func setPackInstalledState(id: ItemCollectionId, installed: Bool, transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)) { + if installed { + self.installedPackIds.insert(id) + } else { + self.installedPackIds.remove(id) + } + + var updatedSelectedPack: StickerPaneSearchPack? + if let selectedPack = self.selectedPack, selectedPack.info.id == id { + if selectedPack.installed != installed { + let pack = StickerPaneSearchPack(info: selectedPack.info, topItems: selectedPack.topItems, installed: installed) + self.selectedPack = pack + updatedSelectedPack = pack + } else { + updatedSelectedPack = selectedPack + } + } + + var updatedPacks = false + self.currentPacks = self.currentPacks.map { pack in + if pack.info.id == id && pack.installed != installed { + updatedPacks = true + return StickerPaneSearchPack(info: pack.info, topItems: pack.topItems, installed: installed) + } else { + return pack + } + } + + if let updatedSelectedPack { + self.selectedPackUpdated?(StickerPaneSearchSelectedPack(info: updatedSelectedPack.info, installed: updatedSelectedPack.installed)) + } + + if updatedSelectedPack != nil || updatedPacks { + self.requestLayout(transition: transition) + } + } + + private func updatePackPanelExpansionFromScroll() { + guard self.shouldDisplayPackPanel else { + return + } + + let contentOffsetY = self.gridNode.scrollView.contentOffset.y + let shouldExpand: Bool + if self.gridNode.scrollView.contentInset.top < 10.0 { + shouldExpand = true + } else { + shouldExpand = contentOffsetY <= -packPanelHeight + 20.0 + } + if self.isPackPanelExpanded != shouldExpand { + self.isPackPanelExpanded = shouldExpand + self.requestLayout(transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } + + private func resetGridScrollToTop() { + let scrollView = self.gridNode.scrollView + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + private func enqueueEntries(_ entries: [StickerSearchEntry], interaction: StickerPaneSearchInteraction, crossfade: Bool = false) { + let previousEntries = self.currentEntries.swap(entries) + let transition = preparedChatMediaInputGridEntryTransition(context: self.context, theme: self.theme, strings: self.strings, from: previousEntries ?? [], to: entries, interaction: interaction, inputNodeInteraction: self.inputNodeInteraction, crossfade: crossfade) + self.enqueueTransition(transition) + } + + private func updateNotFound() { + if self.selectedPack != nil || !self.searchIsActive { + self.notFoundNode.isHidden = true + } else if self.currentSearchIsFinal || !self.currentSearchEntries.isEmpty || !self.currentPacks.isEmpty { + self.notFoundNode.isHidden = !(self.currentSearchEntries.isEmpty && self.currentPacks.isEmpty) + } else { + self.notFoundNode.isHidden = true + } + } + + private func selectPack(_ pack: StickerPaneSearchPack) { + guard let interaction = self.searchInteraction else { + return + } + + self.view.window?.endEditing(true) + self.deactivateSearchBar?() + + self.selectedPackDisposable.set(nil) + self.selectedPack = pack + self.currentStickerCount = 0 + self.notFoundNode.isHidden = true + self.gridNode.isHidden = false + self.trendingPane.isHidden = true + self.selectedPackUpdated?(StickerPaneSearchSelectedPack(info: pack.info, installed: pack.installed)) + + self.enqueueEntries(self.entries(packItems: pack.topItems), interaction: interaction, crossfade: true) + self.isPackPanelExpanded = true + self.requestLayout(transition: .animated(duration: 0.2, curve: .easeInOut)) + self.resetGridScrollToTop() + + let packId = pack.info.id + self.selectedPackDisposable.set((self.context.engine.stickers.loadedStickerPack(reference: .id(id: pack.info.id.id, accessHash: pack.info.accessHash), forceActualized: false) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self, let interaction = self.searchInteraction, self.selectedPack?.info.id == packId else { + return + } + switch result { + case let .result(_, items, _): + self.enqueueEntries(self.entries(packItems: items), interaction: interaction) + case .fetching, .none: + break + } + })) + } + + private func installSelectedStickerPack() { + guard let selectedPack = self.selectedPack, !selectedPack.installed, !self.installedPackIds.contains(selectedPack.info.id) else { + return + } + + let context = self.context + let packId = selectedPack.info.id + let accessHash = selectedPack.info.accessHash + + self.setPackInstalledState(id: packId, installed: true) + + let installSignal = (context.engine.stickers.loadedStickerPack(reference: .id(id: packId.id, accessHash: accessHash), forceActualized: false) + |> mapToSignal { result -> Signal<(StickerPackCollectionInfo, [StickerPackItem]), NoError> in + switch result { + case let .result(info, items, installed): + let info = info._parse() + if installed { + return .single((info, items)) + } else { + return preloadedStickerPackThumbnail(account: context.account, info: StickerPackCollectionInfo.Accessor(info), items: items) + |> filter { $0 } + |> ignoreValues + |> then( + context.engine.stickers.addStickerPackInteractively(info: info, items: items) + |> ignoreValues + ) + |> mapToSignal { _ -> Signal<(StickerPackCollectionInfo, [StickerPackItem]), NoError> in + } + |> then(.single((info, items))) + } + case .fetching: + break + case .none: + break + } + return .complete() + } + |> deliverOnMainQueue) + + self.installDisposable.set(installSignal.start(next: { [weak self] info, items in + guard let self else { + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: self.theme) + self.interaction.getNavigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: self.context), elevatedLayout: false, action: { _ in + return true + })) + })) + } + + func clearSelectedPack(applySearchResults: Bool = true) { + guard self.selectedPack != nil else { + return + } + self.selectedPack = nil + self.selectedPackDisposable.set(nil) + + self.selectedPackUpdated?(nil) + + if applySearchResults, let interaction = self.searchInteraction { + self.currentStickerCount = self.currentSearchStickerCount + self.gridNode.isHidden = !self.searchIsActive + self.trendingPane.isHidden = self.searchIsActive + self.enqueueEntries(self.currentSearchEntries, interaction: interaction, crossfade: true) + self.updateNotFound() + self.requestLayout(transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.theme = theme + self.strings = strings self.notFoundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/StickersNotFoundIcon"), color: theme.list.freeMonoIconColor) self.notFoundLabel.attributedText = NSAttributedString(string: strings.Stickers_NoStickersFound, font: Font.medium(14.0), textColor: theme.list.freeTextColor) } - + private func enqueueTransition(_ transition: StickerPaneSearchGridTransition) { self.enqueuedTransitions.append(transition) - + if self.validLayout != nil { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } } } - + private func dequeueTransition() { if let transition = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) + + if transition.crossfade, let snapshotView = self.gridNode.scrollView.snapshotContentTree() { + snapshotView.frame = self.gridNode.frame + self.gridNode.view.superview?.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + + self.gridNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } let itemTransition: ContainedViewLayoutTransition = .immediate self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in }) } } - + func updatePreviewing(animated: Bool) { self.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? StickerPaneSearchStickerItemNode { itemNode.updatePreviewing(animated: animated) - } else if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode { - itemNode.updatePreviewing(animated: animated) } } self.trendingPane.updatePreviewing(animated: animated) } - + func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? { if !self.trendingPane.isHidden { if let (itemNode, item) = self.trendingPane.itemAt(point: self.view.convert(point, to: self.trendingPane.view)) { @@ -579,36 +1022,165 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { if let itemNode = self.gridNode.itemNodeAtPoint(self.view.convert(point, to: self.gridNode.view)) { if let itemNode = itemNode as? StickerPaneSearchStickerItemNode, let stickerItem = itemNode.stickerItem { return (itemNode, StickerPreviewPeekItem.found(stickerItem)) - } else if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode { - if let (node, item) = itemNode.itemAt(point: self.view.convert(point, to: itemNode.view)) { - return (node, StickerPreviewPeekItem.pack(item.file._parse())) - } } } } return nil } + + private func updatePackButtonsLayout(size: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) { + if self.shouldDisplayPackPanel { + let componentTransition = ComponentTransition(transition) + let panelSize = self.packPanel.update( + transition: componentTransition, + component: AnyComponent(EntityKeyboardTopPanelComponent( + id: AnyHashable("stickerSearchPacks"), + theme: self.theme, + customTintColor: nil, + items: self.currentPacks.map { pack in + return EntityKeyboardTopPanelComponent.Item( + id: AnyHashable(pack.info.id), + isReorderable: false, + content: AnyComponent(StickerSearchPackTopPanelItemComponent( + context: self.context, + theme: self.theme, + info: pack.info, + topItem: pack.topItems.first, + pressed: { [weak self] in + self?.selectPack(pack) + } + )) + ) + }, + containerSideInset: 0.0, + forceActiveItemId: self.selectedPack.flatMap { AnyHashable($0.info.id) }, + displayHighlightInExpanded: true, + automaticallySelectsFirstItem: false, + itemSpacing: 14.0, + activeContentItemIdUpdated: self.packPanelActiveItemUpdated, + reorderItems: { _ in } + )), + environment: { + EntityKeyboardTopContainerPanelEnvironment( + isContentInFocus: true, + height: collapsedPackPanelHeight, + visibilityFractionUpdated: self.packPanelVisibilityFractionUpdated, + isExpandedUpdated: { _, _ in } + ) + }, + containerSize: CGSize(width: size.width, height: self.currentVisiblePackPanelHeight) + ) + + if let view = self.packPanel.view { + if view.superview == nil { + self.view.addSubview(view) + } + componentTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(), size: panelSize)) + } + } else if let view = self.packPanel.view { + view.removeFromSuperview() + } + + let isVisible = self.isInstallPackButtonVisible + + let componentTransition = ComponentTransition(transition) + let edgeEffectHeight: CGFloat = isVisible ? 88.0 + bottomInset : 0.0 + let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - edgeEffectHeight), size: CGSize(width: size.width, height: edgeEffectHeight)) + transition.updateFrame(view: self.bottomEdgeEffectView, frame: edgeEffectFrame) + self.bottomEdgeEffectView.update( + content: self.theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0), + blur: true, + alpha: 1.0, + rect: edgeEffectFrame, + edge: .bottom, + edgeSize: min(edgeEffectFrame.height, 80.0), + transition: componentTransition + ) + transition.updateAlpha(layer: self.bottomEdgeEffectView.layer, alpha: isVisible ? 1.0 : 0.0) + + if isVisible, let selectedPack = self.selectedPack { + let buttonTitle = self.strings.StickerPack_AddStickerCount(selectedPack.info.count) + let buttonForegroundColor = self.theme.list.itemCheckColors.foregroundColor + let buttonBackgroundColor = self.theme.list.itemCheckColors.fillColor + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: bottomInset, innerDiameter: 52.0, sideInset: 30.0) + let buttonSize = self.selectedPackAddButton.update( + transition: componentTransition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .actualGlass, + color: buttonBackgroundColor, + foreground: buttonForegroundColor, + pressedColor: buttonBackgroundColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(buttonTitle), + component: AnyComponent(Text(text: buttonTitle, font: Font.semibold(17.0), color: buttonForegroundColor)) + ), + action: { [weak self] in + self?.installSelectedStickerPack() + } + )), + environment: {}, + containerSize: CGSize(width: max(0.0, size.width - buttonInsets.left - buttonInsets.right), height: 52.0) + ) + + if let buttonView = self.selectedPackAddButton.view { + if buttonView.superview == nil { + self.view.addSubview(buttonView) + } + buttonView.isUserInteractionEnabled = true + buttonView.frame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: size.height - bottomInset - buttonInsets.bottom - buttonSize.height), size: buttonSize) + componentTransition.setAlpha(view: buttonView, alpha: 1.0) + } + } else if let buttonView = self.selectedPackAddButton.view { + buttonView.isUserInteractionEnabled = false + componentTransition.setAlpha(view: buttonView, alpha: 0.0) + } + } + func requestLayout(transition: ContainedViewLayoutTransition) { + guard let (size, leftInset, rightInset, bottomInset, inputHeight, deviceMetrics) = self.validLayout else { + return + } + self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: transition) + } + func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { let firstLayout = self.validLayout == nil - self.validLayout = size - + self.validLayout = (size, leftInset, rightInset, bottomInset, inputHeight, deviceMetrics) + + let edgeEffectHeight: CGFloat = 80.0 + let edgeEffectFrame = CGRect(origin: .zero, size: CGSize(width: size.width, height: edgeEffectHeight)) + transition.updateFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame) + self.topEdgeEffectView.update( + content: self.theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0), + blur: true, + alpha: 1.0, + rect: edgeEffectFrame, + edge: .top, + edgeSize: edgeEffectFrame.height, + transition: ComponentTransition(transition) + ) + transition.updateAlpha(layer: self.topEdgeEffectView.layer, alpha: self.shouldDisplayPackPanel ? 1.0 : 0.0) + self.updatePackButtonsLayout(size: size, bottomInset: bottomInset, transition: transition) + if let image = self.notFoundNode.image { - let areaHeight = size.height - inputHeight - + let areaHeight = max(0.0, size.height - inputHeight) + let labelSize = self.notFoundLabel.updateLayout(CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude)) - + transition.updateFrame(node: self.notFoundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((areaHeight - image.size.height - labelSize.height) / 2.0)), size: image.size)) transition.updateFrame(node: self.notFoundLabel, frame: CGRect(origin: CGPoint(x: floor((image.size.width - labelSize.width) / 2.0), y: image.size.height + 8.0), size: labelSize)) } - + let contentFrame = CGRect(origin: CGPoint(), size: size) - self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: contentFrame.size, insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0 + bottomInset, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + let gridTopInset: CGFloat = 4.0 + self.currentPackPanelHeight + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: contentFrame.size, insets: UIEdgeInsets(top: gridTopInset, left: 0.0, bottom: 4.0 + bottomInset + 64.0, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) transition.updateFrame(node: self.trendingPane, frame: contentFrame) self.trendingPane.updateLayout(size: contentFrame.size, topInset: 0.0, bottomInset: bottomInset, isExpanded: false, isVisible: true, deviceMetrics: deviceMetrics, transition: transition) - + transition.updateFrame(node: self.gridNode, frame: contentFrame) if firstLayout { while !self.enqueuedTransitions.isEmpty { @@ -616,23 +1188,43 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { } } } - + func animateIn(additivePosition: CGFloat, transition: ContainedViewLayoutTransition) { self.gridNode.alpha = 0.0 transition.updateAlpha(node: self.gridNode, alpha: 1.0, completion: { _ in }) + if let view = self.packPanel.view { + view.alpha = 0.0 + ComponentTransition(transition).setAlpha(view: view, alpha: 1.0) + } + self.topEdgeEffectView.alpha = 0.0 + ComponentTransition(transition).setAlpha(view: self.topEdgeEffectView, alpha: self.shouldDisplayPackPanel ? 1.0 : 0.0) + self.bottomEdgeEffectView.alpha = 0.0 + ComponentTransition(transition).setAlpha(view: self.bottomEdgeEffectView, alpha: self.isInstallPackButtonVisible ? 1.0 : 0.0) + if let buttonView = self.selectedPackAddButton.view { + buttonView.alpha = 0.0 + ComponentTransition(transition).setAlpha(view: buttonView, alpha: self.isInstallPackButtonVisible ? 1.0 : 0.0) + } self.trendingPane.alpha = 0.0 transition.updateAlpha(node: self.trendingPane, alpha: 1.0, completion: { _ in }) - + if case let .animated(duration, curve) = transition { self.trendingPane.layer.animatePosition(from: CGPoint(x: 0.0, y: additivePosition), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, additive: true) } } - + func animateOut(transition: ContainedViewLayoutTransition) { transition.updateAlpha(node: self.gridNode, alpha: 0.0, completion: { _ in }) + if let view = self.packPanel.view { + ComponentTransition(transition).setAlpha(view: view, alpha: 0.0) + } + ComponentTransition(transition).setAlpha(view: self.topEdgeEffectView, alpha: 0.0) + ComponentTransition(transition).setAlpha(view: self.bottomEdgeEffectView, alpha: 0.0) + if let buttonView = self.selectedPackAddButton.view { + ComponentTransition(transition).setAlpha(view: buttonView, alpha: 0.0) + } transition.updateAlpha(node: self.trendingPane, alpha: 0.0, completion: { _ in }) transition.updateAlpha(node: self.notFoundNode, alpha: 0.0, completion: { _ in diff --git a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD index 664a0e0e73..565d960620 100644 --- a/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD +++ b/submodules/TelegramUI/Components/ChatFolderLinkPreviewScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent/Sources/ChatListNoticeItem.swift b/submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent/Sources/ChatListNoticeItem.swift index 7c7cb283e4..717b9b56a1 100644 --- a/submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent/Sources/ChatListNoticeItem.swift +++ b/submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent/Sources/ChatListNoticeItem.swift @@ -358,7 +358,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { strongSelf.avatarsNode = avatarsNode } let avatarSize = CGSize(width: 30.0, height: 30.0) - avatarsNode.update(context: item.context, peers: avatarPeers.map { $0._asPeer() }, synchronousLoad: false, imageSize: avatarSize.width, imageSpacing: 16.0, borderWidth: 2.0 - UIScreenPixel, avatarFontSize: 10.0) + avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: false, imageSize: avatarSize.width, imageSpacing: 16.0, borderWidth: 2.0 - UIScreenPixel, avatarFontSize: 10.0) let avatarsSize = CGSize(width: avatarSize.width + 16.0 * CGFloat(avatarPeers.count - 1), height: avatarSize.height) avatarsNode.updateLayout(size: avatarsSize) avatarsNode.frame = CGRect(origin: CGPoint(x: sideInset - 6.0, y: floor((layout.size.height - avatarsSize.height) / 2.0)), size: avatarsSize) diff --git a/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/BUILD b/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/BUILD index 74b602e464..bffe09ad0b 100644 --- a/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/BUILD +++ b/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/TelegramCore", - "//submodules/Postbox", "//submodules/TelegramPresentationData", "//submodules/AccountContext", "//submodules/TelegramUI/Components/GlassBackgroundComponent", diff --git a/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/Sources/ChatListSearchFiltersContainerNode.swift b/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/Sources/ChatListSearchFiltersContainerNode.swift index e9839e633d..4048b8da84 100644 --- a/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/Sources/ChatListSearchFiltersContainerNode.swift +++ b/submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode/Sources/ChatListSearchFiltersContainerNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import AccountContext diff --git a/submodules/TelegramUI/Components/ChatParticipantRightsScreen/BUILD b/submodules/TelegramUI/Components/ChatParticipantRightsScreen/BUILD index 4cc0f3a66f..981a7dd775 100644 --- a/submodules/TelegramUI/Components/ChatParticipantRightsScreen/BUILD +++ b/submodules/TelegramUI/Components/ChatParticipantRightsScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/ChatParticipantRightsScreen/Sources/ChatParticipantRightsScreen.swift b/submodules/TelegramUI/Components/ChatParticipantRightsScreen/Sources/ChatParticipantRightsScreen.swift index 6bbc4ad256..4e3a5f6995 100644 --- a/submodules/TelegramUI/Components/ChatParticipantRightsScreen/Sources/ChatParticipantRightsScreen.swift +++ b/submodules/TelegramUI/Components/ChatParticipantRightsScreen/Sources/ChatParticipantRightsScreen.swift @@ -260,7 +260,7 @@ private final class ChatParticipantRightsContent: CombinedComponent { state.updated(transition: .easeInOut(duration: 0.2)) }, shouldUpdateText: { [weak state] text in - if text.containsEmoji { + if text.containsGraphicEmoji { state?.animateError() return false } diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/BUILD b/submodules/TelegramUI/Components/ChatScheduleTimeController/BUILD index 2c878a8ba4..90488ae6ce 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/BUILD +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/BUILD @@ -14,7 +14,6 @@ swift_library( "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/TelegramCore", - "//submodules/Postbox", "//submodules/AccountContext", "//submodules/TelegramPresentationData", "//submodules/TelegramStringFormatting", @@ -26,6 +25,7 @@ swift_library( "//submodules/TelegramUI/Components/ToastComponent", "//submodules/Markdown", "//submodules/UndoUI", + "//submodules/TooltipUI", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/Components/MultilineTextComponent", "//submodules/Components/BundleIconComponent", @@ -37,6 +37,7 @@ swift_library( "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/Components/LottieAnimationComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeController.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeController.swift index e64e7b12b8..d3e5a6f3c7 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeController.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import AccountContext diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift index 30cfdd3ad1..e4be5f979c 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeControllerNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramStringFormatting diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift index c49ef78263..58ec1e213a 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift @@ -13,10 +13,12 @@ import SheetComponent import ButtonComponent import PlainButtonComponent import BundleIconComponent +import LottieAnimationComponent import GlassBackgroundComponent import GlassBarButtonComponent import DatePickerNode import UndoUI +import TooltipUI private let calendar = Calendar(identifier: .gregorian) @@ -37,6 +39,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { let currentRepeatPeriod: Int32? let suggestedTime: Int32? let minimalTime: Int32? + let silentPosting: Bool let externalState: ExternalState let dismiss: () -> Void @@ -47,6 +50,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { currentRepeatPeriod: Int32?, suggestedTime: Int32?, minimalTime: Int32?, + silentPosting: Bool, externalState: ExternalState, dismiss: @escaping () -> Void ) { @@ -56,6 +60,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { self.currentRepeatPeriod = currentRepeatPeriod self.suggestedTime = suggestedTime self.minimalTime = minimalTime + self.silentPosting = silentPosting self.externalState = externalState self.dismiss = dismiss } @@ -66,6 +71,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component { final class View: UIView { private let cancel = ComponentView() + private let silent = ComponentView() + private let title = ComponentView() private let button = ComponentView() private let secondaryButton = ComponentView() @@ -93,13 +100,15 @@ private final class ChatScheduleTimeSheetContentComponent: Component { private var monthHeight: CGFloat? + private var isSilentPosting = false + private var date: Date? private var minDate: Date? private var maxDate: Date? private var isPickingTime = false private var isPickingRepeatPeriod = false - + private var repeatPeriod: Int32? private let dateFormatter: DateFormatter @@ -144,6 +153,70 @@ private final class ChatScheduleTimeSheetContentComponent: Component { } } + private func presentSilentPostingTooltip() { + guard let component = self.component, let sourceView = self.silent.view else { + return + } + + let peerId: EnginePeer.Id? + switch component.mode { + case let .scheduledMessages(peerIdValue, _): + peerId = peerIdValue + case .reminders: + peerId = component.context.account.peerId + default: + peerId = nil + } + guard let peerId else { + return + } + + let isSilentPosting = self.isSilentPosting + let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer, let environment = self.environment, let controller = self.environment?.controller() else { + return + } + var isChannel = false + if case let .channel(channel) = peer, case .broadcast = channel.info { + isChannel = true + } + let text: String + if case let .user(user) = peer { + if user.id == component.context.account.peerId { + if isSilentPosting { + text = environment.strings.ScheduleMessage_SilentPosting_YouEnabled + } else { + text = environment.strings.ScheduleMessage_SilentPosting_YouDisabled + } + } else { + if isSilentPosting { + text = environment.strings.ScheduleMessage_SilentPosting_UserEnabled(peer.compactDisplayTitle).string + } else { + text = environment.strings.ScheduleMessage_SilentPosting_UserDisabled(peer.compactDisplayTitle).string + } + } + } else if isChannel { + if isSilentPosting { + text = environment.strings.ScheduleMessage_SilentPosting_ChannelEnabled + } else { + text = environment.strings.ScheduleMessage_SilentPosting_ChannelDisabled + } + } else { + if isSilentPosting { + text = environment.strings.ScheduleMessage_SilentPosting_GroupEnabled + } else { + text = environment.strings.ScheduleMessage_SilentPosting_GroupDisabled + } + } + + let sourceFrame = sourceView.convert(sourceView.bounds, to: nil) + controller.present(TooltipScreen(account: component.context.account, sharedContext: component.context.sharedContext, text: .plain(text: text), style: .default, icon: .none, location: .point(sourceFrame, .bottom), shouldDismissOnTouch: { _, _ in + return .dismiss(consume: false) + }), in: .window(.root)) + }) + } + func update(component: ChatScheduleTimeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -157,7 +230,9 @@ private final class ChatScheduleTimeSheetContentComponent: Component { self.environment = environment if self.component == nil { - if case .format = component.mode { + self.isSilentPosting = component.silentPosting + 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 +242,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { } else { self.date = Date() } - } else { + default: self.updateMinimumDate(currentTime: component.currentTime, minimalTime: component.minimalTime) } self.repeatPeriod = component.currentRepeatPeriod @@ -183,40 +258,6 @@ private final class ChatScheduleTimeSheetContentComponent: Component { var contentHeight: CGFloat = 0.0 contentHeight += 30.0 - let barButtonSize = CGSize(width: 44.0, height: 44.0) - let cancelSize = self.cancel.update( - transition: transition, - component: AnyComponent( - GlassBarButtonComponent( - size: barButtonSize, - backgroundColor: nil, - isDark: environment.theme.overallDarkAppearance, - state: .glass, - component: AnyComponentWithIdentity(id: "close", component: AnyComponent( - BundleIconComponent( - name: "Navigation/Close", - tintColor: environment.theme.chat.inputPanel.panelControlColor - ) - )), - action: { [weak self] _ in - guard let self, let component = self.component else { - return - } - component.dismiss() - } - ) - ), - environment: {}, - containerSize: barButtonSize - ) - let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize) - if let cancelView = self.cancel.view { - if cancelView.superview == nil { - self.addSubview(cancelView) - } - transition.setFrame(view: cancelView, frame: cancelFrame) - } - let title: String switch component.mode { case .scheduledMessages: @@ -227,6 +268,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component { title = strings.Conversation_FormatDate_Title case .poll: title = strings.CreatePoll_Deadline_Title + case .search: + title = strings.Conversation_CalendarSearch_Title } let titleSize = self.title.update( transition: transition, @@ -314,78 +357,87 @@ private final class ChatScheduleTimeSheetContentComponent: Component { contentHeight += pickerHeight } - transition.setFrame(layer: self.topSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel))) - self.topSeparator.backgroundColor = environment.theme.list.itemBlocksSeparatorColor.cgColor - if self.topSeparator.superlayer == nil { - self.layer.addSublayer(self.topSeparator) - } - - let timeTitleSize = self.timeTitle.update( - transition: transition, - component: AnyComponent( - Text(text: strings.ScheduleMessage_Time, font: Font.regular(17.0), color: environment.theme.actionSheet.primaryTextColor) - ), - environment: {}, - containerSize: availableSize - ) - let timeTitleFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 16.0), size: timeTitleSize) - if let timeTitleView = self.timeTitle.view { - if timeTitleView.superview == nil { - self.addSubview(timeTitleView) - } - transition.setFrame(view: timeTitleView, frame: timeTitleFrame) - } - let date = self.date ?? Date() + var timeValueFrame: CGRect? - var t: time_t = Int(date.timeIntervalSince1970) - var timeinfo = tm() - localtime_r(&t, &timeinfo); - - let timeString = stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: environment.dateTimeFormat) - let timeValueSize = self.timeValue.update( - transition: transition, - component: AnyComponent( - PlainButtonComponent( - content: AnyComponent( - ButtonContentComponent( - theme: environment.theme, - text: timeString, - isActive: self.isPickingTime, - isLocked: false - ) - ), - action: { [weak self] in - guard let self else { - return - } - if self.isPickingRepeatPeriod { - self.isPickingRepeatPeriod = false - } else { - self.isPickingTime = !self.isPickingTime - } - self.state?.updated() - }, - animateScale: false - ) - ), - environment: { - }, - containerSize: availableSize - ) - let timeValueFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - timeValueSize.width, y: contentHeight + 10.0), size: timeValueSize) - if let timeValueView = self.timeValue.view { - if timeValueView.superview == nil { - self.addSubview(timeValueView) + switch component.mode { + case .search: + break + default: + transition.setFrame(layer: self.topSeparator, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: UIScreenPixel))) + self.topSeparator.backgroundColor = environment.theme.list.itemBlocksSeparatorColor.cgColor + if self.topSeparator.superlayer == nil { + self.layer.addSublayer(self.topSeparator) } - transition.setFrame(view: timeValueView, frame: timeValueFrame) + + let timeTitleSize = self.timeTitle.update( + transition: transition, + component: AnyComponent( + Text(text: strings.ScheduleMessage_Time, font: Font.regular(17.0), color: environment.theme.actionSheet.primaryTextColor) + ), + environment: {}, + containerSize: availableSize + ) + let timeTitleFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 16.0), size: timeTitleSize) + if let timeTitleView = self.timeTitle.view { + if timeTitleView.superview == nil { + self.addSubview(timeTitleView) + } + transition.setFrame(view: timeTitleView, frame: timeTitleFrame) + } + + var t: time_t = Int(date.timeIntervalSince1970) + var timeinfo = tm() + localtime_r(&t, &timeinfo); + + let timeString = stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: environment.dateTimeFormat) + let timeValueSize = self.timeValue.update( + transition: transition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + ButtonContentComponent( + theme: environment.theme, + text: timeString, + isActive: self.isPickingTime, + isLocked: false + ) + ), + action: { [weak self] in + guard let self else { + return + } + if self.isPickingRepeatPeriod { + self.isPickingRepeatPeriod = false + } else { + self.isPickingTime = !self.isPickingTime + } + self.state?.updated() + }, + animateScale: false + ) + ), + environment: { + }, + containerSize: availableSize + ) + let timeValueFrameValue = CGRect(origin: CGPoint(x: availableSize.width - sideInset - timeValueSize.width, y: contentHeight + 10.0), size: timeValueSize) + timeValueFrame = timeValueFrameValue + if let timeValueView = self.timeValue.view { + if timeValueView.superview == nil { + self.addSubview(timeValueView) + } + transition.setFrame(view: timeValueView, frame: timeValueFrameValue) + } + + contentHeight += 56.0 } - contentHeight += 56.0 - 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 +549,8 @@ 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: + buttonTitle = strings.Conversation_CalendarSearch_Done } let buttonSideInset: CGFloat = 30.0 @@ -521,7 +575,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component { controller.completion( ChatScheduleTimeScreen.Result( time: Int32(self.date?.timeIntervalSince1970 ?? 0), - repeatPeriod: self.repeatPeriod + repeatPeriod: self.repeatPeriod, + silentPosting: self.isSilentPosting ) ) component.dismiss() @@ -539,7 +594,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( @@ -547,12 +608,12 @@ private final class ChatScheduleTimeSheetContentComponent: Component { component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( style: .glass, - color: environment.theme.list.itemDestructiveColor.withMultipliedAlpha(0.1), - foreground: environment.theme.list.itemDestructiveColor, - pressedColor: environment.theme.list.itemDestructiveColor.withMultipliedAlpha(0.8), + color: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + foreground: environment.theme.list.itemAccentColor, + pressedColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.8), ), content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( - Text(text: strings.Conversation_FormatDate_RemoveDate, font: Font.semibold(17.0), color: environment.theme.list.itemDestructiveColor) + Text(text: strings.Conversation_FormatDate_RemoveDate, font: Font.semibold(17.0), color: environment.theme.list.itemAccentColor) )), isEnabled: true, displaysProgress: false, @@ -563,7 +624,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component { controller.completion( ChatScheduleTimeScreen.Result( time: 0, - repeatPeriod: nil + repeatPeriod: nil, + silentPosting: false ) ) component.dismiss() @@ -580,7 +642,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { transition.setFrame(view: buttonView, frame: buttonFrame) } contentHeight += buttonSize.height - } else if case .scheduledMessages(true) = component.mode { + } else if case .scheduledMessages(_, true) = component.mode { contentHeight += 8.0 let buttonSize = self.secondaryButton.update( @@ -604,7 +666,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component { controller.completion( ChatScheduleTimeScreen.Result( time: scheduleWhenOnlineTimestamp, - repeatPeriod: nil + repeatPeriod: nil, + silentPosting: self.isSilentPosting ) ) component.dismiss() @@ -629,7 +692,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { let contentSize = CGSize(width: availableSize.width, height: contentHeight) - if self.isPickingTime { + if self.isPickingTime, let timeValueFrame { let _ = self.timePicker.update( transition: transition, component: AnyComponent( @@ -769,6 +832,87 @@ private final class ChatScheduleTimeSheetContentComponent: Component { component.externalState.repeatValueFrame = repeatValueFrame + let barButtonSize = CGSize(width: 44.0, height: 44.0) + let cancelSize = self.cancel.update( + transition: transition, + component: AnyComponent( + GlassBarButtonComponent( + size: barButtonSize, + backgroundColor: nil, + isDark: environment.theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: environment.theme.chat.inputPanel.panelControlColor + ) + )), + action: { [weak self] _ in + guard let self, let component = self.component else { + return + } + component.dismiss() + } + ) + ), + environment: {}, + containerSize: barButtonSize + ) + let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize) + if let cancelView = self.cancel.view { + if cancelView.superview == nil { + self.addSubview(cancelView) + } + transition.setFrame(view: cancelView, frame: cancelFrame) + } + + switch component.mode { + case .scheduledMessages, .reminders: + let silentSize = self.silent.update( + transition: transition, + component: AnyComponent( + GlassBarButtonComponent( + size: barButtonSize, + backgroundColor: nil, + isDark: environment.theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "silent", component: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: self.isSilentPosting ? "NavigationMuteOn" : "NavigationMuteOff", + mode: !transition.animation.isImmediate ? .animating(loop: false) : .still(position: .end), + range: nil, + waitForCompletion: false + ), + colors: ["__allcolors__": environment.theme.chat.inputPanel.panelControlColor], + size: CGSize(width: 30.0, height: 30.0) + ) + )), + action: { [weak self] _ in + guard let self else { + return + } + self.isSilentPosting = !self.isSilentPosting + self.state?.updated(transition: .easeInOut(duration: 0.2)) + + self.presentSilentPostingTooltip() + } + ) + ), + environment: {}, + containerSize: barButtonSize + ) + let silentFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - silentSize.width, y: 16.0), size: silentSize) + if let silentView = self.silent.view { + if silentView.superview == nil { + self.addSubview(silentView) + } + transition.setFrame(view: silentView, frame: silentFrame) + } + default: + break + } + return contentSize } } @@ -791,6 +935,7 @@ private final class ChatScheduleTimeScreenComponent: Component { let currentRepeatPeriod: Int32? let suggestedTime: Int32? let minimalTime: Int32? + let silentPosting: Bool init( context: AccountContext, @@ -798,7 +943,8 @@ private final class ChatScheduleTimeScreenComponent: Component { currentTime: Int32?, currentRepeatPeriod: Int32?, suggestedTime: Int32?, - minimalTime: Int32? + minimalTime: Int32?, + silentPosting: Bool ) { self.context = context self.mode = mode @@ -806,6 +952,7 @@ private final class ChatScheduleTimeScreenComponent: Component { self.currentRepeatPeriod = currentRepeatPeriod self.suggestedTime = suggestedTime self.minimalTime = minimalTime + self.silentPosting = silentPosting } static func ==(lhs: ChatScheduleTimeScreenComponent, rhs: ChatScheduleTimeScreenComponent) -> Bool { @@ -824,6 +971,9 @@ private final class ChatScheduleTimeScreenComponent: Component { if lhs.minimalTime != rhs.minimalTime { return false } + if lhs.silentPosting != rhs.silentPosting { + return false + } return true } @@ -878,6 +1028,7 @@ private final class ChatScheduleTimeScreenComponent: Component { currentRepeatPeriod: component.currentRepeatPeriod, suggestedTime: component.suggestedTime, minimalTime: component.minimalTime, + silentPosting: component.silentPosting, externalState: self.contentExternalState, dismiss: { [weak self] in guard let self else { @@ -943,15 +1094,23 @@ private final class ChatScheduleTimeScreenComponent: Component { public class ChatScheduleTimeScreen: ViewControllerComponentContainer { public enum Mode: Equatable { - case scheduledMessages(sendWhenOnlineAvailable: Bool) + case scheduledMessages(peerId: EnginePeer.Id, sendWhenOnlineAvailable: Bool) case reminders case format case poll + case search } public struct Result { public let time: Int32 public let repeatPeriod: Int32? + public let silentPosting: Bool + + public init(time: Int32, repeatPeriod: Int32?, silentPosting: Bool = false) { + self.time = time + self.repeatPeriod = repeatPeriod + self.silentPosting = silentPosting + } } fileprivate let completion: (Result) -> Void @@ -963,6 +1122,7 @@ public class ChatScheduleTimeScreen: ViewControllerComponentContainer { currentRepeatPeriod: Int32? = nil, suggestedTime: Int32? = nil, minimalTime: Int32? = nil, + silentPosting: Bool = false, isDark: Bool, completion: @escaping (Result) -> Void ) { @@ -974,7 +1134,8 @@ public class ChatScheduleTimeScreen: ViewControllerComponentContainer { currentTime: currentTime, currentRepeatPeriod: currentRepeatPeriod, suggestedTime: suggestedTime, - minimalTime: minimalTime + minimalTime: minimalTime, + silentPosting: silentPosting ), navigationBarAppearance: .none, theme: isDark ? .dark : .default) self.statusBar.statusBarStyle = .Ignore diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeScreen/BUILD b/submodules/TelegramUI/Components/ChatScheduleTimeScreen/BUILD index 1f30dd2987..23187abd0d 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeScreen/BUILD +++ b/submodules/TelegramUI/Components/ChatScheduleTimeScreen/BUILD @@ -14,7 +14,6 @@ swift_library( "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/TelegramCore", - "//submodules/Postbox", "//submodules/AccountContext", "//submodules/TelegramPresentationData", "//submodules/TelegramStringFormatting", diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeScreen/Sources/ChatScheduleTimeScreen.swift b/submodules/TelegramUI/Components/ChatScheduleTimeScreen/Sources/ChatScheduleTimeScreen.swift index e64e7b12b8..d3e5a6f3c7 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeScreen/Sources/ChatScheduleTimeScreen.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeScreen/Sources/ChatScheduleTimeScreen.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import AccountContext diff --git a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift index d33e7e4012..1eab70ac52 100644 --- a/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Components/ChatThemeScreen/Sources/ChatThemeScreen.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import AccountContext @@ -492,7 +491,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { } strongSelf.animatedStickerNode = animatedStickerNode strongSelf.emojiContainerNode.insertSubnode(animatedStickerNode, belowSubnode: strongSelf.placeholderNode) - let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let pathPrefix = item.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.resource.id)) animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix)) animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) @@ -500,7 +499,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode { animatedStickerNode.autoplay = true animatedStickerNode.visibility = strongSelf.visibilityStatus - strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).startStrict()) + strongSelf.stickerFetchedDisposable.set(item.context.engine.resources.fetch(reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource), userLocation: .other, userContentType: .sticker).startStrict()) let thumbnailDimensions = PixelDimensions(width: 512, height: 512) strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: thumbnailDimensions.cgSize) diff --git a/submodules/TelegramUI/Components/ChatTimerScreen/BUILD b/submodules/TelegramUI/Components/ChatTimerScreen/BUILD index a076943333..e7b96cf57c 100644 --- a/submodules/TelegramUI/Components/ChatTimerScreen/BUILD +++ b/submodules/TelegramUI/Components/ChatTimerScreen/BUILD @@ -11,15 +11,18 @@ swift_library( ], deps = [ "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", - "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/TelegramStringFormatting:TelegramStringFormatting", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/SheetComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift b/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift index f784eb74b2..91ded2f86e 100644 --- a/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift +++ b/submodules/TelegramUI/Components/ChatTimerScreen/Sources/ChatTimerScreen.swift @@ -1,14 +1,18 @@ import Foundation import UIKit import Display -import AsyncDisplayKit import TelegramCore import SwiftSignalKit import AccountContext -import SolidRoundedButtonNode import TelegramPresentationData import PresentationDataUtils import TelegramStringFormatting +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import ButtonComponent +import BundleIconComponent +import GlassBarButtonComponent public enum ChatTimerScreenStyle { case `default` @@ -21,101 +25,7 @@ public enum ChatTimerScreenMode { case mute } -public final class ChatTimerScreen: ViewController { - private var controllerNode: ChatTimerScreenNode { - return self.displayNode as! ChatTimerScreenNode - } - - private var animatedIn = false - - private let context: AccountContext - private let style: ChatTimerScreenStyle - private let mode: ChatTimerScreenMode - private let currentTime: Int32? - private let dismissByTapOutside: Bool - private let completion: (Int32) -> Void - - private var presentationData: PresentationData - private var presentationDataDisposable: Disposable? - - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, style: ChatTimerScreenStyle, mode: ChatTimerScreenMode = .sendTimer, currentTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { - self.context = context - self.style = style - self.mode = mode - self.currentTime = currentTime - self.dismissByTapOutside = dismissByTapOutside - self.completion = completion - - self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - - super.init(navigationBarPresentationData: nil) - - self.statusBar.statusBarStyle = .Ignore - - self.blocksBackgroundWhenInOverlay = true - - self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - strongSelf.presentationData = presentationData - strongSelf.controllerNode.updatePresentationData(presentationData) - } - }) - - self.statusBar.statusBarStyle = .Ignore - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDataDisposable?.dispose() - } - - override public func loadDisplayNode() { - self.displayNode = ChatTimerScreenNode(context: self.context, presentationData: presentationData, style: self.style, mode: self.mode, currentTime: self.currentTime, dismissByTapOutside: self.dismissByTapOutside) - self.controllerNode.completion = { [weak self] time in - guard let strongSelf = self else { - return - } - strongSelf.completion(time) - strongSelf.dismiss() - } - self.controllerNode.dismiss = { [weak self] in - self?.presentingViewController?.dismiss(animated: false, completion: nil) - } - self.controllerNode.cancel = { [weak self] in - self?.dismiss() - } - } - - override public func loadView() { - super.loadView() - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if !self.animatedIn { - self.animatedIn = true - self.controllerNode.animateIn() - } - } - - override public func dismiss(completion: (() -> Void)? = nil) { - self.controllerNode.animateOut(completion: completion) - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) - } -} - private protocol TimerPickerView: UIView { - } private class TimerCustomPickerView: UIPickerView, TimerPickerView { @@ -128,20 +38,20 @@ private class TimerCustomPickerView: UIPickerView, TimerPickerView { } } } - + override func didAddSubview(_ subview: UIView) { super.didAddSubview(subview) - + if let selectorColor = self.selectorColor { if subview.bounds.height <= 1.0 { subview.backgroundColor = selectorColor } } } - + override func didMoveToWindow() { super.didMoveToWindow() - + if let selectorColor = self.selectorColor { for subview in self.subviews { if subview.bounds.height <= 1.0 { @@ -162,20 +72,20 @@ private class TimerDatePickerView: UIDatePicker, TimerPickerView { } } } - + override func didAddSubview(_ subview: UIView) { super.didAddSubview(subview) - + if let selectorColor = self.selectorColor { if subview.bounds.height <= 1.0 { subview.backgroundColor = selectorColor } } } - + override func didMoveToWindow() { super.didMoveToWindow() - + if let selectorColor = self.selectorColor { for subview in self.subviews { if subview.bounds.height <= 1.0 { @@ -192,14 +102,14 @@ private let nondigitsCharacterSet = CharacterSet(charactersIn: "0123456789").inv private class TimerPickerItemView: UIView { let valueLabel = UILabel() let unitLabel = UILabel() - + var textColor: UIColor? = nil { didSet { self.valueLabel.textColor = self.textColor self.unitLabel.textColor = self.textColor } } - + var value: (Int32, String)? { didSet { if let (value, string) = self.value { @@ -215,36 +125,36 @@ private class TimerPickerItemView: UIView { self.unitLabel.text = string.trimmingCharacters(in: digitsCharacterSet) } } - + self.setNeedsLayout() } } - + override init(frame: CGRect) { self.valueLabel.backgroundColor = nil self.valueLabel.isOpaque = false self.valueLabel.font = Font.regular(24.0) - + self.unitLabel.backgroundColor = nil self.unitLabel.isOpaque = false self.unitLabel.font = Font.medium(16.0) - + super.init(frame: frame) - + self.addSubview(self.valueLabel) self.addSubview(self.unitLabel) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func layoutSubviews() { super.layoutSubviews() - + self.valueLabel.sizeToFit() self.unitLabel.sizeToFit() - + if let (value, _) = self.value, value == viewOnceTimeout { self.valueLabel.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((self.frame.width - self.valueLabel.frame.size.width) / 2.0), y: floor((self.frame.height - self.valueLabel.frame.height) / 2.0)), size: self.valueLabel.frame.size) } else { @@ -265,514 +175,667 @@ private var timerValues: [Int32] = { return values }() -class ChatTimerScreenNode: ViewControllerTracingNode, ASScrollViewDelegate, UIPickerViewDataSource, UIPickerViewDelegate { - private let context: AccountContext - private let controllerStyle: ChatTimerScreenStyle - private var presentationData: PresentationData - private let dismissByTapOutside: Bool - private let mode: ChatTimerScreenMode - - private let dimNode: ASDisplayNode - private let wrappingScrollNode: ASScrollNode - private let contentContainerNode: ASDisplayNode - private let effectNode: ASDisplayNode - private let backgroundNode: ASDisplayNode - private let contentBackgroundNode: ASDisplayNode - private let titleNode: ASTextNode - private let textNode: ImmediateTextNode - private let cancelButton: HighlightableButtonNode - private let doneButton: SolidRoundedButtonNode - - private let disableButton: HighlightableButtonNode - private let disableButtonTitle: ImmediateTextNode - - private var initialTime: Int32? - private var pickerView: TimerPickerView? - - private let autoremoveTimerValues: [Int32] - - private var containerLayout: (ContainerViewLayout, CGFloat)? - - var completion: ((Int32) -> Void)? - var dismiss: (() -> Void)? - var cancel: (() -> Void)? - - init(context: AccountContext, presentationData: PresentationData, style: ChatTimerScreenStyle, mode: ChatTimerScreenMode, currentTime: Int32?, dismissByTapOutside: Bool) { - self.context = context - self.controllerStyle = style - self.presentationData = presentationData - self.dismissByTapOutside = dismissByTapOutside - self.mode = mode - self.initialTime = currentTime - - self.wrappingScrollNode = ASScrollNode() - self.wrappingScrollNode.view.alwaysBounceVertical = true - self.wrappingScrollNode.view.delaysContentTouches = false - self.wrappingScrollNode.view.canCancelContentTouches = true - - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - - self.contentContainerNode = ASDisplayNode() - self.contentContainerNode.isOpaque = false +private let autoremoveTimerValues: [Int32] = [ + 1 * 24 * 60 * 60 as Int32, + 2 * 24 * 60 * 60 as Int32, + 3 * 24 * 60 * 60 as Int32, + 4 * 24 * 60 * 60 as Int32, + 5 * 24 * 60 * 60 as Int32, + 6 * 24 * 60 * 60 as Int32, + 1 * 7 * 24 * 60 * 60 as Int32, + 2 * 7 * 24 * 60 * 60 as Int32, + 3 * 7 * 24 * 60 * 60 as Int32, + 1 * 31 * 24 * 60 * 60 as Int32, + 2 * 30 * 24 * 60 * 60 as Int32, + 3 * 31 * 24 * 60 * 60 as Int32, + 4 * 30 * 24 * 60 * 60 as Int32, + 5 * 31 * 24 * 60 * 60 as Int32, + 6 * 30 * 24 * 60 * 60 as Int32, + 365 * 24 * 60 * 60 as Int32 +] - self.backgroundNode = ASDisplayNode() - self.backgroundNode.clipsToBounds = true - self.backgroundNode.cornerRadius = 16.0 - - let backgroundColor: UIColor - let textColor: UIColor - let accentColor: UIColor - let blurStyle: UIBlurEffect.Style - switch style { - case .default: - backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - textColor = self.presentationData.theme.actionSheet.primaryTextColor - accentColor = self.presentationData.theme.actionSheet.controlAccentColor - blurStyle = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark - case .media: - backgroundColor = UIColor(rgb: 0x1c1c1e) - textColor = .white - accentColor = self.presentationData.theme.actionSheet.controlAccentColor - blurStyle = .dark +private final class ChatTimerSheetContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let style: ChatTimerScreenStyle + let mode: ChatTimerScreenMode + let currentTime: Int32? + let dismiss: () -> Void + + init( + style: ChatTimerScreenStyle, + mode: ChatTimerScreenMode, + currentTime: Int32?, + dismiss: @escaping () -> Void + ) { + self.style = style + self.mode = mode + self.currentTime = currentTime + self.dismiss = dismiss + } + + static func ==(lhs: ChatTimerSheetContentComponent, rhs: ChatTimerSheetContentComponent) -> Bool { + if lhs.style != rhs.style { + return false } - - self.effectNode = ASDisplayNode(viewBlock: { - return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) - }) - - self.contentBackgroundNode = ASDisplayNode() - self.contentBackgroundNode.backgroundColor = backgroundColor - - let title: String - switch self.mode { - case .sendTimer: - title = self.presentationData.strings.Conversation_Timer_Title - case .autoremove: - title = self.presentationData.strings.Conversation_DeleteTimer_SetupTitle - case .mute: - title = self.presentationData.strings.Conversation_Mute_SetupTitle + if lhs.mode != rhs.mode { + return false } - self.titleNode = ASTextNode() - self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: textColor) - - self.textNode = ImmediateTextNode() - - self.cancelButton = HighlightableButtonNode() - self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: accentColor, for: .normal) - - self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, isShimmering: false) - self.doneButton.title = self.presentationData.strings.Conversation_Timer_Send - - self.disableButton = HighlightableButtonNode() - self.disableButtonTitle = ImmediateTextNode() - self.disableButton.addSubnode(self.disableButtonTitle) - self.disableButtonTitle.attributedText = NSAttributedString(string: self.presentationData.strings.Conversation_DeleteTimer_Disable, font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemAccentColor) - self.disableButton.isHidden = true - - switch self.mode { - case .autoremove: - if self.initialTime != nil { - self.disableButton.isHidden = false + if lhs.currentTime != rhs.currentTime { + return false + } + return true + } + + final class View: UIView, UIPickerViewDataSource, UIPickerViewDelegate { + private let closeButton = ComponentView() + private let title = ComponentView() + private let primaryButton = ComponentView() + private let secondaryButton = ComponentView() + + private var component: ChatTimerSheetContentComponent? + private var environment: EnvironmentType? + private weak var state: EmptyComponentState? + + private var pickerView: TimerPickerView? + private var isCompleting = false + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func selectedValue() -> Int32? { + guard let component = self.component, let pickerView = self.pickerView else { + return nil } - default: - break - } - - self.autoremoveTimerValues = [ - 1 * 24 * 60 * 60 as Int32, - 2 * 24 * 60 * 60 as Int32, - 3 * 24 * 60 * 60 as Int32, - 4 * 24 * 60 * 60 as Int32, - 5 * 24 * 60 * 60 as Int32, - 6 * 24 * 60 * 60 as Int32, - 1 * 7 * 24 * 60 * 60 as Int32, - 2 * 7 * 24 * 60 * 60 as Int32, - 3 * 7 * 24 * 60 * 60 as Int32, - 1 * 31 * 24 * 60 * 60 as Int32, - 2 * 30 * 24 * 60 * 60 as Int32, - 3 * 31 * 24 * 60 * 60 as Int32, - 4 * 30 * 24 * 60 * 60 as Int32, - 5 * 31 * 24 * 60 * 60 as Int32, - 6 * 30 * 24 * 60 * 60 as Int32, - 365 * 24 * 60 * 60 as Int32 - ] - - super.init() - - self.backgroundColor = nil - self.isOpaque = false - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - self.addSubnode(self.dimNode) - - self.wrappingScrollNode.view.delegate = self.wrappedScrollViewDelegate - self.addSubnode(self.wrappingScrollNode) - - self.wrappingScrollNode.addSubnode(self.backgroundNode) - self.wrappingScrollNode.addSubnode(self.contentContainerNode) - - self.backgroundNode.addSubnode(self.effectNode) - self.backgroundNode.addSubnode(self.contentBackgroundNode) - self.contentContainerNode.addSubnode(self.titleNode) - self.contentContainerNode.addSubnode(self.textNode) - self.contentContainerNode.addSubnode(self.cancelButton) - self.contentContainerNode.addSubnode(self.doneButton) - self.contentContainerNode.addSubnode(self.disableButton) - - self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) - self.doneButton.pressed = { [weak self] in - if let strongSelf = self, let pickerView = strongSelf.pickerView { - strongSelf.doneButton.isUserInteractionEnabled = false - if let pickerView = pickerView as? TimerCustomPickerView { - switch strongSelf.mode { - case .sendTimer: - let row = pickerView.selectedRow(inComponent: 0) - let value: Int32 - if row == 0 { - value = viewOnceTimeout - } else { - value = timerValues[row - 1] - } - strongSelf.completion?(value) - case .autoremove: - let timeInterval = strongSelf.autoremoveTimerValues[pickerView.selectedRow(inComponent: 0)] - strongSelf.completion?(Int32(timeInterval)) - case .mute: - break - } - } else if let pickerView = pickerView as? TimerDatePickerView { - switch strongSelf.mode { - case .mute: - let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - Int32(Date().timeIntervalSince1970)) - strongSelf.completion?(timeInterval) - default: - break + + if let pickerView = pickerView as? TimerCustomPickerView { + switch component.mode { + case .sendTimer: + let row = pickerView.selectedRow(inComponent: 0) + if row == 0 { + return viewOnceTimeout + } else { + return timerValues[row - 1] } + case .autoremove: + return autoremoveTimerValues[pickerView.selectedRow(inComponent: 0)] + case .mute: + return nil } - } - } - - self.disableButton.addTarget(self, action: #selector(self.disableButtonPressed), forControlEvents: .touchUpInside) - - self.setupPickerView(currentTime: currentTime) - } - - @objc private func disableButtonPressed() { - self.completion?(0) - } - - func setupPickerView(currentTime: Int32? = nil) { - if let pickerView = self.pickerView { - pickerView.removeFromSuperview() - } - - switch self.mode { - case .sendTimer: - let pickerView = TimerCustomPickerView() - pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18) - pickerView.dataSource = self - pickerView.delegate = self - - self.contentContainerNode.view.addSubview(pickerView) - self.pickerView = pickerView - case .autoremove: - let pickerView = TimerCustomPickerView() - pickerView.dataSource = self - pickerView.delegate = self - - pickerView.selectorColor = self.presentationData.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.18) - - self.contentContainerNode.view.addSubview(pickerView) - self.pickerView = pickerView - - if let value = self.initialTime { - var selectedRowIndex = 0 - for i in 0 ..< self.autoremoveTimerValues.count { - if self.autoremoveTimerValues[i] <= value { - selectedRowIndex = i - } - } - - pickerView.selectRow(selectedRowIndex, inComponent: 0, animated: false) - } - case .mute: - let pickerView = TimerDatePickerView() - pickerView.locale = localeWithStrings(self.presentationData.strings) - pickerView.datePickerMode = .dateAndTime - pickerView.minimumDate = Date() - if #available(iOS 13.4, *) { - pickerView.preferredDatePickerStyle = .wheels - } - pickerView.setValue(self.presentationData.theme.list.itemPrimaryTextColor, forKey: "textColor") - pickerView.setValue(false, forKey: "highlightsToday") - pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18) - pickerView.addTarget(self, action: #selector(self.dataPickerChanged), for: .valueChanged) - - self.contentContainerNode.view.addSubview(pickerView) - self.pickerView = pickerView - } - } - - @objc private func dataPickerChanged() { - if let (layout, navigationBarHeight) = self.containerLayout { - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - } - - func numberOfComponents(in pickerView: UIPickerView) -> Int { - switch self.mode { - case .sendTimer: - return 1 - case .autoremove: - return 1 - case .mute: - return 0 - } - } - - func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - switch self.mode { - case .sendTimer: - return timerValues.count + 1 - case .autoremove: - return self.autoremoveTimerValues.count - case .mute: - return 0 - } - } - - func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { - switch self.mode { - case .sendTimer: - if row == 0 { - let string = self.presentationData.strings.MediaPicker_Timer_ViewOnce - if let view = view as? TimerPickerItemView { - view.value = (viewOnceTimeout, string) - return view - } - - let view = TimerPickerItemView() - view.value = (viewOnceTimeout, string) - view.textColor = .white - return view + } else if let pickerView = pickerView as? TimerDatePickerView { + return Int32(pickerView.date.timeIntervalSince1970) } else { - let value = timerValues[row - 1] - let string = timeIntervalString(strings: self.presentationData.strings, value: value) - if let view = view as? TimerPickerItemView { - view.value = (value, string) - return view - } - - let view = TimerPickerItemView() - view.value = (value, string) - view.textColor = .white - return view + return nil } - case .autoremove: + } + + private func pickerTextColor(component: ChatTimerSheetContentComponent, environment: EnvironmentType) -> UIColor { + switch component.mode { + case .sendTimer: + return .white + case .autoremove: + if case .media = component.style { + return .white + } else { + return environment.theme.list.itemPrimaryTextColor + } + case .mute: + if case .media = component.style { + return .white + } else { + return environment.theme.list.itemPrimaryTextColor + } + } + } + + private func selectAutoremoveValue(_ value: Int32, in pickerView: TimerCustomPickerView) { + var selectedRowIndex = 0 + for i in 0 ..< autoremoveTimerValues.count { + if autoremoveTimerValues[i] <= value { + selectedRowIndex = i + } + } + pickerView.selectRow(selectedRowIndex, inComponent: 0, animated: false) + } + + private func setupPickerView(component: ChatTimerSheetContentComponent, environment: EnvironmentType) { + let previousSelectedValue = self.selectedValue() + let previousDate = (self.pickerView as? TimerDatePickerView)?.date + + if let pickerView = self.pickerView { + pickerView.removeFromSuperview() + } + + switch component.mode { + case .sendTimer: + let pickerView = TimerCustomPickerView() + pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18) + pickerView.dataSource = self + pickerView.delegate = self + self.addSubview(pickerView) + self.pickerView = pickerView + + if let previousSelectedValue { + if previousSelectedValue == viewOnceTimeout { + pickerView.selectRow(0, inComponent: 0, animated: false) + } else if let index = timerValues.firstIndex(of: previousSelectedValue) { + pickerView.selectRow(index + 1, inComponent: 0, animated: false) + } + } + case .autoremove: + let pickerView = TimerCustomPickerView() + pickerView.dataSource = self + pickerView.delegate = self + pickerView.selectorColor = self.pickerTextColor(component: component, environment: environment).withMultipliedAlpha(0.18) + self.addSubview(pickerView) + self.pickerView = pickerView + + if let previousSelectedValue { + self.selectAutoremoveValue(previousSelectedValue, in: pickerView) + } else if let currentTime = component.currentTime { + self.selectAutoremoveValue(currentTime, in: pickerView) + } + case .mute: + let pickerView = TimerDatePickerView() + pickerView.locale = localeWithStrings(environment.strings) + pickerView.datePickerMode = .dateAndTime + pickerView.minimumDate = Date() + if #available(iOS 13.4, *) { + pickerView.preferredDatePickerStyle = .wheels + } + pickerView.setValue(self.pickerTextColor(component: component, environment: environment), forKey: "textColor") + pickerView.setValue(false, forKey: "highlightsToday") + pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18) + pickerView.addTarget(self, action: #selector(self.datePickerChanged), for: .valueChanged) + if let previousDate { + pickerView.date = max(previousDate, Date()) + } + self.addSubview(pickerView) + self.pickerView = pickerView + } + } + + @objc private func datePickerChanged() { + self.state?.updated(transition: .immediate) + } + + private func title(strings: PresentationStrings) -> String { + guard let component = self.component else { + return "" + } + + switch component.mode { + case .sendTimer: + return strings.Conversation_Timer_Title + case .autoremove: + return strings.Conversation_DeleteTimer_SetupTitle + case .mute: + return strings.Conversation_Mute_SetupTitle + } + } + + private func primaryButtonTitle(component: ChatTimerSheetContentComponent, environment: EnvironmentType) -> String { + switch component.mode { + case .sendTimer: + return environment.strings.Conversation_Timer_Send + case .autoremove: + return environment.strings.Conversation_DeleteTimer_Apply + case .mute: + if let pickerView = self.pickerView as? TimerDatePickerView { + let now = Int32(Date().timeIntervalSince1970) + let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - now) + + if timeInterval > 0 { + let timeString = stringForPreciseRelativeTimestamp(strings: environment.strings, relativeTimestamp: Int32(pickerView.date.timeIntervalSince1970), relativeTo: now, dateTimeFormat: environment.dateTimeFormat) + return environment.strings.Conversation_Mute_ApplyMuteUntil(timeString).string + } else { + return environment.strings.Common_Close + } + } else { + return environment.strings.Common_Close + } + } + } + + private func complete(value: Int32) { + guard !self.isCompleting else { + return + } + self.isCompleting = true + + if let controller = self.environment?.controller() as? ChatTimerScreen { + controller.completion(value) + } + self.component?.dismiss() + } + + private func completeWithPickerValue() { + guard let component = self.component, let pickerView = self.pickerView else { + return + } + + if let pickerView = pickerView as? TimerCustomPickerView { + switch component.mode { + case .sendTimer: + let row = pickerView.selectedRow(inComponent: 0) + let value: Int32 + if row == 0 { + value = viewOnceTimeout + } else { + value = timerValues[row - 1] + } + self.complete(value: value) + case .autoremove: + self.complete(value: autoremoveTimerValues[pickerView.selectedRow(inComponent: 0)]) + case .mute: + break + } + } else if let pickerView = pickerView as? TimerDatePickerView { + switch component.mode { + case .mute: + let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - Int32(Date().timeIntervalSince1970)) + self.complete(value: timeInterval) + default: + break + } + } + } + + func update(component: ChatTimerSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let environment = environment[EnvironmentType.self].value + let previousComponent = self.component + let previousEnvironment = self.environment + let themeUpdated: Bool + let stringsUpdated: Bool + if let previousEnvironment { + themeUpdated = previousEnvironment.theme !== environment.theme + stringsUpdated = previousEnvironment.strings !== environment.strings + } else { + themeUpdated = false + stringsUpdated = false + } + + self.component = component + self.environment = environment + self.state = state + + if self.pickerView == nil || previousComponent?.mode != component.mode || previousComponent?.style != component.style || themeUpdated || stringsUpdated { + self.setupPickerView(component: component, environment: environment) + } + + let titleColor: UIColor + switch component.style { + case .default: + titleColor = environment.theme.actionSheet.primaryTextColor + case .media: + titleColor = .white + } + + let barButtonSize = CGSize(width: 44.0, height: 44.0) + let closeButtonSize = self.closeButton.update( + transition: transition, + component: AnyComponent( + GlassBarButtonComponent( + size: barButtonSize, + backgroundColor: nil, + isDark: environment.theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: environment.theme.chat.inputPanel.panelControlColor + ) + )), + action: { [weak self] _ in + self?.component?.dismiss() + } + ) + ), + environment: {}, + containerSize: barButtonSize + ) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.addSubview(closeButtonView) + } + transition.setFrame(view: closeButtonView, frame: CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize)) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + Text(text: self.title(strings: environment.strings), font: Font.semibold(17.0), color: titleColor) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 120.0, height: 44.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: floorToScreenPixels(16.0 + (barButtonSize.height - titleSize.height) / 2.0)), size: titleSize)) + } + + var contentHeight: CGFloat = 68.0 + + let pickerHeight: CGFloat = 216.0 + if let pickerView = self.pickerView { + transition.setFrame(view: pickerView as UIView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: availableSize.width, height: pickerHeight))) + } + contentHeight += pickerHeight + contentHeight += 17.0 + + let buttonSideInset: CGFloat = 30.0 + let primaryButtonTitle = self.primaryButtonTitle(component: component, environment: environment) + let primaryButtonSize = self.primaryButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(primaryButtonTitle), component: AnyComponent( + Text(text: primaryButtonTitle, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) + )), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + self?.completeWithPickerValue() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0) + ) + if let primaryButtonView = self.primaryButton.view { + if primaryButtonView.superview == nil { + self.addSubview(primaryButtonView) + } + transition.setFrame(view: primaryButtonView, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: primaryButtonSize)) + } + contentHeight += primaryButtonSize.height + + if case .autoremove = component.mode, component.currentTime != nil { + contentHeight += 8.0 + + let secondaryButtonTitle = environment.strings.Conversation_DeleteTimer_Disable + let secondaryButtonSize = self.secondaryButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: environment.theme.list.itemDestructiveColor.withMultipliedAlpha(0.1), + foreground: environment.theme.list.itemDestructiveColor, + pressedColor: environment.theme.list.itemDestructiveColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(secondaryButtonTitle), component: AnyComponent( + Text(text: secondaryButtonTitle, font: Font.semibold(17.0), color: environment.theme.list.itemDestructiveColor) + )), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + self?.complete(value: 0) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 52.0) + ) + if let secondaryButtonView = self.secondaryButton.view { + if secondaryButtonView.superview == nil { + self.addSubview(secondaryButtonView) + } + transition.setFrame(view: secondaryButtonView, frame: CGRect(origin: CGPoint(x: buttonSideInset, y: contentHeight), size: secondaryButtonSize)) + } + contentHeight += secondaryButtonSize.height + } else if let secondaryButtonView = self.secondaryButton.view, secondaryButtonView.superview != nil { + secondaryButtonView.removeFromSuperview() + } + + let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : 15.0 + contentHeight += bottomInset + + return CGSize(width: availableSize.width, height: contentHeight) + } + + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + guard let component = self.component else { + return 0 + } + + switch component.mode { + case .sendTimer: + return timerValues.count + 1 + case .autoremove: + return autoremoveTimerValues.count + case .mute: + return 0 + } + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent componentIndex: Int, reusing view: UIView?) -> UIView { + guard let component = self.component, let environment = self.environment else { + return UIView() + } + let itemView: TimerPickerItemView if let current = view as? TimerPickerItemView { itemView = current } else { itemView = TimerPickerItemView() - itemView.textColor = self.presentationData.theme.list.itemPrimaryTextColor } - - let value = self.autoremoveTimerValues[row] - - let string: String - string = timeIntervalString(strings: self.presentationData.strings, value: value) - - itemView.value = (value, string) - - return itemView - case .mute: - preconditionFailure() - } - } - - func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - self.dataPickerChanged() - } - - func updatePresentationData(_ presentationData: PresentationData) { - let previousTheme = self.presentationData.theme - self.presentationData = presentationData - - guard case .default = self.controllerStyle else { - return - } - - if let effectView = self.effectNode.view as? UIVisualEffectView { - effectView.effect = UIBlurEffect(style: presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) - } - - self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) - - if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { - self.setupPickerView() - self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) - } - - self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) - self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) - } - - override func didLoad() { - super.didLoad() - - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never - } - } - - @objc func cancelButtonPressed() { - self.cancel?() - } - - @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if self.dismissByTapOutside, case .ended = recognizer.state { - self.cancelButtonPressed() - } - } - - func animateIn() { - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) - - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - let targetBounds = self.bounds - self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) - self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) - transition.animateView({ - self.bounds = targetBounds - self.dimNode.position = dimPosition - }) - } - - func animateOut(completion: (() -> Void)? = nil) { - var dimCompleted = false - var offsetCompleted = false - - let internalCompletion: () -> Void = { [weak self] in - if let strongSelf = self, dimCompleted && offsetCompleted { - strongSelf.dismiss?() - } - completion?() - } - - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in - dimCompleted = true - internalCompletion() - }) - - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - offsetCompleted = true - internalCompletion() - }) - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) { - if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { - return self.dimNode.view - } - } - return super.hitTest(point, with: event) - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - let contentOffset = scrollView.contentOffset - let additionalTopHeight = max(0.0, -contentOffset.y) - - if additionalTopHeight >= 30.0 { - self.cancelButtonPressed() - } - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.containerLayout = (layout, navigationBarHeight) - - var insets = layout.insets(options: [.statusBar, .input]) - let cleanInsets = layout.insets(options: [.statusBar]) - insets.top = max(10.0, insets.top) - - var buttonOffset: CGFloat = 0.0 - let bottomInset: CGFloat = 10.0 + cleanInsets.bottom - let titleHeight: CGFloat = 54.0 - var contentHeight = titleHeight + bottomInset + 52.0 + 17.0 - let pickerHeight: CGFloat = min(216.0, layout.size.height - contentHeight) - - if !self.disableButton.isHidden { - buttonOffset += 52.0 - } - - contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + pickerHeight + buttonOffset - - let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) - - let sideInset = floor((layout.size.width - width) / 2.0) - let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) - let contentFrame = contentContainerFrame - - var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0)) - if backgroundFrame.minY < contentFrame.minY { - backgroundFrame.origin.y = contentFrame.minY - } - transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let titleSize = self.titleNode.measure(CGSize(width: width, height: titleHeight)) - let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 16.0), size: titleSize) - transition.updateFrame(node: self.titleNode, frame: titleFrame) - - let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight)) - let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize) - transition.updateFrame(node: self.cancelButton, frame: cancelFrame) - - let buttonInset: CGFloat = 16.0 - - switch self.mode { - case .sendTimer: - break - case .autoremove: - self.doneButton.title = self.presentationData.strings.Conversation_DeleteTimer_Apply - case .mute: - if let pickerView = self.pickerView as? TimerDatePickerView { - let timeInterval = max(0, Int32(pickerView.date.timeIntervalSince1970) - Int32(Date().timeIntervalSince1970)) - - if timeInterval > 0 { - let timeString = stringForPreciseRelativeTimestamp(strings: self.presentationData.strings, relativeTimestamp: Int32(pickerView.date.timeIntervalSince1970), relativeTo: Int32(Date().timeIntervalSince1970), dateTimeFormat: self.presentationData.dateTimeFormat) - - self.doneButton.title = self.presentationData.strings.Conversation_Mute_ApplyMuteUntil(timeString).string + itemView.textColor = self.pickerTextColor(component: component, environment: environment) + + switch component.mode { + case .sendTimer: + if row == 0 { + let string = environment.strings.MediaPicker_Timer_ViewOnce + itemView.value = (viewOnceTimeout, string) } else { - self.doneButton.title = self.presentationData.strings.Common_Close + let value = timerValues[row - 1] + let string = timeIntervalString(strings: environment.strings, value: value) + itemView.value = (value, string) } - } else { - self.doneButton.title = self.presentationData.strings.Common_Close + case .autoremove: + let value = autoremoveTimerValues[row] + let string = timeIntervalString(strings: environment.strings, value: value) + itemView.value = (value, string) + case .mute: + preconditionFailure() } + + return itemView + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + self.state?.updated(transition: .immediate) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class ChatTimerSheetComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let style: ChatTimerScreenStyle + let mode: ChatTimerScreenMode + let currentTime: Int32? + + init( + style: ChatTimerScreenStyle, + mode: ChatTimerScreenMode, + currentTime: Int32? + ) { + self.style = style + self.mode = mode + self.currentTime = currentTime + } + + static func ==(lhs: ChatTimerSheetComponent, rhs: ChatTimerSheetComponent) -> Bool { + if lhs.style != rhs.style { + return false + } + if lhs.mode != rhs.mode { + return false + } + if lhs.currentTime != rhs.currentTime { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>() + private let sheetAnimateOut = ActionSlot>() + + private var component: ChatTimerSheetComponent? + private var environment: EnvironmentType? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func dismiss() { + self.sheetAnimateOut.invoke(Action { [weak self] _ in + guard let self, let controller = self.environment?.controller() else { + return + } + controller.dismiss(completion: nil) + }) + } + + func update(component: ChatTimerSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + let sheetEnvironment = SheetComponentEnvironment( + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { [weak self] _ in + self?.dismiss() + } + ) + + let backgroundColor: UIColor + switch component.style { + case .default: + backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor + case .media: + backgroundColor = UIColor(rgb: 0x1c1c1e) + } + + let _ = self.sheet.update( + transition: transition, + component: AnyComponent(SheetComponent( + content: AnyComponent(ChatTimerSheetContentComponent( + style: component.style, + mode: component.mode, + currentTime: component.currentTime, + dismiss: { [weak self] in + self?.dismiss() + } + )), + style: .glass, + backgroundColor: .color(backgroundColor), + followContentSizeChanges: true, + animateOut: self.sheetAnimateOut + )), + environment: { + environment + sheetEnvironment + }, + containerSize: availableSize + ) + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } + + 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) + } +} + +public final class ChatTimerScreen: ViewControllerComponentContainer { + fileprivate let completion: (Int32) -> Void + + public init( + context: AccountContext, + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + style: ChatTimerScreenStyle, + mode: ChatTimerScreenMode = .sendTimer, + currentTime: Int32? = nil, + completion: @escaping (Int32) -> Void + ) { + self.completion = completion + + super.init( + context: context, + component: ChatTimerSheetComponent( + style: style, + mode: mode, + currentTime: currentTime + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: style == .media ? .dark : .default, + updatedPresentationData: updatedPresentationData + ) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() } - - let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) - let doneButtonFrame = CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width, height: doneButtonHeight) - transition.updateFrame(node: self.doneButton, frame: doneButtonFrame) - - let disableButtonTitleSize = self.disableButtonTitle.updateLayout(CGSize(width: contentFrame.width, height: doneButtonHeight)) - let disableButtonFrame = CGRect(origin: CGPoint(x: doneButtonFrame.minX, y: doneButtonFrame.maxY), size: CGSize(width: contentFrame.width - buttonInset * 2.0, height: doneButtonHeight)) - transition.updateFrame(node: self.disableButton, frame: disableButtonFrame) - transition.updateFrame(node: self.disableButtonTitle, frame: CGRect(origin: CGPoint(x: floor((disableButtonFrame.width - disableButtonTitleSize.width) / 2.0), y: floor((disableButtonFrame.height - disableButtonTitleSize.height) / 2.0)), size: disableButtonTitleSize)) - - self.pickerView?.frame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: pickerHeight)) - - transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) } } diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleComponent.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleComponent.swift index 64ed2c7941..c2c1e04a57 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleComponent.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleComponent.swift @@ -376,7 +376,7 @@ public final class ChatTitleComponent: Component { var titleStatusIcon: ChatTitleCredibilityIcon = .none var isEnabled = true switch component.content { - case let .peer(peerView, customTitle, _, _, isScheduledMessages, isMuted, _, isEnabledValue): + case let .peer(peerView, customTitle, _, _, isScheduledMessages, isMuted, _, hidePeerStatus, isEnabledValue): if peerView.peerId.isReplies { titleSegments = [AnimatedTextComponent.Item( id: AnyHashable(0), @@ -448,7 +448,7 @@ public final class ChatTitleComponent: Component { titleCredibilityIcon = .fake } else if peer.isScam { titleCredibilityIcon = .scam - } else if let emojiStatus = peer.emojiStatus { + } else if !hidePeerStatus, let emojiStatus = peer.emojiStatus { titleStatusIcon = .emojiStatus(emojiStatus) } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { titleCredibilityIcon = .premium @@ -612,7 +612,7 @@ public final class ChatTitleComponent: Component { var inputActivitiesAllowed = true switch component.content { - case let .peer(peerView, _, _, _, isScheduledMessages, _, _, _): + case let .peer(peerView, _, _, _, isScheduledMessages, _, _, _, _): if let peer = peerView.peer { if peer.id == component.context.account.peerId || isScheduledMessages || peer.id.isRepliesOrVerificationCodes { inputActivitiesAllowed = false @@ -710,7 +710,7 @@ public final class ChatTitleComponent: Component { } } else { switch component.content { - case let .peer(peerView, customTitle, customSubtitle, onlineMemberCount, isScheduledMessages, _, customMessageCount, _): + case let .peer(peerView, customTitle, customSubtitle, onlineMemberCount, isScheduledMessages, _, customMessageCount, _, _): if let customSubtitle { let string = NSAttributedString(string: customSubtitle, font: subtitleFont, textColor: component.theme.chat.inputPanel.inputControlColor) state = .info(string, .generic) diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index 58fffdcb90..283fc51bd4 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -110,14 +110,14 @@ public enum ChatTitleContent: Equatable { } } - case peer(peerView: PeerData, customTitle: String?, customSubtitle: String?, onlineMemberCount: (total: Int32?, recent: Int32?), isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?, isEnabled: Bool) + case peer(peerView: PeerData, customTitle: String?, customSubtitle: String?, onlineMemberCount: (total: Int32?, recent: Int32?), isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?, hidePeerStatus: Bool, isEnabled: Bool) case replyThread(type: ReplyThreadType, count: Int) case custom(title: [TitleTextItem], subtitle: String?, isEnabled: Bool) public static func ==(lhs: ChatTitleContent, rhs: ChatTitleContent) -> Bool { switch lhs { - case let .peer(peerView, customTitle, customSubtitle, onlineMemberCount, isScheduledMessages, isMuted, customMessageCount, isEnabled): - if case let .peer(rhsPeerView, rhsCustomTitle, rhsCustomSubtitle, rhsOnlineMemberCount, rhsIsScheduledMessages, rhsIsMuted, rhsCustomMessageCount, rhsIsEnabled) = rhs { + case let .peer(peerView, customTitle, customSubtitle, onlineMemberCount, isScheduledMessages, isMuted, customMessageCount, hidePeerStatus, isEnabled): + if case let .peer(rhsPeerView, rhsCustomTitle, rhsCustomSubtitle, rhsOnlineMemberCount, rhsIsScheduledMessages, rhsIsMuted, rhsCustomMessageCount, rhsHidePeerStatus, rhsIsEnabled) = rhs { if peerView != rhsPeerView { return false } @@ -139,6 +139,9 @@ public enum ChatTitleContent: Equatable { if customMessageCount != rhsCustomMessageCount { return false } + if hidePeerStatus != rhsHidePeerStatus { + return false + } if isEnabled != rhsIsEnabled { return false } @@ -269,7 +272,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { var titleStatusIcon: ChatTitleCredibilityIcon = .none var isEnabled = true switch titleContent { - case let .peer(peerView, customTitle, _, _, isScheduledMessages, isMuted, _, isEnabledValue): + case let .peer(peerView, customTitle, _, _, isScheduledMessages, isMuted, _, hidePeerStatus, isEnabledValue): if peerView.peerId.isReplies { let typeText: String = self.strings.DialogList_Replies segments = [.text(0, NSAttributedString(string: typeText, font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] @@ -306,7 +309,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { titleCredibilityIcon = .fake } else if peer.isScam { titleCredibilityIcon = .scam - } else if let emojiStatus = peer.emojiStatus { + } else if !hidePeerStatus, let emojiStatus = peer.emojiStatus { titleStatusIcon = .emojiStatus(emojiStatus) } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { titleCredibilityIcon = .premium @@ -476,8 +479,8 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { var enableAnimation = false switch titleContent { - case let .peer(_, customTitle, _, _, _, _, _, _): - if case let .peer(_, previousCustomTitle, _, _, _, _, _, _) = oldValue { + case let .peer(_, customTitle, _, _, _, _, _, _, _): + if case let .peer(_, previousCustomTitle, _, _, _, _, _, _, _) = oldValue { if customTitle != previousCustomTitle { enableAnimation = false } @@ -503,7 +506,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { var inputActivitiesAllowed = true if let titleContent = self.titleContent { switch titleContent { - case let .peer(peerView, _, _, _, isScheduledMessages, _, _, _): + case let .peer(peerView, _, _, _, isScheduledMessages, _, _, _, _): if let peer = peerView.peer { if peer.id == self.context.account.peerId || isScheduledMessages || peer.id.isRepliesOrVerificationCodes { inputActivitiesAllowed = false @@ -604,7 +607,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { } else { if let titleContent = self.titleContent { switch titleContent { - case let .peer(peerView, customTitle, customSubtitle, onlineMemberCount, isScheduledMessages, _, customMessageCount, _): + case let .peer(peerView, customTitle, customSubtitle, onlineMemberCount, isScheduledMessages, _, customMessageCount, _, _): if let customSubtitle { let string = NSAttributedString(string: customSubtitle, font: subtitleFont, textColor: titleTheme.rootController.navigationBar.secondaryTextColor) state = .info(string, .generic) diff --git a/submodules/TelegramUI/Components/CocoonInfoScreen/BUILD b/submodules/TelegramUI/Components/CocoonInfoScreen/BUILD index 014f566a64..2256fb995e 100644 --- a/submodules/TelegramUI/Components/CocoonInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/CocoonInfoScreen/BUILD @@ -14,7 +14,6 @@ swift_library( "//submodules/AsyncDisplayKit", "//submodules/Display", "//submodules/ComponentFlow", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/TelegramPresentationData", "//submodules/TelegramUIPreferences", diff --git a/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift b/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift index ff119d1368..f7e1b6a566 100644 --- a/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift +++ b/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import AccountContext diff --git a/submodules/TelegramUI/Components/ComposePollScreen/BUILD b/submodules/TelegramUI/Components/ComposePollScreen/BUILD index 358b7890a4..cecd26b01f 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/BUILD +++ b/submodules/TelegramUI/Components/ComposePollScreen/BUILD @@ -60,6 +60,8 @@ swift_library( "//submodules/CounterControllerTitleView", "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/ICloudResources", + "//submodules/CountrySelectionUI", + "//submodules/TelegramUI/Components/ShareWithPeersScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift index c7f9fdcb36..d200bcaf60 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift @@ -35,6 +35,8 @@ import ContextUI import StickerPeekUI import EdgeEffect import LocationUI +import CountrySelectionUI +import ShareWithPeersScreen public final class ComposedPoll { public struct Text { @@ -54,6 +56,8 @@ public final class ComposedPoll { public let revotingDisabled: Bool public let shuffleAnswers: Bool public let hideResultsUntilClose: Bool + public let restrictToSubscribers: Bool + public let limitToCountries: [String] public let text: Text public let description: Text @@ -72,6 +76,8 @@ public final class ComposedPoll { revotingDisabled: Bool, shuffleAnswers: Bool, hideResultsUntilClose: Bool, + restrictToSubscribers: Bool, + limitToCountries: [String], text: Text, description: Text, media: AnyMediaReference?, @@ -88,6 +94,8 @@ public final class ComposedPoll { self.revotingDisabled = revotingDisabled self.shuffleAnswers = shuffleAnswers self.hideResultsUntilClose = hideResultsUntilClose + self.restrictToSubscribers = restrictToSubscribers + self.limitToCountries = limitToCountries self.text = text self.description = description self.media = media @@ -229,11 +237,17 @@ final class ComposePollScreenComponent: Component { private var canAddOptions: Bool = false private var canRevote: Bool = false private var shuffleOptions: Bool = false - private var isQuiz: Bool = false + private(set) var isQuiz: Bool = false private var selectedQuizOptionIds = Set() private var limitDuration: Bool = false private var timeLimit: TimeLimit = .duration(24 * 60 * 60) private var hideResults: Bool = false + private var restrictToSubscribers: Bool = false + private var limitByCountry: Bool = false + private var limitToCountries: [String] = [] + + private var currentLocale: Locale? + private var didSetupCountries = false private var currentInputMode: ListComposePollOptionComponent.InputMode = .keyboard @@ -258,6 +272,8 @@ final class ComposePollScreenComponent: Component { private var cachedShuffleIcon: UIImage? private var cachedQuizIcon: UIImage? private var cachedDurationIcon: UIImage? + private var cachedSubscribersIcon: UIImage? + private var cachedCountryIcon: UIImage? private var cachedEmptyIcon: UIImage? private var reorderRecognizer: ReorderGestureRecognizer? @@ -346,7 +362,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) } @@ -450,13 +466,17 @@ 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 + case countriesNeeded } var hasAnyData: Bool { @@ -483,18 +503,11 @@ 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 - if self.isQuiz { - mappedKind = .quiz(multipleAnswers: self.effectiveIsMultiAnswer) - } else { - mappedKind = .poll(multipleAnswers: self.effectiveIsMultiAnswer) - } - + var mappedOptions: [TelegramMediaPollOption] = [] var selectedQuizOptions: [Data] = [] for pollOption in self.pollOptions { @@ -529,8 +542,21 @@ final class ComposePollScreenComponent: Component { )) } - if mappedOptions.count < 2 { - return nil + let mappedKind: TelegramMediaPollKind + if self.isQuiz { + mappedKind = .quiz(multipleAnswers: self.effectiveIsMultiAnswer) + } else { + mappedKind = .poll(multipleAnswers: self.effectiveIsMultiAnswer && mappedOptions.count > 1) + } + + if self.isQuiz && mappedOptions.count < 2 { + return .optionsNeeded + } else if !self.isQuiz && mappedOptions.count < 1 { + return .optionsNeeded + } + + if self.limitByCountry && self.limitToCountries.isEmpty { + return .countriesNeeded } var mappedCorrectAnswers: [Data]? @@ -538,12 +564,12 @@ final class ComposePollScreenComponent: Component { if !selectedQuizOptions.isEmpty { mappedCorrectAnswers = selectedQuizOptions } else { - return nil + return .quizCorrectOptionNeeded } } 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 { @@ -605,6 +631,8 @@ final class ComposePollScreenComponent: Component { revotingDisabled: !self.canRevote, shuffleAnswers: self.shuffleOptions, hideResultsUntilClose: self.hideResults, + restrictToSubscribers: self.restrictToSubscribers, + limitToCountries: self.limitByCountry ? self.limitToCountries : [], text: ComposedPoll.Text(string: self.pollTextInputState.text.string, entities: textEntities), description: ComposedPoll.Text(string: self.pollDescriptionInputState.text.string, entities: descriptionEntities), media: self.pollDescriptionMedia?.media, @@ -621,7 +649,8 @@ final class ComposePollScreenComponent: Component { media: mappedSolution.2?.media ) }, - hasUnseenVotes: false + hasUnseenVotes: false, + canViewStats: false ), deadlineTimeout: deadlineTimeout, deadlineDate: deadlineDate, @@ -730,7 +759,6 @@ final class ComposePollScreenComponent: Component { mode: .standard(.default), chatLocation: .peer(id: component.context.account.peerId), subject: nil, - peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, @@ -742,8 +770,6 @@ final class ComposePollScreenComponent: Component { businessIntro: nil ) - //self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor - let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) let inputNodeHeight = heightAndOverflow.0 let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) @@ -961,7 +987,7 @@ final class ComposePollScreenComponent: Component { switch result { case let .media(resultMedia): if let resultImage = resultMedia.media as? TelegramMediaImage, let resultLargest = largestImageRepresentation(resultImage.representations) { - component.context.account.postbox.mediaBox.moveResourceData(from: largest.resource.id, to: resultLargest.resource.id, synchronous: true) + component.context.engine.resources.moveResourceData(from: EngineMediaResource.Id(largest.resource.id), to: EngineMediaResource.Id(resultLargest.resource.id), synchronous: true) } media.media = resultMedia @@ -1000,7 +1026,7 @@ final class ComposePollScreenComponent: Component { switch result { case let .media(resultMedia): if let resultFile = resultMedia.media as? TelegramMediaFile { - component.context.account.postbox.mediaBox.moveResourceData(from: file.resource.id, to: resultFile.resource.id, synchronous: true) + component.context.engine.resources.moveResourceData(from: EngineMediaResource.Id(file.resource.id), to: EngineMediaResource.Id(resultFile.resource.id), synchronous: true) } media.media = resultMedia media.progress = nil @@ -1214,6 +1240,41 @@ final class ComposePollScreenComponent: Component { (self.environment?.controller() as? ComposePollScreen)?.parentController()?.push(controller) } + private func openCountriesSelection() { + guard let component = self.component else { + return + } + + let maxCount: Int32 + if let data = component.context.currentAppConfiguration.with({ $0 }).data, let value = data["poll_countries_max"] as? Double { + maxCount = Int32(value) + } else { + maxCount = 12 + } + + let stateContext = CountriesMultiselectionScreen.StateContext( + context: component.context, + subject: .countries, + maxCount: maxCount, + initialSelectedCountries: self.limitToCountries, + showFragment: true + ) + let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in + let controller = CountriesMultiselectionScreen( + context: component.context, + stateContext: stateContext, + completion: { [weak self] countries in + guard let self else { + return + } + self.limitToCountries = countries + self.state?.updated() + } + ) + (self?.environment?.controller() as? ComposePollScreen)?.parentController()?.push(controller) + }) + } + func deactivateInput() { self.currentInputMode = .keyboard if hasFirstResponder(self) { @@ -1239,13 +1300,15 @@ 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 } if self.component == nil { + self.currentLocale = localeWithStrings(environment.strings) + self.isQuiz = component.isQuiz ?? false if !self.isQuiz { self.isMultiAnswer = true @@ -1412,8 +1475,14 @@ final class ComposePollScreenComponent: Component { self.cachedShuffleIcon = renderSettingsIcon(name: "Item List/Icons/Shuffle", backgroundColors: [UIColor(rgb: 0xAF52DE)]) self.cachedQuizIcon = renderSettingsIcon(name: "Item List/Icons/Checkbox", backgroundColors: [UIColor(rgb: 0x34C759)]) self.cachedDurationIcon = renderSettingsIcon(name: "Item List/Icons/Timer", backgroundColors: [UIColor(rgb: 0xFF453A)]) + self.cachedSubscribersIcon = renderSettingsIcon(name: "Item List/Icons/Profile", backgroundColors: [UIColor(rgb: 0x0A84FF)]) + self.cachedCountryIcon = renderSettingsIcon(name: "Item List/Icons/Flag", backgroundColors: [UIColor(rgb: 0xFF9F0A)]) 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 @@ -1438,7 +1507,6 @@ final class ComposePollScreenComponent: Component { pollTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent( externalState: self.pollTextInputState, context: component.context, - style: .glass, theme: theme, strings: environment.strings, resetText: self.resetPollText.flatMap { resetText in @@ -1474,7 +1542,6 @@ final class ComposePollScreenComponent: Component { pollTextSectionItems.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ListComposePollOptionComponent( externalState: self.pollDescriptionInputState, context: component.context, - style: .glass, theme: theme, strings: environment.strings, resetText: nil, @@ -1590,7 +1657,6 @@ final class ComposePollScreenComponent: Component { pollOptionsSectionItems.append(AnyComponentWithIdentity(id: pollOption.id, component: AnyComponent(ListComposePollOptionComponent( externalState: pollOption.textInputState, context: component.context, - style: .glass, theme: theme, strings: environment.strings, resetText: pollOption.resetText.flatMap { resetText in @@ -2249,6 +2315,140 @@ final class ComposePollScreenComponent: Component { maximumNumberOfLines: 0 )) } + + if isChannel { + pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "subscribers", component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_RestrictToSubscribers, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 2 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_RestrictToSubscribersInfo, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 3, + lineSpacing: 0.1 + ))) + ], alignment: .left, spacing: 4.0)), + verticalAlignment: .middle, + contentInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0), + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent( + Image(image: self.cachedSubscribersIcon, size: CGSize(width: 30.0, height: 30.0)) + )), false), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.restrictToSubscribers, action: { [weak self] _ in + guard let self else { + return + } + self.restrictToSubscribers = !self.restrictToSubscribers + self.state?.updated(transition: .spring(duration: 0.4)) + })), + action: nil + )))) + + pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "limitCountry", component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_LimitCountry, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 2 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_LimitCountryInfo, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0), + textColor: theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 3, + lineSpacing: 0.1 + ))) + ], alignment: .left, spacing: 4.0)), + verticalAlignment: .middle, + contentInsets: UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0), + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent( + Image(image: self.cachedCountryIcon, size: CGSize(width: 30.0, height: 30.0)) + )), false), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: self.limitByCountry, action: { [weak self] _ in + guard let self else { + return + } + self.limitByCountry = !self.limitByCountry + self.state?.updated(transition: .spring(duration: 0.4)) + + if self.limitByCountry { + if !self.didSetupCountries { + let countriesConfiguration = component.context.currentCountriesConfiguration.with { $0 } + AuthorizationSequenceCountrySelectionController.setupCountryCodes(countries: countriesConfiguration.countries, codesByPrefix: countriesConfiguration.countriesByPrefix) + } + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: self.scrollView.contentSize.height - self.scrollView.bounds.size.height), animated: true) + } + })), + action: nil + )))) + + if self.limitByCountry { + var value: String + if self.limitToCountries.count > 1 { + value = environment.strings.CreatePoll_AllowedCountries_Countries(Int32(self.limitToCountries.count)) + } else if self.limitToCountries.count == 1, let countryCode = self.limitToCountries.first { + if countryCode == "FT" { + value = "Fragment" + } else { + value = self.currentLocale?.localizedString(forRegionCode: countryCode) ?? countryCode + } + } else { + value = "" + } + + pollSettingsSectionItems.append(AnyComponentWithIdentity(id: "countries", component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreatePoll_AllowedCountries, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 2 + ))) + ], alignment: .left, spacing: 4.0)), + verticalAlignment: .middle, + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent( + Image(image: self.cachedEmptyIcon, size: CGSize(width: 30.0, height: 30.0)) + )), false), + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: value, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )))), + accessory: .arrow, + action: { [weak self] view in + guard let self else { + return + } + self.deactivateInput() + self.openCountriesSelection() + } + )))) + } + } let pollSettingsSectionSize = self.pollSettingsSection.update( transition: transition, @@ -2309,7 +2509,6 @@ final class ComposePollScreenComponent: Component { AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent( externalState: self.quizAnswerTextInputState, context: component.context, - style: .glass, theme: theme, strings: environment.strings, resetText: self.resetQuizAnswerText.flatMap { resetText in @@ -2581,31 +2780,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 +2814,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 +2825,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 +2847,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 +3084,49 @@ 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 = componentView.isQuiz ? presentationData.strings.CreatePoll_OptionsNeeded : presentationData.strings.CreatePoll_OptionsNeededOne + case .quizCorrectOptionNeeded: + title = nil + if componentView.effectiveIsMultiAnswer { + text = presentationData.strings.CreatePoll_QuizCorrectOptionNeededMultiple + } else { + text = presentationData.strings.CreatePoll_QuizCorrectOptionNeeded + } + case .countriesNeeded: + title = nil + text = presentationData.strings.CreatePoll_QuizCountryNeeded } + + 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/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift index 876b0964dc..c38c24e3aa 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/PollAttachmentScreen.swift @@ -39,10 +39,7 @@ public func presentPollAttachmentScreen( chatLocation: nil, isScheduledMessages: false, buttons: availableButtons, - initialButton: .gallery, - makeEntityInputView: { - return nil - } + initialButton: .gallery ) let inputMediaNodeDataPromise = Promise(nil) if let inputMediaNodeData { @@ -64,7 +61,7 @@ public func presentPollAttachmentScreen( |> map(Optional.init) )) } - + attachmentController.requestController = { [weak attachmentController] type, controllerCompletion in let mediaPickerPollSubject: MediaPickerScreenImpl.Subject.AssetsMode.PollMode let filePickerPollSubject: AttachmentFileControllerSource.PollMode @@ -87,7 +84,7 @@ public func presentPollAttachmentScreen( locationPickerPollSubject = .option stickerPickerPollSubject = .option } - + switch type { case .gallery: let controller = MediaPickerScreenImpl( @@ -122,7 +119,7 @@ public func presentPollAttachmentScreen( bannedSendMedia: nil, presentGallery: { [weak attachmentController] in attachmentController?.dismiss(animated: true) - + let controller = MediaPickerScreenImpl( context: context, updatedPresentationData: updatedPresentationData, @@ -150,7 +147,7 @@ public func presentPollAttachmentScreen( }, presentFiles: { [weak attachmentController] in attachmentController?.dismiss(animated: true) - + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let controller = legacyICloudFilePicker(theme: presentationData.theme, documentTypes: ["public.item"], completion: { urls in guard let url = urls.first else { @@ -172,7 +169,7 @@ public func presentPollAttachmentScreen( if let audioMetadata = item.audioMetadata { attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil)) } - + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes, alternativeRepresentations: []) completion(.standalone(media: file)) }) diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/StickerAttachmentScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/StickerAttachmentScreen.swift index 65fcfc9330..7ea3af3a22 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/StickerAttachmentScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/StickerAttachmentScreen.swift @@ -88,6 +88,7 @@ final class StickerAttachmentScreenComponent: Component { var id: AnyHashable var version: Int var isPreset: Bool + var canLoadMore: Bool } private struct EmojiSearchState { @@ -107,6 +108,7 @@ final class StickerAttachmentScreenComponent: Component { self.emojiSearchState.set(.single(self.emojiSearchStateValue)) } } + private var emojiSearchContext: EmojiSearchContext? private let stickerSearchDisposable = MetaDisposable() private let stickerSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) @@ -307,7 +309,7 @@ final class StickerAttachmentScreenComponent: Component { if installed { return .complete() } else { - return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) + return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) |> map { _ in return Void() } } case .fetching: break @@ -390,15 +392,18 @@ final class StickerAttachmentScreenComponent: Component { switch query { case .none: + self.emojiSearchContext = nil self.emojiSearchDisposable.set(nil) - self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) case let .text(rawQuery, languageCode): let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) if query.isEmpty { + self.emojiSearchContext = nil self.emojiSearchDisposable.set(nil) - self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) } else { + self.emojiSearchContext = nil var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) if !languageCode.lowercased().hasPrefix("en") { signal = signal @@ -418,24 +423,25 @@ final class StickerAttachmentScreenComponent: Component { signal, hasPremium ) - |> mapToSignal { keywords, hasPremium -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + |> mapToSignal { keywords, hasPremium -> Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?), NoError> in var allEmoticons: [String: String] = [:] for keyword in keywords { for emoticon in keyword.emoticons { allEmoticons[emoticon] = keyword.keyword } } - let remoteSignal: Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError> + let remoteSignal: Signal + let emojiSearchContext: EmojiSearchContext? if hasPremium { - remoteSignal = context.engine.stickers.searchEmoji(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode) + let currentEmojiSearchContext = context.engine.stickers.emojiSearchContext(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode) + emojiSearchContext = currentEmojiSearchContext + remoteSignal = currentEmojiSearchContext.state } else { - remoteSignal = .single(([], true)) + emojiSearchContext = nil + remoteSignal = .single(EmojiSearchContext.State(items: [], canLoadMore: false, isLoadingMore: false)) } return remoteSignal - |> mapToSignal { foundEmoji -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in - if foundEmoji.items.isEmpty && !foundEmoji.isFinalResult { - return .complete() - } + |> map { foundEmoji -> (groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?) in var items: [EmojiPagerContentComponent.Item] = [] let appendUnicodeEmoji = { @@ -485,7 +491,7 @@ final class StickerAttachmentScreenComponent: Component { appendUnicodeEmoji() } - return .single([EmojiPagerContentComponent.ItemGroup( + return ([EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, @@ -502,7 +508,7 @@ final class StickerAttachmentScreenComponent: Component { headerItem: nil, fillWithLoadingPlaceholders: false, items: items - )]) + )], foundEmoji.canLoadMore, foundEmoji.items.isEmpty && foundEmoji.isLoadingMore, emojiSearchContext) } } @@ -515,11 +521,13 @@ final class StickerAttachmentScreenComponent: Component { return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + self.emojiSearchContext = result.searchContext + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.groups, id: AnyHashable(query), version: version, isPreset: false, canLoadMore: result.canLoadMore), isSearching: result.isSearching) version += 1 })) } case let .category(value): + self.emojiSearchContext = nil let resultSignal = context.engine.stickers.searchEmoji(category: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -592,10 +600,10 @@ final class StickerAttachmentScreenComponent: Component { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) version += 1 })) } @@ -607,6 +615,9 @@ final class StickerAttachmentScreenComponent: Component { // self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) }, onScroll: {}, + loadMore: { [weak self] in + self?.emojiSearchContext?.loadMore() + }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, @@ -708,7 +719,7 @@ final class StickerAttachmentScreenComponent: Component { if installed { return .complete() } else { - return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) + return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) |> map { _ in return Void() } } case .fetching: break @@ -865,10 +876,10 @@ final class StickerAttachmentScreenComponent: Component { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) return } - strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) version += 1 })) } @@ -926,7 +937,7 @@ final class StickerAttachmentScreenComponent: Component { } let defaultSearchState: EmojiPagerContentComponent.SearchState = emojiSearchResult.isPreset ? .active : .empty(hasResults: true) - emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState) + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState, canLoadMore: emojiSearchResult.canLoadMore) } else if emojiSearchState.isSearching { emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiContent.contentItemGroups, itemContentUniqueId: emojiContent.itemContentUniqueId, emptySearchResults: emojiContent.emptySearchResults, searchState: .searching) } diff --git a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift index 7a8f2eeadf..5eb51a3f78 100644 --- a/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift +++ b/submodules/TelegramUI/Components/ComposeTodoScreen/Sources/ComposeTodoScreen.swift @@ -3,7 +3,6 @@ import UIKit import Display import AccountContext import TelegramCore -import Postbox import SwiftSignalKit import TelegramPresentationData import ComponentFlow @@ -205,7 +204,7 @@ final class ComposeTodoScreenComponent: Component { for (id, itemView) in self.todoItemsSectionContainer.itemViews { if let view = itemView.contents.view as? ListComposePollOptionComponent.View, !view.isRevealed && !view.currentText.isEmpty { let viewFrame = view.convert(view.bounds, to: self.todoItemsSectionContainer) - let iconFrame = CGRect(origin: CGPoint(x: viewFrame.maxX - 40.0, 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) } @@ -471,7 +470,6 @@ final class ComposeTodoScreenComponent: Component { mode: .standard(.default), chatLocation: .peer(id: component.context.account.peerId), subject: nil, - peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, @@ -786,7 +784,6 @@ final class ComposeTodoScreenComponent: Component { todoTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent( externalState: self.todoTextInputState, context: component.context, - style: .glass, theme: theme, strings: environment.strings, isEnabled: canEdit, @@ -876,7 +873,6 @@ final class ComposeTodoScreenComponent: Component { todoItemsSectionItems.append(AnyComponentWithIdentity(id: todoItem.id, component: AnyComponent(ListComposePollOptionComponent( externalState: todoItem.textInputState, context: component.context, - style: .glass, theme: theme, strings: environment.strings, isEnabled: isEnabled, @@ -976,6 +972,12 @@ final class ComposeTodoScreenComponent: Component { } } }, + present: { [weak self] c in + guard let controller = self?.environment?.controller() else { + return + } + controller.present(c, in: .window(.root)) + }, tag: todoItem.textFieldTag )))) diff --git a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift index f4dffd096e..834c0c56fc 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() @@ -579,7 +583,7 @@ final class NewContactScreenComponent: Component { return } if case let .peer(peer, _) = self.resolvedPeer { - if let infoController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let infoController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { if let navigationController = component.context.sharedContext.mainWindow?.viewController as? NavigationController { navigationController.pushViewController(infoController) } @@ -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( @@ -713,7 +716,6 @@ final class NewContactScreenComponent: Component { ListComposePollOptionComponent( externalState: nil, context: component.context, - style: .glass, theme: theme, strings: strings, placeholder: NSAttributedString(string: strings.AddContact_NotePlaceholder, font: Font.regular(17.0), textColor: theme.list.itemPlaceholderTextColor), @@ -1033,22 +1035,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/ContentReportScreen/BUILD b/submodules/TelegramUI/Components/ContentReportScreen/BUILD index 4f552545f4..b96a7ef898 100644 --- a/submodules/TelegramUI/Components/ContentReportScreen/BUILD +++ b/submodules/TelegramUI/Components/ContentReportScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift index 0ef201b886..40aa79c5bf 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift @@ -424,6 +424,12 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode { self.text = self.presentationData.strings.Camera_CollageReorderingInfo self.targetSelectionIndex = nil icon = UIImage(bundleImageName: "Chat/Context Menu/Tip") + case .deleteReaction: + self.action = nil + self.text = self.presentationData.strings.Chat_DeleteReactionInfo + self.targetSelectionIndex = nil + icon = nil + isUserInteractionEnabled = false } self.iconNode = ASImageNode() diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift index 7c1fa64281..d65a39fad8 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextControllerExtractedPresentationNode.swift @@ -134,17 +134,18 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo private final class ItemContentNode: ASDisplayNode { let offsetContainerNode: ASDisplayNode var containingItem: ContextControllerTakeViewInfo.ContainingItem - + var animateClippingFromContentAreaInScreenSpace: CGRect? var storedGlobalFrame: CGRect? var storedGlobalBoundsFrame: CGRect? - + var presentationScale: CGFloat = 1.0 + init(containingItem: ContextControllerTakeViewInfo.ContainingItem) { self.offsetContainerNode = ASDisplayNode() self.containingItem = containingItem - + super.init() - + self.addSubnode(self.offsetContainerNode) } @@ -633,6 +634,21 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } let contentNodeValue = ItemContentNode(containingItem: takeInfo.containingItem) contentNodeValue.animateClippingFromContentAreaInScreenSpace = takeInfo.contentAreaInScreenSpace + + // Mirror any ancestor scale on the source (e.g. a sheet's container transform) onto the offset + // container so the extracted contents render at the same visual size as in-place — without this + // they pop to 1:1 when reparented into the unscaled overlay window. + let sourceView = takeInfo.containingItem.view + let modeledWidth = sourceView.bounds.width + if modeledWidth > 0.001 { + let visualWidth = sourceView.convert(sourceView.bounds, to: nil).width + let detectedScale = visualWidth / modeledWidth + if abs(detectedScale - 1.0) > 0.001 { + contentNodeValue.presentationScale = detectedScale + contentNodeValue.offsetContainerNode.layer.transform = CATransform3DMakeScale(detectedScale, detectedScale, 1.0) + } + } + self.scrollNode.insertSubnode(contentNodeValue, aboveSubnode: self.actionsContainerNode) self.itemContentNode = contentNodeValue itemContentNode = contentNodeValue @@ -818,7 +834,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if let transitionInfo = reference.transitionInfo() { if let referenceView = transitionInfo.referenceView as? ContextExtractableContainer { if #available(iOS 26.2, *) { - if transitionInfo.referenceView.bounds.width == transitionInfo.referenceView.bounds.height { + if !"".isEmpty, transitionInfo.referenceView.bounds.width == transitionInfo.referenceView.bounds.height { contextExtractableContainer = (referenceView, convertFrame(transitionInfo.referenceView.bounds.inset(by: transitionInfo.insets), from: transitionInfo.referenceView, to: self.view)) } } @@ -1165,6 +1181,13 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo if let contentNode = itemContentNode { var contentFrame = CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX, y: contentRect.minY - contentNode.containingItem.contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentNode.containingItem.view.bounds.size) + // contentRect.minY was derived from storedGlobalFrame.maxY (visual) minus contentRect.height (modeled); + // when an ancestor scale is in effect those don't cancel cleanly, leaving a (1 - scale) * (cy + ch) + // residue that pulls the content upward. Add it back so the content lands at the source's visual Y. + if contentNode.presentationScale != 1.0 { + let cr = contentNode.containingItem.contentRect + contentFrame.origin.y += (1.0 - contentNode.presentationScale) * (cr.minY + cr.height) + } if case let .extracted(extracted) = self.source { if extracted.adjustContentHorizontally { contentFrame.origin.x = combinedActionsFrame.minX @@ -1542,6 +1565,12 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo switch result { case .default, .custom: animationInContentYDistance = currentContentLocalFrame.minY - currentContentScreenFrame.minY + // Same modeled-vs-visual mismatch as the static contentFrame compensation: contentRect.minY (used by + // currentContentLocalFrame) was derived with modeled height while the source-side reference uses visual + // height, leaving a `ch * (1 - scale)` residue that animates the content downward on dismiss. + if let contentNode = itemContentNode, contentNode.presentationScale != 1.0 { + animationInContentYDistance += contentNode.containingItem.contentRect.height * (1.0 - contentNode.presentationScale) + } case .dismissWithoutContent: animationInContentYDistance = 0.0 if let contentNode = itemContentNode { diff --git a/submodules/TelegramUI/Components/DCTAnimationCacheImpl/BUILD b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/BUILD new file mode 100644 index 0000000000..273ab8aead --- /dev/null +++ b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "DCTAnimationCacheImpl", + module_name = "DCTAnimationCacheImpl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/CryptoUtils:CryptoUtils", + "//submodules/ManagedFile:ManagedFile", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/AnimationCache/ImageDCT:ImageDCT", + "//third-party/subcodec:SubcodecObjC", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/DCTAnimationCacheImpl.swift b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/DCTAnimationCacheImpl.swift new file mode 100644 index 0000000000..cd2c8f6e37 --- /dev/null +++ b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/DCTAnimationCacheImpl.swift @@ -0,0 +1,1558 @@ +import Foundation +import UIKit +import SwiftSignalKit +import CryptoUtils +import ManagedFile +import Compression +import AnimationCache + +private let algorithm: compression_algorithm = COMPRESSION_LZFSE + +private func alignUp(size: Int, align: Int) -> Int { + precondition(((align - 1) & align) == 0, "Align must be a power of two") + + let alignmentMask = align - 1 + return (size + alignmentMask) & ~alignmentMask +} + +private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? { + if useTotalFileAllocatedSize { + let url = URL(fileURLWithPath: path) + if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .fileAllocatedSizeKey]))) { + if values.isRegularFile ?? false { + if let fileSize = values.fileAllocatedSize { + return Int64(fileSize) + } + } + } + } + + var value = stat() + if stat(path, &value) == 0 { + return value.st_size + } else { + return nil + } +} + +private func md5Hash(_ string: String) -> String { + let hashData = string.data(using: .utf8)!.withUnsafeBytes { bytes -> Data in + return CryptoMD5(bytes.baseAddress!, Int32(bytes.count)) + } + return hashData.withUnsafeBytes { bytes -> String in + let uintBytes = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + return String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", uintBytes[0], uintBytes[1], uintBytes[2], uintBytes[3], uintBytes[4], uintBytes[5], uintBytes[6], uintBytes[7], uintBytes[8], uintBytes[9], uintBytes[10], uintBytes[11], uintBytes[12], uintBytes[13], uintBytes[14], uintBytes[15]) + } +} + +private func itemSubpath(hashString: String, width: Int, height: Int) -> (directory: String, fileName: String) { + assert(hashString.count == 32) + var directory = "" + + for i in 0 ..< 1 { + if !directory.isEmpty { + directory.append("/") + } + directory.append(String(hashString[hashString.index(hashString.startIndex, offsetBy: i * 2) ..< hashString.index(hashString.startIndex, offsetBy: (i + 1) * 2)])) + } + + return (directory, "\(hashString)_\(width)x\(height)") +} + +private func roundUp(_ numToRound: Int, multiple: Int) -> Int { + if multiple == 0 { + return numToRound + } + + let remainder = numToRound % multiple + if remainder == 0 { + return numToRound; + } + + return numToRound + multiple - remainder +} + +private func compressData(data: Data, addSizeHeader: Bool = false) -> Data? { + let scratchData = malloc(compression_encode_scratch_buffer_size(algorithm))! + defer { + free(scratchData) + } + + let headerSize = addSizeHeader ? 4 : 0 + var compressedData = Data(count: headerSize + data.count + 16 * 1024) + let resultSize = compressedData.withUnsafeMutableBytes { buffer -> Int in + guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return 0 + } + + if addSizeHeader { + var decompressedSize: UInt32 = UInt32(data.count) + memcpy(bytes, &decompressedSize, 4) + } + + return data.withUnsafeBytes { sourceBuffer -> Int in + return compression_encode_buffer(bytes.advanced(by: headerSize), buffer.count - headerSize, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), sourceBuffer.count, scratchData, algorithm) + } + } + + if resultSize <= 0 { + return nil + } + compressedData.count = headerSize + resultSize + return compressedData +} + +private func decompressData(data: Data, range: Range, decompressedSize: Int) -> Data? { + let scratchData = malloc(compression_decode_scratch_buffer_size(algorithm))! + defer { + free(scratchData) + } + + var decompressedFrameData = Data(count: decompressedSize) + let resultSize = decompressedFrameData.withUnsafeMutableBytes { buffer -> Int in + guard let bytes = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return 0 + } + return data.withUnsafeBytes { sourceBuffer -> Int in + return compression_decode_buffer(bytes, buffer.count, sourceBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: range.lowerBound), range.upperBound - range.lowerBound, scratchData, algorithm) + } + } + + if resultSize <= 0 { + return nil + } + if decompressedFrameData.count != resultSize { + decompressedFrameData.count = resultSize + } + return decompressedFrameData +} + +private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { + enum WriteError: Error { + case generic + } + + struct CompressedResult { + var animationPath: String + } + + private struct FrameMetadata { + var duration: Double + } + + var queue: Queue { + return self.innerQueue + } + let innerQueue: Queue + var isCancelled: Bool = false + + private let compressedPath: String + private var file: ManagedFile? + private var compressedWriter: CompressedFileWriter? + private let completion: (CompressedResult?) -> Void + + + private var currentSurface: ImageARGB? + private var currentYUVASurface: ImageYUVA420? + private var currentFrameFloat: FloatCoefficientsYUVA420? + private var previousFrameCoefficients: DctCoefficientsYUVA420? + private var deltaFrameFloat: FloatCoefficientsYUVA420? + private var previousYUVASurface: ImageYUVA420? + private var currentDctData: DctData? + private var differenceCoefficients: DctCoefficientsYUVA420? + private var currentDctCoefficients: DctCoefficientsYUVA420? + private var contentLengthOffset: Int? + private var isFailed: Bool = false + private var isFinished: Bool = false + + private var frames: [FrameMetadata] = [] + + private let dctQualityLuma: Int + private let dctQualityChroma: Int + private let dctQualityDelta: Int + + private let lock = Lock() + + init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) { + self.dctQualityLuma = 70 + self.dctQualityChroma = 88 + self.dctQualityDelta = 22 + + self.innerQueue = queue + self.compressedPath = allocateTempFile() + + guard let file = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else { + return nil + } + self.file = file + self.compressedWriter = CompressedFileWriter(file: file) + self.completion = completion + } + + func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) { + do { + try self.lock.throwingLocked { + let width = roundUp(proposedWidth, multiple: 16) + let height = roundUp(proposedHeight, multiple: 16) + + let surface: ImageARGB + if let current = self.currentSurface { + if current.argbPlane.width == width && current.argbPlane.height == height { + surface = current + surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Void in + memset(bytes.baseAddress!, 0, bytes.count) + } + } else { + self.isFailed = true + return + } + } else { + surface = ImageARGB(width: width, height: height, rowAlignment: 32) + self.currentSurface = surface + } + + let duration = surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Double? in + return drawingBlock(AnimationCacheItemDrawingSurface( + argb: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), + width: width, + height: height, + bytesPerRow: surface.argbPlane.bytesPerRow, + length: bytes.count + )) + } + + guard let duration = duration else { + return + } + + try addInternal(with: { yuvaSurface in + surface.toYUVA420(target: yuvaSurface) + + return duration + }, width: width, height: height, insertKeyframe: insertKeyframe) + } + } catch { + } + } + + func addYUV(with drawingBlock: (ImageYUVA420) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) throws { + let width = roundUp(proposedWidth, multiple: 16) + let height = roundUp(proposedHeight, multiple: 16) + + do { + try self.lock.throwingLocked { + try addInternal(with: { yuvaSurface in + return drawingBlock(yuvaSurface) + }, width: width, height: height, insertKeyframe: insertKeyframe) + } + } catch { + } + } + + func addInternal(with drawingBlock: (ImageYUVA420) -> Double?, width: Int, height: Int, insertKeyframe: Bool) throws { + if width == 0 || height == 0 { + self.isFailed = true + throw WriteError.generic + } + if self.isFailed || self.isFinished { + throw WriteError.generic + } + + guard !self.isFailed, !self.isFinished, let file = self.file, let compressedWriter = self.compressedWriter else { + throw WriteError.generic + } + + var isFirstFrame = false + + let yuvaSurface: ImageYUVA420 + if let current = self.currentYUVASurface { + if current.yPlane.width == width && current.yPlane.height == height { + yuvaSurface = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + isFirstFrame = true + + yuvaSurface = ImageYUVA420(width: width, height: height, rowAlignment: nil) + self.currentYUVASurface = yuvaSurface + } + + let currentFrameFloat: FloatCoefficientsYUVA420 + if let current = self.currentFrameFloat { + if current.yPlane.width == width && current.yPlane.height == height { + currentFrameFloat = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + currentFrameFloat = FloatCoefficientsYUVA420(width: width, height: height) + self.currentFrameFloat = currentFrameFloat + } + + let previousFrameCoefficients: DctCoefficientsYUVA420 + if let current = self.previousFrameCoefficients { + if current.yPlane.width == width && current.yPlane.height == height { + previousFrameCoefficients = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + previousFrameCoefficients = DctCoefficientsYUVA420(width: width, height: height) + self.previousFrameCoefficients = previousFrameCoefficients + } + + let deltaFrameFloat: FloatCoefficientsYUVA420 + if let current = self.deltaFrameFloat { + if current.yPlane.width == width && current.yPlane.height == height { + deltaFrameFloat = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + deltaFrameFloat = FloatCoefficientsYUVA420(width: width, height: height) + self.deltaFrameFloat = deltaFrameFloat + } + + let dctData: DctData + if let current = self.currentDctData { + dctData = current + } else { + dctData = DctData(generatingTablesAtQualityLuma: self.dctQualityLuma, chroma: self.dctQualityChroma, delta: self.dctQualityDelta) + self.currentDctData = dctData + } + + let duration = drawingBlock(yuvaSurface) + + guard let duration = duration else { + return + } + + let dctCoefficients: DctCoefficientsYUVA420 + if let current = self.currentDctCoefficients { + if current.yPlane.width == width && current.yPlane.height == height { + dctCoefficients = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + dctCoefficients = DctCoefficientsYUVA420(width: width, height: height) + self.currentDctCoefficients = dctCoefficients + } + + let differenceCoefficients: DctCoefficientsYUVA420 + if let current = self.differenceCoefficients { + if current.yPlane.width == width && current.yPlane.height == height { + differenceCoefficients = current + } else { + self.isFailed = true + throw WriteError.generic + } + } else { + differenceCoefficients = DctCoefficientsYUVA420(width: width, height: height) + self.differenceCoefficients = differenceCoefficients + } + + #if !arch(arm64) + var insertKeyframe = insertKeyframe + insertKeyframe = true + #endif + + let previousYUVASurface: ImageYUVA420 + if let current = self.previousYUVASurface { + previousYUVASurface = current + } else { + previousYUVASurface = ImageYUVA420(width: dctCoefficients.yPlane.width, height: dctCoefficients.yPlane.height, rowAlignment: nil) + self.previousYUVASurface = previousYUVASurface + } + + let isKeyframe: Bool + if !isFirstFrame && !insertKeyframe { + isKeyframe = false + + //previous + delta = current + //delta = current - previous + yuvaSurface.toCoefficients(target: differenceCoefficients) + differenceCoefficients.subtract(other: previousFrameCoefficients) + differenceCoefficients.dct4x4(dctData: dctData, target: dctCoefficients) + + //previous + delta = current + dctCoefficients.idct4x4Add(dctData: dctData, target: previousFrameCoefficients) + //previousFrameCoefficients.add(other: differenceCoefficients) + } else { + isKeyframe = true + + yuvaSurface.dct8x8(dctData: dctData, target: dctCoefficients) + + dctCoefficients.idct8x8(dctData: dctData, target: yuvaSurface) + yuvaSurface.toCoefficients(target: previousFrameCoefficients) + } + + if isFirstFrame { + file.write(6 as UInt32) + + file.write(UInt32(dctCoefficients.yPlane.width)) + file.write(UInt32(dctCoefficients.yPlane.height)) + + let lumaDctTable = dctData.lumaTable.serializedData() + file.write(UInt32(lumaDctTable.count)) + let _ = file.write(lumaDctTable) + + let chromaDctTable = dctData.chromaTable.serializedData() + file.write(UInt32(chromaDctTable.count)) + let _ = file.write(chromaDctTable) + + let deltaDctTable = dctData.deltaTable.serializedData() + file.write(UInt32(deltaDctTable.count)) + let _ = file.write(deltaDctTable) + + self.contentLengthOffset = Int(file.position()) + file.write(0 as UInt32) + } + + do { + let frameLength = dctCoefficients.yPlane.data.count + dctCoefficients.uPlane.data.count + dctCoefficients.vPlane.data.count + dctCoefficients.aPlane.data.count + try compressedWriter.writeUInt32(UInt32(frameLength)) + + try compressedWriter.writeUInt32(isKeyframe ? 1 : 0) + + for i in 0 ..< 4 { + let dctPlane: DctCoefficientPlane + switch i { + case 0: + dctPlane = dctCoefficients.yPlane + case 1: + dctPlane = dctCoefficients.uPlane + case 2: + dctPlane = dctCoefficients.vPlane + case 3: + dctPlane = dctCoefficients.aPlane + default: + preconditionFailure() + } + + try compressedWriter.writeUInt32(UInt32(dctPlane.data.count)) + try dctPlane.data.withUnsafeBytes { bytes in + try compressedWriter.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) + } + } + + self.frames.append(FrameMetadata(duration: duration)) + } catch { + self.isFailed = true + throw WriteError.generic + } + } + + func finish() { + do { + let result = try self.finishInternal() + self.completion(result) + } catch { + } + } + + func finishInternal() throws -> CompressedResult? { + var shouldComplete = false + self.lock.locked { + if !self.isFinished { + self.isFinished = true + shouldComplete = true + + guard let contentLengthOffset = self.contentLengthOffset, let file = self.file, let compressedWriter = self.compressedWriter else { + self.isFailed = true + return + } + assert(contentLengthOffset >= 0) + + do { + try compressedWriter.flush() + + let metadataPosition = file.position() + let contentLength = Int(metadataPosition) - contentLengthOffset - 4 + let _ = file.seek(position: Int64(contentLengthOffset)) + file.write(UInt32(contentLength)) + + let _ = file.seek(position: metadataPosition) + file.write(UInt32(self.frames.count)) + for frame in self.frames { + file.write(Float32(frame.duration)) + } + + if !self.isFailed { + self.compressedWriter = nil + self.file = nil + + file._unsafeClose() + } + } catch { + self.isFailed = true + } + } + } + + if shouldComplete { + if !self.isFailed { + return CompressedResult(animationPath: self.compressedPath) + } else { + let _ = try? FileManager.default.removeItem(atPath: self.compressedPath) + return nil + } + } else { + return nil + } + } +} + +private final class AnimationCacheItemAccessor { + private enum ReadError: Error { + case generic + } + + final class CurrentFrame { + let index: Int + var remainingDuration: Double + let duration: Double + let yuva: ImageYUVA420 + + init(index: Int, duration: Double, yuva: ImageYUVA420) { + self.index = index + self.duration = duration + self.remainingDuration = duration + self.yuva = yuva + } + } + + struct FrameInfo { + let duration: Double + } + + private let data: Data + private var compressedDataReader: DecompressedData? + private let range: Range + private let frameMapping: [Int: FrameInfo] + private let width: Int + private let height: Int + private let durationMapping: [Double] + + private var currentFrame: CurrentFrame? + + private var currentYUVASurface: ImageYUVA420? + private var currentCoefficients: DctCoefficientsYUVA420? + private let currentDctData: DctData + private var sharedDctCoefficients: DctCoefficientsYUVA420? + private var deltaCoefficients: DctCoefficientsYUVA420? + + init(data: Data, range: Range, frameMapping: [FrameInfo], width: Int, height: Int, dctData: DctData) { + self.data = data + self.range = range + self.width = width + self.height = height + + var resultFrameMapping: [Int: FrameInfo] = [:] + var durationMapping: [Double] = [] + + for i in 0 ..< frameMapping.count { + let frame = frameMapping[i] + resultFrameMapping[i] = frame + durationMapping.append(frame.duration) + } + + self.frameMapping = resultFrameMapping + self.durationMapping = durationMapping + + self.currentDctData = dctData + } + + private func loadNextFrame() -> Bool { + var didLoop = false + let index: Int + if let currentFrame = self.currentFrame { + if currentFrame.index + 1 >= self.durationMapping.count { + index = 0 + self.compressedDataReader = nil + didLoop = true + } else { + index = currentFrame.index + 1 + } + } else { + index = 0 + self.compressedDataReader = nil + } + + if self.compressedDataReader == nil { + self.compressedDataReader = DecompressedData(compressedData: self.data, dataRange: self.range) + } + + guard let compressedDataReader = self.compressedDataReader else { + self.currentFrame = nil + return didLoop + } + + do { + let frameLength = Int(try compressedDataReader.readUInt32()) + + let frameType = Int(try compressedDataReader.readUInt32()) + + let dctCoefficients: DctCoefficientsYUVA420 + if let sharedDctCoefficients = self.sharedDctCoefficients, sharedDctCoefficients.yPlane.width == self.width, sharedDctCoefficients.yPlane.height == self.height, !"".isEmpty { + dctCoefficients = sharedDctCoefficients + } else { + dctCoefficients = DctCoefficientsYUVA420(width: self.width, height: self.height) + self.sharedDctCoefficients = dctCoefficients + } + + var frameOffset = 0 + for i in 0 ..< 4 { + let planeLength = Int(try compressedDataReader.readUInt32()) + if planeLength < 0 || planeLength > 20 * 1024 * 1024 { + throw ReadError.generic + } + + let plane: DctCoefficientPlane + switch i { + case 0: + plane = dctCoefficients.yPlane + case 1: + plane = dctCoefficients.uPlane + case 2: + plane = dctCoefficients.vPlane + case 3: + plane = dctCoefficients.aPlane + default: + throw ReadError.generic + } + + if planeLength != plane.data.count { + throw ReadError.generic + } + + if frameOffset + plane.data.count > frameLength { + throw ReadError.generic + } + + try plane.data.withUnsafeMutableBytes { bytes in + try compressedDataReader.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) + } + frameOffset += plane.data.count + } + + let yuvaSurface: ImageYUVA420 + if let currentYUVASurface = self.currentYUVASurface { + yuvaSurface = currentYUVASurface + } else { + yuvaSurface = ImageYUVA420(width: dctCoefficients.yPlane.width, height: dctCoefficients.yPlane.height, rowAlignment: nil) + } + + let currentCoefficients: DctCoefficientsYUVA420 + if let current = self.currentCoefficients { + currentCoefficients = current + } else { + currentCoefficients = DctCoefficientsYUVA420(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height) + self.currentCoefficients = currentCoefficients + } + + /*let deltaCoefficients: DctCoefficientsYUVA420 + if let current = self.deltaCoefficients { + deltaCoefficients = current + } else { + deltaCoefficients = DctCoefficientsYUVA420(width: yuvaSurface.yPlane.width, height: yuvaSurface.yPlane.height) + self.deltaCoefficients = deltaCoefficients + }*/ + + switch frameType { + case 1: + dctCoefficients.idct8x8(dctData: self.currentDctData, target: yuvaSurface) + yuvaSurface.toCoefficients(target: currentCoefficients) + default: + dctCoefficients.idct4x4Add(dctData: self.currentDctData, target: currentCoefficients) + //currentCoefficients.add(other: deltaCoefficients) + + currentCoefficients.toYUVA420(target: yuvaSurface) + } + + self.currentFrame = CurrentFrame(index: index, duration: self.durationMapping[index], yuva: yuvaSurface) + } catch { + self.currentFrame = nil + self.compressedDataReader = nil + } + + return didLoop + } + + func reset() { + self.currentFrame = nil + } + + func advance(advance: AnimationCacheItem.Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItem.AdvanceResult? { + var didLoop = false + switch advance { + case let .frames(count): + for _ in 0 ..< count { + if self.loadNextFrame() { + didLoop = true + } + } + case let .duration(duration): + var durationOverflow = duration + while true { + if let currentFrame = self.currentFrame { + currentFrame.remainingDuration -= durationOverflow + if currentFrame.remainingDuration <= 0.0 { + durationOverflow = -currentFrame.remainingDuration + if self.loadNextFrame() { + didLoop = true + } + } else { + break + } + } else { + if self.loadNextFrame() { + didLoop = true + } + break + } + } + } + + guard let currentFrame = self.currentFrame else { + return nil + } + + switch requestedFormat { + case .rgba: + let currentSurface = ImageARGB(width: currentFrame.yuva.yPlane.width, height: currentFrame.yuva.yPlane.height, rowAlignment: 32) + currentFrame.yuva.toARGB(target: currentSurface) + + return AnimationCacheItem.AdvanceResult( + frame: AnimationCacheItemFrame(format: .rgba(data: currentSurface.argbPlane.data, width: currentSurface.argbPlane.width, height: currentSurface.argbPlane.height, bytesPerRow: currentSurface.argbPlane.bytesPerRow), duration: currentFrame.duration), + didLoop: didLoop + ) + case .yuva: + return AnimationCacheItem.AdvanceResult( + frame: AnimationCacheItemFrame( + format: .yuva( + y: AnimationCacheItemFrame.Plane( + data: currentFrame.yuva.yPlane.data, + width: currentFrame.yuva.yPlane.width, + height: currentFrame.yuva.yPlane.height, + bytesPerRow: currentFrame.yuva.yPlane.bytesPerRow + ), + u: AnimationCacheItemFrame.Plane( + data: currentFrame.yuva.uPlane.data, + width: currentFrame.yuva.uPlane.width, + height: currentFrame.yuva.uPlane.height, + bytesPerRow: currentFrame.yuva.uPlane.bytesPerRow + ), + v: AnimationCacheItemFrame.Plane( + data: currentFrame.yuva.vPlane.data, + width: currentFrame.yuva.vPlane.width, + height: currentFrame.yuva.vPlane.height, + bytesPerRow: currentFrame.yuva.vPlane.bytesPerRow + ), + a: AnimationCacheItemFrame.Plane( + data: currentFrame.yuva.aPlane.data, + width: currentFrame.yuva.aPlane.width, + height: currentFrame.yuva.aPlane.height, + bytesPerRow: currentFrame.yuva.aPlane.bytesPerRow + ) + ), + duration: currentFrame.duration + ), + didLoop: didLoop + ) + } + } +} + +private func readData(data: Data, offset: Int, count: Int) -> Data { + var result = Data(count: count) + result.withUnsafeMutableBytes { bytes -> Void in + data.withUnsafeBytes { dataBytes -> Void in + memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), count) + } + } + return result +} + +private func readUInt32(data: Data, offset: Int) -> UInt32 { + var value: UInt32 = 0 + withUnsafeMutableBytes(of: &value, { bytes -> Void in + data.withUnsafeBytes { dataBytes -> Void in + memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) + } + }) + + return value +} + +private func readFloat32(data: Data, offset: Int) -> Float32 { + var value: Float32 = 0 + withUnsafeMutableBytes(of: &value, { bytes -> Void in + data.withUnsafeBytes { dataBytes -> Void in + memcpy(bytes.baseAddress!, dataBytes.baseAddress!.advanced(by: offset), 4) + } + }) + + return value +} + +private func writeUInt32(data: inout Data, value: UInt32) { + var value: UInt32 = value + withUnsafeBytes(of: &value, { bytes -> Void in + data.count += 4 + data.withUnsafeMutableBytes { dataBytes -> Void in + memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) + } + }) +} + +private func writeFloat32(data: inout Data, value: Float32) { + var value: Float32 = value + withUnsafeBytes(of: &value, { bytes -> Void in + data.count += 4 + data.withUnsafeMutableBytes { dataBytes -> Void in + memcpy(dataBytes.baseAddress!.advanced(by: dataBytes.count - 4), bytes.baseAddress!, 4) + } + }) +} + +private final class CompressedFileWriter { + enum WriteError: Error { + case generic + } + + private let file: ManagedFile + private let stream: UnsafeMutablePointer + + private let tempOutputBufferSize: Int = 64 * 1024 + private let tempOutputBuffer: UnsafeMutablePointer + private let tempInputBufferCapacity: Int = 64 * 1024 + private let tempInputBuffer: UnsafeMutablePointer + private var tempInputBufferSize: Int = 0 + + private var didFail: Bool = false + + init?(file: ManagedFile) { + self.file = file + + self.stream = UnsafeMutablePointer.allocate(capacity: 1) + guard compression_stream_init(self.stream, COMPRESSION_STREAM_ENCODE, algorithm) != COMPRESSION_STATUS_ERROR else { + self.stream.deallocate() + return nil + } + + self.tempOutputBuffer = UnsafeMutablePointer.allocate(capacity: self.tempOutputBufferSize) + self.tempInputBuffer = UnsafeMutablePointer.allocate(capacity: self.tempInputBufferCapacity) + } + + deinit { + compression_stream_destroy(self.stream) + self.stream.deallocate() + self.tempOutputBuffer.deallocate() + self.tempInputBuffer.deallocate() + } + + private func flushBuffer() throws { + if self.didFail { + throw WriteError.generic + } + + if self.tempInputBufferSize <= 0 { + return + } + + self.stream.pointee.src_ptr = UnsafePointer(self.tempInputBuffer) + self.stream.pointee.src_size = self.tempInputBufferSize + + while true { + self.stream.pointee.dst_ptr = self.tempOutputBuffer + self.stream.pointee.dst_size = self.tempOutputBufferSize + + let status = compression_stream_process(self.stream, 0) + if status == COMPRESSION_STATUS_ERROR { + self.didFail = true + throw WriteError.generic + } + + let writtenBytes = self.tempOutputBufferSize - self.stream.pointee.dst_size + if writtenBytes > 0 { + let _ = self.file.write(self.tempOutputBuffer, count: writtenBytes) + } + + if status == COMPRESSION_STATUS_END { + break + } else { + if self.stream.pointee.src_size == 0 { + break + } + } + } + + self.tempInputBufferSize = 0 + } + + func write(bytes: UnsafePointer, count: Int) throws { + var writtenBytes = 0 + while writtenBytes < count { + let availableBytes = self.tempInputBufferCapacity - self.tempInputBufferSize + if availableBytes == 0 { + try flushBuffer() + } else { + let writeCount = min(availableBytes, count - writtenBytes) + + memcpy(self.tempInputBuffer.advanced(by: self.tempInputBufferSize), bytes.advanced(by: writtenBytes), writeCount) + self.tempInputBufferSize += writeCount + writtenBytes += writeCount + } + } + } + + func flush() throws { + if self.didFail { + throw WriteError.generic + } + + try self.flushBuffer() + + while true { + self.stream.pointee.dst_ptr = self.tempOutputBuffer + self.stream.pointee.dst_size = self.tempOutputBufferSize + + let status = compression_stream_process(self.stream, Int32(COMPRESSION_STREAM_FINALIZE.rawValue)) + if status == COMPRESSION_STATUS_ERROR { + self.didFail = true + throw WriteError.generic + } + + let writtenBytes = self.tempOutputBufferSize - self.stream.pointee.dst_size + if writtenBytes > 0 { + let _ = self.file.write(self.tempOutputBuffer, count: writtenBytes) + } + + if status == COMPRESSION_STATUS_END { + break + } + } + } + + func writeUInt32(_ value: UInt32) throws { + var value: UInt32 = value + try withUnsafeBytes(of: &value, { bytes -> Void in + try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + } + + func writeFloat32(_ value: Float32) throws { + var value: Float32 = value + try withUnsafeBytes(of: &value, { bytes -> Void in + try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + } +} + +private final class DecompressedData { + enum ReadError: Error { + case didReadToEnd + } + + private let compressedData: Data + private let dataRange: Range + private let stream: UnsafeMutablePointer + private var isComplete = false + + init?(compressedData: Data, dataRange: Range) { + self.compressedData = compressedData + self.dataRange = dataRange + + self.stream = UnsafeMutablePointer.allocate(capacity: 1) + guard compression_stream_init(self.stream, COMPRESSION_STREAM_DECODE, algorithm) != COMPRESSION_STATUS_ERROR else { + self.stream.deallocate() + return nil + } + + self.compressedData.withUnsafeBytes { bytes in + self.stream.pointee.src_ptr = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: dataRange.lowerBound) + self.stream.pointee.src_size = dataRange.upperBound - dataRange.lowerBound + } + } + + deinit { + compression_stream_destroy(self.stream) + self.stream.deallocate() + } + + func read(bytes: UnsafeMutablePointer, count: Int) throws { + if self.isComplete { + throw ReadError.didReadToEnd + } + + self.stream.pointee.dst_ptr = bytes + self.stream.pointee.dst_size = count + + let status = compression_stream_process(self.stream, 0) + + if status == COMPRESSION_STATUS_ERROR { + self.isComplete = true + throw ReadError.didReadToEnd + } else if status == COMPRESSION_STATUS_END { + if self.stream.pointee.src_size == 0 { + self.isComplete = true + } + } + + if self.stream.pointee.dst_size != 0 { + throw ReadError.didReadToEnd + } + } + + func readUInt32() throws -> UInt32 { + var value: UInt32 = 0 + try withUnsafeMutableBytes(of: &value, { bytes -> Void in + try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + return value + } + + func readFloat32() throws -> Float32 { + var value: Float32 = 0 + try withUnsafeMutableBytes(of: &value, { bytes -> Void in + try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + return value + } +} + +private enum LoadItemError: Error { + case dataError +} + +private func loadItem(path: String) throws -> AnimationCacheItem { + guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else { + throw LoadItemError.dataError + } + + var offset: Int = 0 + let dataLength = compressedData.count + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let formatVersion = readUInt32(data: compressedData, offset: offset) + offset += 4 + if formatVersion != 6 { + throw LoadItemError.dataError + } + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let width = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let height = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let dctLumaTableLength = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + Int(dctLumaTableLength) > dataLength { + throw LoadItemError.dataError + } + let dctLumaData = readData(data: compressedData, offset: offset, count: Int(dctLumaTableLength)) + offset += Int(dctLumaTableLength) + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let dctChromaTableLength = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + Int(dctChromaTableLength) > dataLength { + throw LoadItemError.dataError + } + let dctChromaData = readData(data: compressedData, offset: offset, count: Int(dctChromaTableLength)) + offset += Int(dctChromaTableLength) + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let dctDeltaTableLength = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + Int(dctDeltaTableLength) > dataLength { + throw LoadItemError.dataError + } + let dctDeltaData = readData(data: compressedData, offset: offset, count: Int(dctDeltaTableLength)) + offset += Int(dctDeltaTableLength) + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let contentLength = Int(readUInt32(data: compressedData, offset: offset)) + offset += 4 + + let compressedFrameDataRange = offset ..< (offset + contentLength) + offset += contentLength + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let frameCount = Int(readUInt32(data: compressedData, offset: offset)) + offset += 4 + + var frameMapping: [AnimationCacheItemAccessor.FrameInfo] = [] + for _ in 0 ..< frameCount { + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let frameDuration = readFloat32(data: compressedData, offset: offset) + offset += 4 + + frameMapping.append(AnimationCacheItemAccessor.FrameInfo(duration: Double(frameDuration))) + } + + guard let dctData = DctData(lumaTable: dctLumaData, chromaTable: dctChromaData, deltaTable: dctDeltaData) else { + throw LoadItemError.dataError + } + + let itemAccessor = AnimationCacheItemAccessor(data: compressedData, range: compressedFrameDataRange, frameMapping: frameMapping, width: Int(width), height: Int(height), dctData: dctData) + + return AnimationCacheItem(numFrames: frameMapping.count, advanceImpl: { advance, requestedFormat in + return itemAccessor.advance(advance: advance, requestedFormat: requestedFormat) + }, resetImpl: { + itemAccessor.reset() + }) +} + +private func adaptItemFromHigherResolution(currentQueue: Queue, itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { + guard let higherResolutionItem = try? loadItem(path: higherResolutionPath) else { + return nil + } + guard let writer = AnimationCacheItemWriterImpl(queue: currentQueue, allocateTempFile: allocateTempFile, completion: { + _ in + }) else { + return nil + } + + do { + for _ in 0 ..< higherResolutionItem.numFrames { + try writer.addYUV(with: { yuva in + guard let frame = higherResolutionItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: yuva.yPlane.rowAlignment)) else { + return nil + } + switch frame.frame.format { + case .rgba: + return nil + case let .yuva(y, u, v, a): + yuva.yPlane.copyScaled(fromPlane: y) + yuva.uPlane.copyScaled(fromPlane: u) + yuva.vPlane.copyScaled(fromPlane: v) + yuva.aPlane.copyScaled(fromPlane: a) + } + + return frame.frame.duration + }, proposedWidth: width, proposedHeight: height, insertKeyframe: true) + } + + guard let result = try writer.finishInternal() else { + return nil + } + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + return nil + } + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { + return nil + } + if let size = fileSize(itemPath) { + updateStorageStats(itemPath, size) + } + + guard let item = try? loadItem(path: itemPath) else { + return nil + } + return item + } catch { + return nil + } +} + +private func generateFirstFrameFromItem(currentQueue: Queue, itemPath: String, animationItemPath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> Bool { + guard let animationItem = try? loadItem(path: animationItemPath) else { + return false + } + guard let writer = AnimationCacheItemWriterImpl(queue: currentQueue, allocateTempFile: allocateTempFile, completion: { _ in + }) else { + return false + } + + do { + for _ in 0 ..< min(1, animationItem.numFrames) { + guard let frame = animationItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: 1)) else { + return false + } + switch frame.frame.format { + case .rgba: + return false + case let .yuva(y, u, v, a): + try writer.addYUV(with: { yuva in + assert(yuva.yPlane.bytesPerRow == y.bytesPerRow) + assert(yuva.uPlane.bytesPerRow == u.bytesPerRow) + assert(yuva.vPlane.bytesPerRow == v.bytesPerRow) + assert(yuva.aPlane.bytesPerRow == a.bytesPerRow) + + yuva.yPlane.copyScaled(fromPlane: y) + yuva.uPlane.copyScaled(fromPlane: u) + yuva.vPlane.copyScaled(fromPlane: v) + yuva.aPlane.copyScaled(fromPlane: a) + + return frame.frame.duration + }, proposedWidth: y.width, proposedHeight: y.height, insertKeyframe: true) + } + } + + guard let result = try writer.finishInternal() else { + return false + } + + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { + return false + } + if let size = fileSize(itemPath) { + updateStorageStats(itemPath, size) + } + return true + } catch { + return false + } +} + +private func findHigherResolutionFileForAdaptation(itemDirectoryPath: String, baseName: String, baseSuffix: String, width: Int, height: Int) -> String? { + var candidates: [(path: String, width: Int, height: Int)] = [] + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: itemDirectoryPath), includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants, errorHandler: nil) { + for url in enumerator { + guard let url = url as? URL else { + continue + } + let fileName = url.lastPathComponent + if fileName.hasPrefix(baseName) { + let scanner = Scanner(string: fileName) + guard scanner.scanString(baseName) != nil else { + continue + } + guard let itemWidth = scanner.scanInt() else { + continue + } + guard scanner.scanString("x") != nil else { + continue + } + guard let itemHeight = scanner.scanInt() else { + continue + } + if !baseSuffix.isEmpty { + guard scanner.scanString(baseSuffix) != nil else { + continue + } + } + guard scanner.isAtEnd else { + continue + } + if itemWidth > width && itemHeight > height { + candidates.append((url.path, itemWidth, itemHeight)) + } + } + } + } + if !candidates.isEmpty { + candidates.sort(by: { $0.width < $1.width }) + return candidates[0].path + } + return nil +} + +public final class DCTAnimationCacheImpl: AnimationCache { + private final class Impl { + private struct ItemKey: Hashable { + var id: String + var width: Int + var height: Int + } + + private final class ItemContext { + let subscribers = Bag<(AnimationCacheItemResult) -> Void>() + let disposable = MetaDisposable() + + deinit { + self.disposable.dispose() + } + } + + private let queue: Queue + private let basePath: String + private let allocateTempFile: () -> String + private let updateStorageStats: (String, Int64) -> Void + + private let fetchQueues: [Queue] + private var nextFetchQueueIndex: Int = 0 + + private var itemContexts: [ItemKey: ItemContext] = [:] + + init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { + self.queue = queue + + let fetchQueueCount: Int + if ProcessInfo.processInfo.processorCount > 2 { + fetchQueueCount = 3 + } else { + fetchQueueCount = 2 + } + + self.fetchQueues = (0 ..< fetchQueueCount).map { i in Queue(name: "DCTAnimationCacheImpl-Fetch\(i)", qos: .default) } + self.basePath = basePath + self.allocateTempFile = allocateTempFile + self.updateStorageStats = updateStorageStats + } + + deinit { + } + + func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" + let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" + + if FileManager.default.fileExists(atPath: itemPath), let item = try? loadItem(path: itemPath) { + updateResult(AnimationCacheItemResult(item: item, isFinal: true)) + + return EmptyDisposable + } + let key = ItemKey(id: sourceId, width: Int(size.width), height: Int(size.height)) + + let itemContext: ItemContext + var beginFetch = false + if let current = self.itemContexts[key] { + itemContext = current + } else { + itemContext = ItemContext() + self.itemContexts[key] = itemContext + beginFetch = true + } + + let queue = self.queue + let index = itemContext.subscribers.add(updateResult) + + updateResult(AnimationCacheItemResult(item: nil, isFinal: false)) + + if beginFetch { + let fetchQueueIndex = self.nextFetchQueueIndex + self.nextFetchQueueIndex += 1 + let allocateTempFile = self.allocateTempFile + let updateStorageStats = self.updateStorageStats + guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in + queue.async { + guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { + return + } + + strongSelf.itemContexts.removeValue(forKey: key) + + guard let result = result else { + return + } + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + return + } + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { + return + } + if let size = fileSize(itemPath) { + updateStorageStats(itemPath, size) + } + + let _ = generateFirstFrameFromItem(currentQueue: queue, itemPath: itemFirstFramePath, animationItemPath: itemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) + + for f in itemContext.subscribers.copyItems() { + guard let item = try? loadItem(path: itemPath) else { + continue + } + f(AnimationCacheItemResult(item: item, isFinal: true)) + } + } + }) else { + return EmptyDisposable + } + + let fetchDisposable = MetaDisposable() + fetchDisposable.set(fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: false))) + + itemContext.disposable.set(ActionDisposable { [weak writer] in + if let writer = writer { + writer.isCancelled = true + } + + fetchDisposable.dispose() + }) + } + + return ActionDisposable { [weak self, weak itemContext] in + queue.async { + guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { + return + } + itemContext.subscribers.remove(index) + if itemContext.subscribers.isEmpty { + itemContext.disposable.dispose() + strongSelf.itemContexts.removeValue(forKey: key) + } + } + } + } + + static func getFirstFrameSynchronously(basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) -> AnimationCacheItem? { + let hashString = md5Hash(sourceId) + let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" + + if FileManager.default.fileExists(atPath: itemFirstFramePath) { + if let item = try? loadItem(path: itemFirstFramePath) { + return item + } + } + + if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { + if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { + return adaptedItem + } + } + + return nil + } + + static func getFirstFrame(queue: Queue, basePath: String, sourceId: String, size: CGSize, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let hashString = md5Hash(sourceId) + let sourceIdPath = itemSubpath(hashString: hashString, width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" + let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" + + if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = try? loadItem(path: itemFirstFramePath) { + completion(AnimationCacheItemResult(item: item, isFinal: true)) + return EmptyDisposable + } + + if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { + if let adaptedItem = adaptItemFromHigherResolution(currentQueue: .mainQueue(), itemPath: itemFirstFramePath, width: Int(size.width), height: Int(size.height), itemDirectoryPath: itemDirectoryPath, higherResolutionPath: adaptationItemPath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) { + completion(AnimationCacheItemResult(item: adaptedItem, isFinal: true)) + return EmptyDisposable + } + } + + if let fetch = fetch { + completion(AnimationCacheItemResult(item: nil, isFinal: false)) + + guard let writer = AnimationCacheItemWriterImpl(queue: queue, allocateTempFile: allocateTempFile, completion: { result in + queue.async { + guard let result = result else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + let _ = try? FileManager.default.removeItem(atPath: itemFirstFramePath) + guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemFirstFramePath) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + if let size = fileSize(itemFirstFramePath) { + updateStorageStats(itemFirstFramePath, size) + } + guard let item = try? loadItem(path: itemFirstFramePath) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + + completion(AnimationCacheItemResult(item: item, isFinal: true)) + } + }) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return EmptyDisposable + } + + let fetchDisposable = fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: true)) + return fetchDisposable + } else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return EmptyDisposable + } + } + } + + private let queue: Queue + private let basePath: String + private let impl: QueueLocalObject + private let allocateTempFile: () -> String + private let updateStorageStats: (String, Int64) -> Void + + public init(basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { + let queue = Queue() + self.queue = queue + self.basePath = basePath + self.allocateTempFile = allocateTempFile + self.updateStorageStats = updateStorageStats + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) + }) + } + + public func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + self.impl.with { impl in + disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { result in + subscriber.putNext(result) + if result.isFinal { + subscriber.putCompletion() + } + })) + } + + return disposable + } + |> runOn(self.queue) + } + + public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { + return Impl.getFirstFrameSynchronously(basePath: self.basePath, sourceId: sourceId, size: size, allocateTempFile: self.allocateTempFile, updateStorageStats: self.updateStorageStats) + } + + public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let disposable = MetaDisposable() + + let basePath = self.basePath + let allocateTempFile = self.allocateTempFile + let updateStorageStats = self.updateStorageStats + queue.async { + disposable.set(Impl.getFirstFrame(queue: queue, basePath: basePath, sourceId: sourceId, size: size, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats, fetch: fetch, completion: completion)) + } + + return disposable + } +} diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/ImageData.swift similarity index 99% rename from submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift rename to submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/ImageData.swift index 66c853510f..7dfaecfc0b 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/ImageData.swift +++ b/submodules/TelegramUI/Components/DCTAnimationCacheImpl/Sources/ImageData.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import AnimationCache import ImageDCT import Accelerate diff --git a/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/BUILD b/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/BUILD new file mode 100644 index 0000000000..6c27e9c86b --- /dev/null +++ b/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "DCTMultiAnimationRendererImpl", + module_name = "DCTMultiAnimationRendererImpl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/Sources/DCTMultiAnimationRendererImpl.swift b/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/Sources/DCTMultiAnimationRendererImpl.swift new file mode 100644 index 0000000000..8ab7c6f9d0 --- /dev/null +++ b/submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl/Sources/DCTMultiAnimationRendererImpl.swift @@ -0,0 +1,1078 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import AnimationCache +import MultiAnimationRenderer +import Accelerate +import IOSurface + +private final class LoadFrameGroupTask { + let task: () -> () -> Void + let queueAffinity: Int + + init(task: @escaping () -> () -> Void, queueAffinity: Int) { + self.task = task + self.queueAffinity = queueAffinity + } +} + +private var yuvToRgbConversion: vImage_YpCbCrToARGB = { + var info = vImage_YpCbCrToARGB() + var pixelRange = vImage_YpCbCrPixelRange(Yp_bias: 16, CbCr_bias: 128, YpRangeMax: 235, CbCrRangeMax: 240, YpMax: 255, YpMin: 0, CbCrMax: 255, CbCrMin: 0) + vImageConvert_YpCbCrToARGB_GenerateConversion(kvImage_YpCbCrToARGBMatrix_ITU_R_709_2, &pixelRange, &info, kvImage420Yp8_Cb8_Cr8, kvImageARGB8888, 0) + return info +}() + +private final class ItemAnimationContext { + fileprivate final class Frame { + let frame: AnimationCacheItemFrame + let duration: Double + + let contentsAsImage: UIImage? + let contentsAsCVPixelBuffer: CVPixelBuffer? + + let size: CGSize + + var remainingDuration: Double + + private var blurredRepresentationValue: UIImage? + + init?(frame: AnimationCacheItemFrame) { + self.frame = frame + self.duration = frame.duration + self.remainingDuration = frame.duration + + switch frame.format { + case let .rgba(data, width, height, bytesPerRow): + guard let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) else { + return nil + } + + data.withUnsafeBytes { bytes -> Void in + memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) + } + + guard let image = context.generateImage() else { + return nil + } + + self.contentsAsImage = image + self.contentsAsCVPixelBuffer = nil + self.size = CGSize(width: CGFloat(width), height: CGFloat(height)) + case let .yuva(y, u, v, a): + var pixelBuffer: CVPixelBuffer? = nil + let _ = CVPixelBufferCreate(kCFAllocatorDefault, y.width, y.height, kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar, [ + kCVPixelBufferIOSurfacePropertiesKey: NSDictionary() + ] as CFDictionary, &pixelBuffer) + guard let pixelBuffer else { + return nil + } + + CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + } + guard let baseAddressY = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else { + return nil + } + guard let baseAddressCbCr = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else { + return nil + } + guard let baseAddressA = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2) else { + return nil + } + + let dstBufferY = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressY), height: vImagePixelCount(y.height), width: vImagePixelCount(y.width), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)) + let dstBufferCbCr = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressCbCr), height: vImagePixelCount(y.height / 2), width: vImagePixelCount(y.width / 2), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)) + let dstBufferA = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressA), height: vImagePixelCount(y.height), width: vImagePixelCount(y.width), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2)) + + y.data.withUnsafeBytes { (yBytes: UnsafeRawBufferPointer) -> Void in + if dstBufferY.rowBytes == y.bytesPerRow { + memcpy(dstBufferY.data, yBytes.baseAddress!, yBytes.count) + } else { + for i in 0 ..< y.height { + memcpy(dstBufferY.data.advanced(by: dstBufferY.rowBytes * i), yBytes.baseAddress!.advanced(by: y.bytesPerRow * i), y.bytesPerRow) + } + } + } + + a.data.withUnsafeBytes { (aBytes: UnsafeRawBufferPointer) -> Void in + if dstBufferA.rowBytes == a.bytesPerRow { + memcpy(dstBufferA.data, aBytes.baseAddress!, aBytes.count) + } else { + for i in 0 ..< y.height { + memcpy(dstBufferA.data.advanced(by: dstBufferA.rowBytes * i), aBytes.baseAddress!.advanced(by: a.bytesPerRow * i), a.bytesPerRow) + } + } + } + + u.data.withUnsafeBytes { (uBytes: UnsafeRawBufferPointer) -> Void in + v.data.withUnsafeBytes { (vBytes: UnsafeRawBufferPointer) -> Void in + let sourceU = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: uBytes.baseAddress!), + height: vImagePixelCount(u.height), + width: vImagePixelCount(u.width), + rowBytes: u.bytesPerRow + ) + let sourceV = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: vBytes.baseAddress!), + height: vImagePixelCount(v.height), + width: vImagePixelCount(v.width), + rowBytes: v.bytesPerRow + ) + + withUnsafePointer(to: sourceU, { sourceU in + withUnsafePointer(to: sourceV, { sourceV in + var srcPlanarBuffers: [ + UnsafePointer? + ] = [sourceU, sourceV] + var destChannels: [UnsafeMutableRawPointer?] = [ + dstBufferCbCr.data.advanced(by: 1), + dstBufferCbCr.data + ] + + let channelCount = 2 + + vImageConvert_PlanarToChunky8( + &srcPlanarBuffers, + &destChannels, + UInt32(channelCount), + MemoryLayout.stride * channelCount, + vImagePixelCount(u.width), + vImagePixelCount(u.height), + dstBufferCbCr.rowBytes, + vImage_Flags(kvImageDoNotTile) + ) + }) + }) + } + } + + self.contentsAsImage = nil + self.contentsAsCVPixelBuffer = pixelBuffer + self.size = CGSize(width: CGFloat(y.width), height: CGFloat(y.height)) + } + } + + func blurredRepresentation(color: UIColor?) -> UIImage? { + if let blurredRepresentationValue = self.blurredRepresentationValue { + return blurredRepresentationValue + } + + switch frame.format { + case let .rgba(data, width, height, bytesPerRow): + let blurredWidth = 12 + let blurredHeight = 12 + guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: bytesPerRow) else { + return nil + } + + let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)) + + data.withUnsafeBytes { bytes -> Void in + if let dataProvider = CGDataProvider(dataInfo: nil, data: bytes.baseAddress!, size: bytes.count, releaseData: { _, _, _ in }) { + let image = CGImage( + width: width, + height: height, + bitsPerComponent: 8, + bitsPerPixel: 32, + bytesPerRow: bytesPerRow, + space: DeviceGraphicsContextSettings.shared.colorSpace, + bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo, + provider: dataProvider, + decode: nil, + shouldInterpolate: true, + intent: .defaultIntent + ) + if let image = image { + context.withFlippedContext { c in + c.setFillColor((color ?? .white).cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + c.draw(image, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8))) + } + } + } + + var destinationBuffer = vImage_Buffer() + destinationBuffer.width = UInt(blurredWidth) + destinationBuffer.height = UInt(blurredHeight) + destinationBuffer.data = context.bytes + destinationBuffer.rowBytes = context.bytesPerRow + + vImageBoxConvolve_ARGB8888(&destinationBuffer, + &destinationBuffer, + nil, + 0, 0, + UInt32(15), + UInt32(15), + nil, + vImage_Flags(kvImageTruncateKernel)) + + let divisor: Int32 = 0x1000 + + let rwgt: CGFloat = 0.3086 + let gwgt: CGFloat = 0.6094 + let bwgt: CGFloat = 0.0820 + + let adjustSaturation: CGFloat = 1.7 + + let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation + let b = (1.0 - adjustSaturation) * rwgt + let c = (1.0 - adjustSaturation) * rwgt + let d = (1.0 - adjustSaturation) * gwgt + let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation + let f = (1.0 - adjustSaturation) * gwgt + let g = (1.0 - adjustSaturation) * bwgt + let h = (1.0 - adjustSaturation) * bwgt + let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation + + let satMatrix: [CGFloat] = [ + a, b, c, 0, + d, e, f, 0, + g, h, i, 0, + 0, 0, 0, 1 + ] + + var matrix: [Int16] = satMatrix.map { value in + return Int16(value * CGFloat(divisor)) + } + + vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) + + context.withFlippedContext { c in + c.setFillColor((color ?? .white).withMultipliedAlpha(0.6).cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + } + } + + self.blurredRepresentationValue = context.generateImage() + return self.blurredRepresentationValue + case let .yuva(y, u, v, a): + let blurredWidth = 12 + let blurredHeight = 12 + let size = CGSize(width: blurredWidth, height: blurredHeight) + + var sourceY = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: y.data.withUnsafeBytes { $0.baseAddress! }), + height: vImagePixelCount(y.height), + width: vImagePixelCount(y.width), + rowBytes: y.bytesPerRow + ) + + var sourceU = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: u.data.withUnsafeBytes { $0.baseAddress! }), + height: vImagePixelCount(u.height), + width: vImagePixelCount(u.width), + rowBytes: u.bytesPerRow + ) + + var sourceV = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: v.data.withUnsafeBytes { $0.baseAddress! }), + height: vImagePixelCount(v.height), + width: vImagePixelCount(v.width), + rowBytes: v.bytesPerRow + ) + + var sourceA = vImage_Buffer( + data: UnsafeMutableRawPointer(mutating: a.data.withUnsafeBytes { $0.baseAddress! }), + height: vImagePixelCount(a.height), + width: vImagePixelCount(a.width), + rowBytes: a.bytesPerRow + ) + + let scaledYData = malloc(blurredWidth * blurredHeight)! + defer { + free(scaledYData) + } + + let scaledUData = malloc(blurredWidth * blurredHeight / 4)! + defer { + free(scaledUData) + } + + let scaledVData = malloc(blurredWidth * blurredHeight / 4)! + defer { + free(scaledVData) + } + + let scaledAData = malloc(blurredWidth * blurredHeight)! + defer { + free(scaledAData) + } + + var scaledY = vImage_Buffer( + data: scaledYData, + height: vImagePixelCount(blurredHeight), + width: vImagePixelCount(blurredWidth), + rowBytes: blurredWidth + ) + + var scaledU = vImage_Buffer( + data: scaledUData, + height: vImagePixelCount(blurredHeight / 2), + width: vImagePixelCount(blurredWidth / 2), + rowBytes: blurredWidth / 2 + ) + + var scaledV = vImage_Buffer( + data: scaledVData, + height: vImagePixelCount(blurredHeight / 2), + width: vImagePixelCount(blurredWidth / 2), + rowBytes: blurredWidth / 2 + ) + + var scaledA = vImage_Buffer( + data: scaledAData, + height: vImagePixelCount(blurredHeight), + width: vImagePixelCount(blurredWidth), + rowBytes: blurredWidth + ) + + vImageScale_Planar8(&sourceY, &scaledY, nil, vImage_Flags(kvImageHighQualityResampling)) + vImageScale_Planar8(&sourceU, &scaledU, nil, vImage_Flags(kvImageHighQualityResampling)) + vImageScale_Planar8(&sourceV, &scaledV, nil, vImage_Flags(kvImageHighQualityResampling)) + vImageScale_Planar8(&sourceA, &scaledA, nil, vImage_Flags(kvImageHighQualityResampling)) + + guard let context = DrawingContext(size: size, scale: 1.0, clear: true) else { + return nil + } + + var destinationBuffer = vImage_Buffer( + data: context.bytes, + height: vImagePixelCount(blurredHeight), + width: vImagePixelCount(blurredWidth), + rowBytes: context.bytesPerRow + ) + + var result = kvImageNoError + + var permuteMap: [UInt8] = [1, 2, 3, 0] + result = vImageConvert_420Yp8_Cb8_Cr8ToARGB8888(&scaledY, &scaledU, &scaledV, &destinationBuffer, &yuvToRgbConversion, &permuteMap, 255, vImage_Flags(kvImageDoNotTile)) + if result != kvImageNoError { + return nil + } + + result = vImageOverwriteChannels_ARGB8888(&scaledA, &destinationBuffer, &destinationBuffer, 1 << 0, vImage_Flags(kvImageDoNotTile)); + if result != kvImageNoError { + return nil + } + + vImageBoxConvolve_ARGB8888(&destinationBuffer, + &destinationBuffer, + nil, + 0, 0, + UInt32(15), + UInt32(15), + nil, + vImage_Flags(kvImageTruncateKernel)) + + let divisor: Int32 = 0x1000 + + let rwgt: CGFloat = 0.3086 + let gwgt: CGFloat = 0.6094 + let bwgt: CGFloat = 0.0820 + + let adjustSaturation: CGFloat = 1.7 + + let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation + let b = (1.0 - adjustSaturation) * rwgt + let c = (1.0 - adjustSaturation) * rwgt + let d = (1.0 - adjustSaturation) * gwgt + let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation + let f = (1.0 - adjustSaturation) * gwgt + let g = (1.0 - adjustSaturation) * bwgt + let h = (1.0 - adjustSaturation) * bwgt + let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation + + let satMatrix: [CGFloat] = [ + a, b, c, 0, + d, e, f, 0, + g, h, i, 0, + 0, 0, 0, 1 + ] + + var matrix: [Int16] = satMatrix.map { value in + return Int16(value * CGFloat(divisor)) + } + + vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) + + context.withFlippedContext { c in + c.setFillColor((color ?? .white).withMultipliedAlpha(0.6).cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + } + + self.blurredRepresentationValue = context.generateImage() + return self.blurredRepresentationValue + } + } + } + + static let queue0 = Queue(name: "ItemAnimationContext-0", qos: .default) + static let queue1 = Queue(name: "ItemAnimationContext-1", qos: .default) + + private let useYuvA: Bool + + private let cache: AnimationCache + let queueAffinity: Int + private let stateUpdated: () -> Void + + private var disposable: Disposable? + private var displayLink: ConstantDisplayLinkAnimator? + private var item: Atomic? + private var itemPlaceholderAndFrameIndex: (UIImage, Int)? + + private var currentFrame: Frame? + private var loadingFrameTaskId: Int? + private var nextLoadingFrameTaskId: Int = 0 + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + self.stateUpdated() + } + } + } + + let targets = Bag>() + + init(cache: AnimationCache, queueAffinity: Int, itemId: String, size: CGSize, useYuvA: Bool, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, stateUpdated: @escaping () -> Void) { + self.cache = cache + self.queueAffinity = queueAffinity + self.useYuvA = useYuvA + self.stateUpdated = stateUpdated + + self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + if let item = result.item { + strongSelf.item = Atomic(value: item) + } + if let (placeholder, index) = strongSelf.itemPlaceholderAndFrameIndex { + strongSelf.itemPlaceholderAndFrameIndex = nil + strongSelf.setFrameIndex(index: index, placeholder: placeholder) + } + strongSelf.updateIsPlaying() + } + }) + } + + deinit { + self.disposable?.dispose() + self.displayLink?.invalidate() + } + + func setFrameIndex(index: Int, placeholder: UIImage) { + if let item = self.item { + let nextFrame = item.with { item -> AnimationCacheItemFrame? in + item.reset() + for i in 0 ... index { + let result = item.advance(advance: .frames(1), requestedFormat: .rgba) + if i == index { + return result?.frame + } + } + return nil + } + + self.loadingFrameTaskId = nil + + if let nextFrame = nextFrame, let currentFrame = Frame(frame: nextFrame) { + self.currentFrame = currentFrame + + for target in self.targets.copyItems() { + if let target = target.value { + if let image = currentFrame.contentsAsImage { + target.transitionToContents(image.cgImage!, didLoop: false) + } else if let pixelBuffer = currentFrame.contentsAsCVPixelBuffer { + target.transitionToContents(pixelBuffer, didLoop: false) + } + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + } + } + } + } else { + for target in self.targets.copyItems() { + if let target = target.value { + target.transitionToContents(placeholder.cgImage!, didLoop: false) + } + } + + self.itemPlaceholderAndFrameIndex = (placeholder, index) + } + } + + func updateAddedTarget(target: MultiAnimationRenderTarget) { + if let currentFrame = self.currentFrame { + if let cgImage = currentFrame.contentsAsImage?.cgImage { + target.transitionToContents(cgImage, didLoop: false) + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + } else if let pixelBuffer = currentFrame.contentsAsCVPixelBuffer { + target.transitionToContents(pixelBuffer, didLoop: false) + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + } + } + + self.updateIsPlaying() + } + + func updateIsPlaying() { + var isPlaying = true + if self.item == nil { + isPlaying = false + } + + var shouldBeAnimating = false + for target in self.targets.copyItems() { + if let target = target.value { + if target.shouldBeAnimating { + shouldBeAnimating = true + break + } + } + } + if !shouldBeAnimating { + isPlaying = false + } + + self.isPlaying = isPlaying + } + + func animationTick(advanceTimestamp: Double) -> LoadFrameGroupTask? { + return self.update(advanceTimestamp: advanceTimestamp) + } + + private func update(advanceTimestamp: Double) -> LoadFrameGroupTask? { + guard let item = self.item else { + return nil + } + + var frameAdvance: AnimationCacheItem.Advance? + if self.loadingFrameTaskId == nil { + if let currentFrame = self.currentFrame, advanceTimestamp > 0.0 { + let divisionFactor = advanceTimestamp / currentFrame.remainingDuration + let wholeFactor = round(divisionFactor) + if abs(wholeFactor - divisionFactor) < 0.005 { + currentFrame.remainingDuration = 0.0 + frameAdvance = .frames(Int(wholeFactor)) + } else { + currentFrame.remainingDuration -= advanceTimestamp + if currentFrame.remainingDuration <= 0.0 { + frameAdvance = .duration(currentFrame.duration + max(0.0, -currentFrame.remainingDuration)) + } + } + } else if self.currentFrame == nil { + frameAdvance = .frames(1) + } + } + + if let frameAdvance = frameAdvance, self.loadingFrameTaskId == nil { + let taskId = self.nextLoadingFrameTaskId + self.nextLoadingFrameTaskId += 1 + + self.loadingFrameTaskId = taskId + let useYuvA = self.useYuvA + + return LoadFrameGroupTask(task: { [weak self] in + let currentFrame: (frame: Frame, didLoop: Bool)? + do { + if let (frame, didLoop) = try item.tryWith({ item -> (AnimationCacheItemFrame, Bool)? in + let defaultFormat: AnimationCacheItemFrame.RequestedFormat + if useYuvA { + defaultFormat = .yuva(rowAlignment: 1) + } else { + defaultFormat = .rgba + } + + if let result = item.advance(advance: frameAdvance, requestedFormat: defaultFormat) { + return (result.frame, result.didLoop) + } else { + return nil + } + }), let mappedFrame = Frame(frame: frame) { + currentFrame = (mappedFrame, didLoop) + } else { + currentFrame = nil + } + } catch { + assertionFailure() + currentFrame = nil + } + + return { + guard let strongSelf = self else { + return + } + + if strongSelf.loadingFrameTaskId != taskId { + return + } + + strongSelf.loadingFrameTaskId = nil + + if let currentFrame = currentFrame { + strongSelf.currentFrame = currentFrame.frame + for target in strongSelf.targets.copyItems() { + if let target = target.value { + if let image = currentFrame.frame.contentsAsImage { + target.transitionToContents(image.cgImage!, didLoop: currentFrame.didLoop) + } else if let pixelBuffer = currentFrame.frame.contentsAsCVPixelBuffer { + target.transitionToContents(pixelBuffer, didLoop: currentFrame.didLoop) + } + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.frame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + } + } + } + } + }, queueAffinity: self.queueAffinity) + } + + if let _ = self.currentFrame { + for target in self.targets.copyItems() { + if let target = target.value { + target.updateDisplayPlaceholder(displayPlaceholder: false) + } + } + } + + return nil + } +} + +public final class DCTMultiAnimationRendererImpl: MultiAnimationRenderer { + private final class GroupContext { + private let firstFrameQueue: Queue + private let stateUpdated: () -> Void + + private struct ItemKey: Hashable { + var id: String + var width: Int + var height: Int + var uniqueId: Int + } + + private var itemContexts: [ItemKey: ItemAnimationContext] = [:] + private var nextQueueAffinity: Int = 0 + private var nextUniqueId: Int = 1 + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + self.stateUpdated() + } + } + } + + init(firstFrameQueue: Queue, stateUpdated: @escaping () -> Void) { + self.firstFrameQueue = firstFrameQueue + self.stateUpdated = stateUpdated + } + + func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, useYuvA: Bool, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { + var uniqueId = 0 + if unique { + uniqueId = self.nextUniqueId + self.nextUniqueId += 1 + } + + let itemKey = ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: uniqueId) + let itemContext: ItemAnimationContext + if let current = self.itemContexts[itemKey] { + itemContext = current + } else { + let queueAffinity = self.nextQueueAffinity + self.nextQueueAffinity += 1 + itemContext = ItemAnimationContext(cache: cache, queueAffinity: queueAffinity, itemId: itemId, size: size, useYuvA: useYuvA, fetch: fetch, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.itemContexts[itemKey] = itemContext + } + + let index = itemContext.targets.add(Weak(target)) + itemContext.updateAddedTarget(target: target) + + let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in + Queue.mainQueue().async { + guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { + return + } + itemContext.targets.remove(index) + if itemContext.targets.isEmpty { + strongSelf.itemContexts.removeValue(forKey: itemKey) + } + } + } + + let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in + guard let itemContext = itemContext else { + return + } + itemContext.updateIsPlaying() + } + + return ActionDisposable { [weak self, weak itemContext, weak target] in + guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { + return + } + if let target = target { + target.deinitCallbacks.remove(deinitIndex) + target.updateStateCallbacks.remove(updateStateIndex) + } + itemContext.targets.remove(index) + if itemContext.targets.isEmpty { + strongSelf.itemContexts.removeValue(forKey: itemKey) + } + }.strict() + } + + func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) { + guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else { + return false + } + guard let loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) else { + return false + } + + if let image = loadedFrame.contentsAsImage { + target.contents = image.cgImage + } else if let pixelBuffer = loadedFrame.contentsAsCVPixelBuffer { + target.contents = pixelBuffer + } + target.numFrames = item.numFrames + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + + return true + } else { + return false + } + } + + func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { + var hadIntermediateUpdate = false + return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { [weak target] item in + guard let item = item.item else { + let isFinal = item.isFinal + hadIntermediateUpdate = true + Queue.mainQueue().async { + completion(false, isFinal) + } + return + } + + let loadedFrame: ItemAnimationContext.Frame? + if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { + loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) + } else { + loadedFrame = nil + } + + Queue.mainQueue().async { + guard let target = target else { + completion(false, true) + return + } + target.numFrames = item.numFrames + if let loadedFrame = loadedFrame { + if let cgImage = loadedFrame.contentsAsImage?.cgImage { + if hadIntermediateUpdate { + target.transitionToContents(cgImage, didLoop: false) + } else { + target.contents = cgImage + } + } else if let pixelBuffer = loadedFrame.contentsAsCVPixelBuffer { + if hadIntermediateUpdate { + target.transitionToContents(pixelBuffer, didLoop: false) + } else { + target.contents = pixelBuffer + } + } + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } + + completion(true, true) + } else { + completion(false, true) + } + } + }).strict() + } + + func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { + return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { item in + guard let item = item.item else { + Queue.mainQueue().async { + completion(nil) + } + return + } + + let loadedFrame: ItemAnimationContext.Frame? + if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { + loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) + } else { + loadedFrame = nil + } + + Queue.mainQueue().async { + if let loadedFrame = loadedFrame { + if let cgImage = loadedFrame.contentsAsImage?.cgImage { + completion(cgImage) + } else { + completion(nil) + } + } else { + completion(nil) + } + } + }).strict() + } + + func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { + if let itemContext = self.itemContexts[ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: 0)] { + itemContext.setFrameIndex(index: frameIndex, placeholder: placeholder) + } + } + + private func updateIsPlaying() { + var isPlaying = false + for (_, itemContext) in self.itemContexts { + if itemContext.isPlaying { + isPlaying = true + break + } + } + + self.isPlaying = isPlaying + } + + func animationTick(advanceTimestamp: Double) -> [LoadFrameGroupTask] { + var tasks: [LoadFrameGroupTask] = [] + for (_, itemContext) in self.itemContexts { + if itemContext.isPlaying { + if let task = itemContext.animationTick(advanceTimestamp: advanceTimestamp) { + tasks.append(task) + } + } + } + + return tasks + } + } + + public static let firstFrameQueue = Queue(name: "DCTMultiAnimationRenderer-FirstFrame", qos: .userInteractive) + + public var useYuvA: Bool = false + private var groupContext: GroupContext? + private var frameSkip: Int + private var displayTimer: Foundation.Timer? + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + if self.isPlaying { + if self.displayTimer == nil { + final class TimerTarget: NSObject { + private let f: () -> Void + + init(_ f: @escaping () -> Void) { + self.f = f + } + + @objc func timerEvent() { + self.f() + } + } + let frameInterval = Double(self.frameSkip) / 60.0 + let displayTimer = Foundation.Timer(timeInterval: frameInterval, target: TimerTarget { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.animationTick(frameInterval: frameInterval) + }, selector: #selector(TimerTarget.timerEvent), userInfo: nil, repeats: true) + self.displayTimer = displayTimer + RunLoop.main.add(displayTimer, forMode: .common) + } + } else { + if let displayTimer = self.displayTimer { + self.displayTimer = nil + displayTimer.invalidate() + } + } + } + } + } + + public init() { + if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.processorCount > 2 { + self.frameSkip = 1 + } else { + self.frameSkip = 2 + } + } + + public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: DCTMultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, unique: unique, size: size, useYuvA: self.useYuvA, fetch: fetch) + + return ActionDisposable { + disposable.dispose() + }.strict() + } + + public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: DCTMultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size) + } + + public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: DCTMultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() + } + + public func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { + let groupContext: GroupContext + if let current = self.groupContext { + groupContext = current + } else { + groupContext = GroupContext(firstFrameQueue: DCTMultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.groupContext = groupContext + } + + return groupContext.loadFirstFrameAsImage(cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() + } + + public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { + if let groupContext = self.groupContext { + groupContext.setFrameIndex(itemId: itemId, size: size, frameIndex: frameIndex, placeholder: placeholder) + } + } + + private func updateIsPlaying() { + var isPlaying = false + if let groupContext = self.groupContext { + if groupContext.isPlaying { + isPlaying = true + } + } + + self.isPlaying = isPlaying + } + + private func animationTick(frameInterval: Double) { + let secondsPerFrame = frameInterval + + var tasks: [LoadFrameGroupTask] = [] + if let groupContext = self.groupContext { + if groupContext.isPlaying { + tasks.append(contentsOf: groupContext.animationTick(advanceTimestamp: secondsPerFrame)) + } + } + + if !tasks.isEmpty { + let tasks0 = tasks.filter { $0.queueAffinity % 2 == 0 } + let tasks1 = tasks.filter { $0.queueAffinity % 2 == 1 } + let allTasks = [tasks0, tasks1] + + let taskCompletions = Atomic<[Int: [() -> Void]]>(value: [:]) + let queues: [Queue] = [ItemAnimationContext.queue0, ItemAnimationContext.queue1] + + for i in 0 ..< 2 { + let partTasks = allTasks[i] + let id = i + queues[i].async { + var completions: [() -> Void] = [] + for task in partTasks { + let complete = task.task() + completions.append(complete) + } + + var complete = false + let _ = taskCompletions.modify { current in + var current = current + current[id] = completions + if current.count == 2 { + complete = true + } + return current + } + + if complete { + Queue.mainQueue().async { + let allCompletions = taskCompletions.with { $0 } + for (_, fs) in allCompletions { + for f in fs { + f() + } + } + } + } + } + } + } + } +} diff --git a/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD b/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD index 945235a576..45f9626c9b 100644 --- a/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD +++ b/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift b/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift index 2f6a008dc0..ad65c83f95 100644 --- a/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift +++ b/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift @@ -4,7 +4,6 @@ import AsyncDisplayKit import Display import ComponentFlow import SwiftSignalKit -import Postbox import TelegramCore import Markdown import TextFormat diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 9c2f87ad96..9d68d758fe 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -38,8 +38,8 @@ private func randomGenericReactionEffect(context: AccountContext) -> Signal filter(\.complete) + let dataDisposable = (context.engine.resources.data(resource: EngineMediaResource(file.resource)) + |> filter(\.isComplete) |> take(1)).start(next: { data in subscriber.putNext(data.path) subscriber.putCompletion() @@ -260,6 +260,7 @@ public final class EmojiStatusSelectionController: ViewController { var id: AnyHashable var version: Int var isPreset: Bool + var canLoadMore: Bool } private struct EmojiSearchState { @@ -297,6 +298,7 @@ public final class EmojiStatusSelectionController: ViewController { private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? private let emojiSearchDisposable = MetaDisposable() + private var emojiSearchContext: EmojiSearchContext? private let emojiSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) { didSet { @@ -339,24 +341,24 @@ public final class EmojiStatusSelectionController: ViewController { self.componentHost = ComponentView() self.componentShadowLayer = SimpleLayer() - self.componentShadowLayer.shadowOpacity = 0.12 + self.componentShadowLayer.shadowOpacity = 0.35 self.componentShadowLayer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor - self.componentShadowLayer.shadowOffset = CGSize(width: 0.0, height: 2.0) - self.componentShadowLayer.shadowRadius = 16.0 + self.componentShadowLayer.shadowOffset = CGSize(width: 0.0, height: 10.0) + self.componentShadowLayer.shadowRadius = 30.0 self.cloudLayer0 = SimpleLayer() self.cloudShadowLayer0 = SimpleLayer() - self.cloudShadowLayer0.shadowOpacity = 0.12 - self.cloudShadowLayer0.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor - self.cloudShadowLayer0.shadowOffset = CGSize(width: 0.0, height: 2.0) - self.cloudShadowLayer0.shadowRadius = 16.0 + self.cloudShadowLayer0.shadowOpacity = self.componentShadowLayer.shadowOpacity + self.cloudShadowLayer0.shadowColor = self.componentShadowLayer.shadowColor + self.cloudShadowLayer0.shadowOffset = self.componentShadowLayer.shadowOffset + self.cloudShadowLayer0.shadowRadius = self.componentShadowLayer.shadowRadius self.cloudLayer1 = SimpleLayer() self.cloudShadowLayer1 = SimpleLayer() - self.cloudShadowLayer1.shadowOpacity = 0.12 - self.cloudShadowLayer1.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor - self.cloudShadowLayer1.shadowOffset = CGSize(width: 0.0, height: 2.0) - self.cloudShadowLayer1.shadowRadius = 16.0 + self.cloudShadowLayer1.shadowOpacity = self.componentShadowLayer.shadowOpacity + self.cloudShadowLayer1.shadowColor = self.componentShadowLayer.shadowColor + self.cloudShadowLayer1.shadowOffset = self.componentShadowLayer.shadowOffset + self.cloudShadowLayer1.shadowRadius = self.componentShadowLayer.shadowRadius super.init() @@ -412,7 +414,7 @@ public final class EmojiStatusSelectionController: ViewController { } else { strongSelf.stableEmptyResultEmoji = nil } - emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : .active) + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : .active, canLoadMore: emojiSearchResult.canLoadMore) } else { strongSelf.stableEmptyResultEmoji = nil } @@ -478,20 +480,23 @@ public final class EmojiStatusSelectionController: ViewController { guard let self = self else { return } - + switch query { case .none: + self.emojiSearchContext = nil self.emojiSearchDisposable.set(nil) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) case let .text(rawQuery, languageCode): let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) - + if query.isEmpty { + self.emojiSearchContext = nil self.emojiSearchDisposable.set(nil) self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) } else { let context = self.context - + self.emojiSearchContext = nil + var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) if !languageCode.lowercased().hasPrefix("en") { signal = signal @@ -505,7 +510,7 @@ public final class EmojiStatusSelectionController: ViewController { ) } } - + let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> map { peer -> Bool in guard case let .user(user) = peer else { @@ -514,62 +519,60 @@ public final class EmojiStatusSelectionController: ViewController { return user.isPremium } |> distinctUntilChanged - - let resultSignal = signal - |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in - return combineLatest( - context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), - context.engine.stickers.availableReactions(), - hasPremium - ) - |> take(1) - |> map { view, availableReactions, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in - var result: [(String, TelegramMediaFile.Accessor?, String)] = [] - - var allEmoticons: [String: String] = [:] - for keyword in keywords { - for emoticon in keyword.emoticons { - allEmoticons[emoticon] = keyword.keyword - } + + let resultSignal = combineLatest( + signal, + hasPremium + ) + |> mapToSignal { keywords, hasPremium -> Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?), NoError> in + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword } - - for entry in view.entries { - guard let item = entry.item as? StickerPackItem else { + } + + let currentEmojiSearchContext = context.engine.stickers.emojiSearchContext(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode) + let emojiSearchContext: EmojiSearchContext? = currentEmojiSearchContext + let remoteSignal: Signal = currentEmojiSearchContext.state + let remotePacksSignal: Signal = context.engine.stickers.searchEmojiSets(query: query) + |> mapToSignal { localResult in + return .single(localResult) + |> then( + context.engine.stickers.searchEmojiSetsRemotely(query: query) + |> map { remoteResult in + return localResult.merge(with: remoteResult) + } + ) + } + + return combineLatest(remoteSignal, remotePacksSignal) + |> map { foundEmoji, foundPacks -> (groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?) in + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for itemFile in foundEmoji.items { + if existingIds.contains(itemFile.fileId) { continue } - if let alt = item.file.customEmojiAlt { - if !item.file.isPremiumEmoji || hasPremium { - if !alt.isEmpty, let keyword = allEmoticons[alt] { - result.append((alt, item.file, keyword)) - } else if alt == query { - result.append((alt, item.file, alt)) - } - } + existingIds.insert(itemFile.fileId) + if itemFile.isPremiumEmoji && !hasPremium { + continue } + let animationData = EntityKeyboardAnimationData(file: TelegramMediaFile.Accessor(itemFile)) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: TelegramMediaFile.Accessor(itemFile), + subgroupId: nil, + icon: .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + items.append(item) } - - var items: [EmojiPagerContentComponent.Item] = [] - - var existingIds = Set() - for item in result { - if let itemFile = item.1 { - if existingIds.contains(itemFile.fileId) { - continue - } - existingIds.insert(itemFile.fileId) - let animationData = EntityKeyboardAnimationData(file: itemFile) - let item = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: itemFile, subgroupId: nil, - icon: .none, - tintMode: animationData.isTemplate ? .primary : .none - ) - items.append(item) - } - } - - return [EmojiPagerContentComponent.ItemGroup( + + var resultGroups: [EmojiPagerContentComponent.ItemGroup] = [] + resultGroups.append(EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, @@ -586,10 +589,64 @@ public final class EmojiStatusSelectionController: ViewController { headerItem: nil, fillWithLoadingPlaceholders: false, items: items - )] + )) + + for (collectionId, info, _, _) in foundPacks.infos { + if let info = info as? StickerPackCollectionInfo { + var topItems: [StickerPackItem] = [] + for e in foundPacks.entries { + if let item = e.item as? StickerPackItem { + if e.index.collectionId == collectionId { + topItems.append(item) + } + } + } + + var groupItems: [EmojiPagerContentComponent.Item] = [] + for item in topItems { + var tintMode: EmojiPagerContentComponent.Item.TintMode = .none + if item.file.isCustomTemplateEmoji { + tintMode = .primary + } + + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + groupItems.append(resultItem) + } + + resultGroups.append(EmojiPagerContentComponent.ItemGroup( + supergroupId: AnyHashable(info.id), + groupId: AnyHashable(info.id), + title: info.title, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: 3, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: groupItems + )) + } + } + + return (resultGroups, foundEmoji.canLoadMore, foundEmoji.items.isEmpty && foundEmoji.isLoadingMore, emojiSearchContext) } } - + var version = 0 self.emojiSearchStateValue.isSearching = true self.emojiSearchDisposable.set((resultSignal @@ -598,12 +655,14 @@ public final class EmojiStatusSelectionController: ViewController { guard let self else { return } - - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + + self.emojiSearchContext = result.searchContext + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.groups, id: AnyHashable(query), version: version, isPreset: false, canLoadMore: result.canLoadMore), isSearching: result.isSearching) version += 1 })) } case let .category(value): + self.emojiSearchContext = nil let resultSignal = self.context.engine.stickers.searchEmoji(category: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -677,11 +736,11 @@ public final class EmojiStatusSelectionController: ViewController { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) return } - - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) version += 1 })) } @@ -689,6 +748,9 @@ public final class EmojiStatusSelectionController: ViewController { updateScrollingToItemGroup: { }, onScroll: {}, + loadMore: { + self?.emojiSearchContext?.loadMore() + }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, @@ -973,16 +1035,12 @@ public final class EmojiStatusSelectionController: ViewController { if self.presentationData.theme.overallDarkAppearance { listBackgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor separatorColor = self.presentationData.theme.list.itemBlocksSeparatorColor - self.componentShadowLayer.shadowOpacity = 0.32 - self.cloudShadowLayer0.shadowOpacity = 0.32 - self.cloudShadowLayer1.shadowOpacity = 0.32 } else { listBackgroundColor = self.presentationData.theme.list.plainBackgroundColor separatorColor = self.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5) - self.componentShadowLayer.shadowOpacity = 0.12 - self.cloudShadowLayer0.shadowOpacity = 0.12 - self.cloudShadowLayer1.shadowOpacity = 0.12 } + self.cloudShadowLayer0.shadowOpacity = self.componentShadowLayer.shadowOpacity + self.cloudShadowLayer1.shadowOpacity = self.componentShadowLayer.shadowOpacity self.cloudLayer0.backgroundColor = listBackgroundColor.cgColor self.cloudLayer1.backgroundColor = listBackgroundColor.cgColor @@ -1207,10 +1265,10 @@ public final class EmojiStatusSelectionController: ViewController { for reaction in availableReactions.reactions { if case let .builtin(value) = reaction.value, value == emojiString { if let aroundAnimation = reaction.aroundAnimation?._parse() { - return context.account.postbox.mediaBox.resourceData(aroundAnimation.resource) + return context.engine.resources.data(resource: EngineMediaResource(aroundAnimation.resource)) |> take(1) |> map { data -> String? in - if data.complete { + if data.isComplete { return data.path } else { return nil @@ -1396,10 +1454,10 @@ public final class EmojiStatusSelectionController: ViewController { for reaction in availableReactions.reactions { if case let .builtin(value) = reaction.value, value == emojiString { if let aroundAnimation = reaction.aroundAnimation?._parse() { - return context.account.postbox.mediaBox.resourceData(aroundAnimation.resource) + return context.engine.resources.data(resource: EngineMediaResource(aroundAnimation.resource)) |> take(1) |> map { data -> String? in - if data.complete { + if data.isComplete { return data.path } else { return nil @@ -1430,12 +1488,7 @@ public final class EmojiStatusSelectionController: ViewController { } } - public enum Mode { - case statusSelection - case backgroundSelection(completion: (TelegramMediaFile?) -> Void) - case customStatusSelection(completion: (TelegramMediaFile?, Int32?) -> Void) - case quickReactionSelection(completion: () -> Void) - } + public typealias Mode = EmojiStatusSelectionControllerMode private let context: AccountContext private weak var sourceView: UIView? diff --git a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/BUILD b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/BUILD index 14b1bac8ef..6c78adcc19 100644 --- a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/BUILD +++ b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/BUILD @@ -17,7 +17,6 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/AccountContext:AccountContext", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TextFormat:TextFormat", diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD index 294e001b6f..cdfd5f6774 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/TelegramUIPreferences", "//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage", diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 344e3fc428..b4ac019707 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -13,6 +13,7 @@ import AnimationCache import LottieAnimationCache import VideoAnimationCache import MultiAnimationRenderer +import DCTMultiAnimationRendererImpl import ShimmerEffect import TextFormat import TelegramUIPreferences @@ -817,7 +818,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { let isThumbnailCancelled = Atomic(value: false) self.loadDisposable = arguments.renderer.loadFirstFrame(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in if !result { - MultiAnimationRendererImpl.firstFrameQueue.async { + DCTMultiAnimationRendererImpl.firstFrameQueue.async { let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) DispatchQueue.main.async { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 0ca5c0ae13..1c0a044379 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -28,6 +28,7 @@ swift_library( "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent", "//submodules/TelegramUI/Components/LottieComponent", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 18695c713d..f9e3617e60 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -266,6 +266,7 @@ public final class EmojiPagerContentComponent: Component { public let updateScrollingToItemGroup: () -> Void public let externalCancel: (() -> Void)? public let onScroll: () -> Void + public let loadMore: (() -> Void)? public let chatPeerId: PeerId? public let peekBehavior: EmojiContentPeekBehavior? public let customLayout: CustomLayout? @@ -296,6 +297,7 @@ public final class EmojiPagerContentComponent: Component { updateScrollingToItemGroup: @escaping () -> Void, externalCancel: (() -> Void)? = nil, onScroll: @escaping () -> Void, + loadMore: (() -> Void)? = nil, chatPeerId: PeerId?, peekBehavior: EmojiContentPeekBehavior?, customLayout: CustomLayout?, @@ -324,6 +326,7 @@ public final class EmojiPagerContentComponent: Component { self.updateScrollingToItemGroup = updateScrollingToItemGroup self.externalCancel = externalCancel self.onScroll = onScroll + self.loadMore = loadMore self.chatPeerId = chatPeerId self.peekBehavior = peekBehavior self.customLayout = customLayout @@ -626,6 +629,7 @@ public final class EmojiPagerContentComponent: Component { public let contentItemGroups: [ItemGroup] public let itemLayoutType: ItemLayoutType public let itemContentUniqueId: ContentId? + public let canLoadMore: Bool public let searchState: SearchState public let warpContentsOnEdges: Bool public let hideBackground: Bool @@ -652,6 +656,7 @@ public final class EmojiPagerContentComponent: Component { contentItemGroups: [ItemGroup], itemLayoutType: ItemLayoutType, itemContentUniqueId: ContentId?, + canLoadMore: Bool = false, searchState: SearchState, warpContentsOnEdges: Bool, hideBackground: Bool, @@ -677,6 +682,7 @@ public final class EmojiPagerContentComponent: Component { self.contentItemGroups = contentItemGroups self.itemLayoutType = itemLayoutType self.itemContentUniqueId = itemContentUniqueId + self.canLoadMore = canLoadMore self.searchState = searchState self.warpContentsOnEdges = warpContentsOnEdges self.hideBackground = hideBackground @@ -693,7 +699,7 @@ public final class EmojiPagerContentComponent: Component { self.customTintColor = customTintColor } - public func withUpdatedItemGroups(panelItemGroups: [ItemGroup], contentItemGroups: [ItemGroup], itemContentUniqueId: ContentId?, emptySearchResults: EmptySearchResults?, searchState: SearchState) -> EmojiPagerContentComponent { + public func withUpdatedItemGroups(panelItemGroups: [ItemGroup], contentItemGroups: [ItemGroup], itemContentUniqueId: ContentId?, emptySearchResults: EmptySearchResults?, searchState: SearchState, canLoadMore: Bool? = nil) -> EmojiPagerContentComponent { return EmojiPagerContentComponent( id: self.id, context: self.context, @@ -705,6 +711,7 @@ public final class EmojiPagerContentComponent: Component { contentItemGroups: contentItemGroups, itemLayoutType: self.itemLayoutType, itemContentUniqueId: itemContentUniqueId, + canLoadMore: canLoadMore ?? self.canLoadMore, searchState: searchState, warpContentsOnEdges: self.warpContentsOnEdges, hideBackground: self.hideBackground, @@ -734,6 +741,7 @@ public final class EmojiPagerContentComponent: Component { contentItemGroups: contentItemGroups, itemLayoutType: self.itemLayoutType, itemContentUniqueId: itemContentUniqueId, + canLoadMore: self.canLoadMore, searchState: searchState, warpContentsOnEdges: self.warpContentsOnEdges, hideBackground: self.hideBackground, @@ -763,6 +771,7 @@ public final class EmojiPagerContentComponent: Component { contentItemGroups: contentItemGroups, itemLayoutType: self.itemLayoutType, itemContentUniqueId: itemContentUniqueId, + canLoadMore: self.canLoadMore, searchState: searchState, warpContentsOnEdges: self.warpContentsOnEdges, hideBackground: self.hideBackground, @@ -814,6 +823,9 @@ public final class EmojiPagerContentComponent: Component { if lhs.itemContentUniqueId != rhs.itemContentUniqueId { return false } + if lhs.canLoadMore != rhs.canLoadMore { + return false + } if lhs.searchState != rhs.searchState { return false } @@ -1395,6 +1407,7 @@ public final class EmojiPagerContentComponent: Component { private var vibrancyClippingView: UIView private var vibrancyEffectView: UIView? public private(set) var mirrorContentClippingView: UIView? + private let mirrorScrollViewClippingView: UIView private let mirrorContentScrollView: UIView private var warpView: WarpView? private var mirrorContentWarpView: WarpView? @@ -1418,6 +1431,7 @@ public final class EmojiPagerContentComponent: Component { private var visibleGroupPremiumButtons: [AnyHashable: ComponentView] = [:] private var visibleGroupExpandActionButtons: [AnyHashable: GroupExpandActionButton] = [:] private var expandedGroupIds: Set = Set() + private var requestedLoadMoreContentId: ContentId? private var ignoreScrolling: Bool = false private var keepTopPanelVisibleUntilScrollingInput: Bool = false @@ -1445,6 +1459,13 @@ public final class EmojiPagerContentComponent: Component { private var tapRecognizer: UITapGestureRecognizer? private var longTapRecognizer: UILongPressGestureRecognizer? + private func hasSameContentId(_ lhs: ContentId?, _ rhs: ContentId?) -> Bool { + if let rhs, rhs.version < 2 { + return false + } + return lhs?.id == rhs?.id + } + override init(frame: CGRect) { self.backgroundView = BlurredBackgroundView(color: nil) self.backgroundTintView = UIView() @@ -1463,6 +1484,9 @@ public final class EmojiPagerContentComponent: Component { self.scrollViewClippingView = UIView() self.scrollViewClippingView.clipsToBounds = true + self.mirrorScrollViewClippingView = UIView() + self.mirrorScrollViewClippingView.clipsToBounds = true + self.mirrorContentScrollView = UIView() self.mirrorContentScrollView.layer.anchorPoint = CGPoint() self.mirrorContentScrollView.clipsToBounds = true @@ -1502,6 +1526,8 @@ public final class EmojiPagerContentComponent: Component { self.addSubview(self.scrollViewClippingView) self.scrollViewClippingView.addSubview(self.scrollView) + self.mirrorScrollViewClippingView.addSubview(self.mirrorContentScrollView) + self.scrollView.addSubview(self.placeholdersContainerView) let contextGesture = ContextGesture(target: self, action: #selector(self.tapGesture(_:))) @@ -1641,6 +1667,16 @@ public final class EmojiPagerContentComponent: Component { fatalError("init(coder:) has not been implemented") } + private var mirrorOverlayContainerView: UIView? { + if let mirrorContentClippingView = self.mirrorContentClippingView { + return mirrorContentClippingView + } else if let vibrancyEffectView = self.vibrancyEffectView { + return vibrancyEffectView + } else { + return nil + } + } + func updateIsWarpEnabled(isEnabled: Bool) { if isEnabled { if self.warpView == nil { @@ -1654,6 +1690,7 @@ public final class EmojiPagerContentComponent: Component { let mirrorContentWarpView = WarpView(frame: CGRect()) self.mirrorContentWarpView = mirrorContentWarpView + self.mirrorScrollViewClippingView.addSubview(mirrorContentWarpView) mirrorContentWarpView.contentView.addSubview(self.mirrorContentScrollView) } } else { @@ -1666,12 +1703,7 @@ public final class EmojiPagerContentComponent: Component { if let mirrorContentWarpView = self.mirrorContentWarpView { self.mirrorContentWarpView = nil - if let mirrorContentClippingView = self.mirrorContentClippingView { - mirrorContentClippingView.addSubview(self.mirrorContentScrollView) - } else if let vibrancyEffectView = self.vibrancyEffectView { - vibrancyEffectView.addSubview(self.mirrorContentScrollView) - } - + self.mirrorScrollViewClippingView.addSubview(self.mirrorContentScrollView) mirrorContentWarpView.removeFromSuperview() } } @@ -3003,6 +3035,8 @@ public final class EmojiPagerContentComponent: Component { if let stateContext = self.component?.inputInteractionHolder.inputInteraction?.stateContext { stateContext.scrollPosition = scrollView.bounds.minY } + + self.maybeLoadMore() } public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { @@ -4048,6 +4082,37 @@ public final class EmojiPagerContentComponent: Component { } } + private func maybeLoadMore() { + guard let component = self.component, !self.isUpdating else { + return + } + guard component.canLoadMore, let contentId = component.itemContentUniqueId else { + self.requestedLoadMoreContentId = nil + return + } + guard let loadMore = component.inputInteractionHolder.inputInteraction?.loadMore else { + return + } + + let loadMoreBoundary: CGFloat + if component.contentItemGroups.first?.groupId == AnyHashable("search"), let itemLayout = self.itemLayout, itemLayout.itemGroupLayouts.count > 1 { + loadMoreBoundary = itemLayout.itemGroupLayouts[1].frame.minY + } else { + loadMoreBoundary = self.scrollView.contentSize.height + } + + let remainingDistance = loadMoreBoundary - self.scrollView.bounds.maxY + if remainingDistance > 200.0 { + return + } + if self.requestedLoadMoreContentId == contentId { + return + } + + self.requestedLoadMoreContentId = contentId + loadMore() + } + private func expandGroup(groupId: AnyHashable) { self.expandedGroupIds.insert(groupId) @@ -4067,12 +4132,11 @@ public final class EmojiPagerContentComponent: Component { mirrorContentClippingView = UIView() mirrorContentClippingView.clipsToBounds = false self.mirrorContentClippingView = mirrorContentClippingView - - if let mirrorContentWarpView = self.mirrorContentWarpView { - mirrorContentClippingView.addSubview(mirrorContentWarpView) - } else { - mirrorContentClippingView.addSubview(self.mirrorContentScrollView) - } + } + if self.mirrorScrollViewClippingView.superview !== mirrorContentClippingView { + mirrorContentClippingView.insertSubview(self.mirrorScrollViewClippingView, at: 0) + } else { + mirrorContentClippingView.sendSubviewToBack(self.mirrorScrollViewClippingView) } let clippingFrame = CGRect(origin: CGPoint(x: 0.0, y: pagerEnvironment.containerInsets.top), size: CGSize(width: backgroundFrame.width, height: backgroundFrame.height)) @@ -4096,9 +4160,13 @@ public final class EmojiPagerContentComponent: Component { } self.vibrancyEffectView = vibrancyEffectView self.backgroundTintView.mask = vibrancyEffectView - self.vibrancyClippingView.addSubview(self.mirrorContentScrollView) vibrancyEffectView.addSubview(self.vibrancyClippingView) } + if self.mirrorScrollViewClippingView.superview !== self.vibrancyClippingView { + self.vibrancyClippingView.insertSubview(self.mirrorScrollViewClippingView, at: 0) + } else { + self.vibrancyClippingView.sendSubviewToBack(self.mirrorScrollViewClippingView) + } } if component.hideBackground { @@ -4283,7 +4351,7 @@ public final class EmojiPagerContentComponent: Component { var previousAbsoluteItemPositions: [VisualItemKey: CGPoint] = [:] var anchorItems: [EmojiKeyboardItemLayer.Key: CGRect] = [:] - if let previousComponent = previousComponent, let previousItemLayout = self.itemLayout, previousComponent.contentItemGroups != component.contentItemGroups, previousComponent.itemContentUniqueId == component.itemContentUniqueId { + if let previousComponent = previousComponent, let previousItemLayout = self.itemLayout, previousComponent.contentItemGroups != component.contentItemGroups, self.hasSameContentId(previousComponent.itemContentUniqueId, component.itemContentUniqueId) { if !transition.animation.isImmediate { var previousItemPositionsValue: [VisualItemKey: CGPoint] = [:] for groupIndex in 0 ..< previousComponent.contentItemGroups.count { @@ -4405,7 +4473,7 @@ public final class EmojiPagerContentComponent: Component { if previousComponent == nil { isFirstUpdate = true } - if previousComponent?.itemContentUniqueId != component.itemContentUniqueId { + if !self.hasSameContentId(previousComponent?.itemContentUniqueId, component.itemContentUniqueId) { resetScrolling = true } if resetScrolling { @@ -4413,8 +4481,8 @@ public final class EmojiPagerContentComponent: Component { } var animateContentCrossfade = false - if let previousComponent, previousComponent.itemContentUniqueId != component.itemContentUniqueId, itemTransition.animation.isImmediate { - if !(previousComponent.contentItemGroups.contains(where: { $0.fillWithLoadingPlaceholders }) && component.contentItemGroups.contains(where: { $0.fillWithLoadingPlaceholders })) && previousComponent.itemContentUniqueId?.id != component.itemContentUniqueId?.id { + if let previousComponent, !self.hasSameContentId(previousComponent.itemContentUniqueId, component.itemContentUniqueId), itemTransition.animation.isImmediate { + if !(previousComponent.contentItemGroups.contains(where: { $0.fillWithLoadingPlaceholders }) && component.contentItemGroups.contains(where: { $0.fillWithLoadingPlaceholders })) { animateContentCrossfade = true } } @@ -4501,6 +4569,9 @@ public final class EmojiPagerContentComponent: Component { transition.setFrame(view: self.vibrancyClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize)) transition.setBounds(view: self.vibrancyClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize)) + transition.setFrame(view: self.mirrorScrollViewClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize)) + transition.setBounds(view: self.mirrorScrollViewClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize)) + let previousSize = self.scrollView.bounds.size self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollSize) @@ -4590,10 +4661,12 @@ public final class EmojiPagerContentComponent: Component { } let previousBounds = self.scrollView.bounds - self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffsetY), animated: false) let scrollOffset = previousBounds.minY - contentOffsetY - transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: scrollOffset), to: CGPoint(), additive: true) - animatedScrollOffset = scrollOffset + if abs(scrollOffset) > 0.5 { + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffsetY), animated: false) + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: scrollOffset), to: CGPoint(), additive: true) + animatedScrollOffset = scrollOffset + } break outer } @@ -4662,11 +4735,9 @@ public final class EmojiPagerContentComponent: Component { if self.isSearchActivated { if visibleSearchHeader.superview != self { self.addSubview(visibleSearchHeader) - if self.mirrorContentClippingView != nil { - self.mirrorContentClippingView?.addSubview(visibleSearchHeader.tintContainerView) - } else { - self.mirrorContentScrollView.superview?.superview?.addSubview(visibleSearchHeader.tintContainerView) - } + } + if let mirrorOverlayContainerView = self.mirrorOverlayContainerView, visibleSearchHeader.tintContainerView.superview !== mirrorOverlayContainerView { + mirrorOverlayContainerView.addSubview(visibleSearchHeader.tintContainerView) } } else { /*if useOpaqueTheme { @@ -4727,7 +4798,7 @@ public final class EmojiPagerContentComponent: Component { self.visibleSearchHeader = visibleSearchHeader if self.isSearchActivated { self.addSubview(visibleSearchHeader) - self.mirrorContentClippingView?.addSubview(visibleSearchHeader.tintContainerView) + self.mirrorOverlayContainerView?.addSubview(visibleSearchHeader.tintContainerView) } else { self.scrollView.addSubview(visibleSearchHeader) self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift index df4dd2efbb..c7b08f6fa2 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift @@ -1241,6 +1241,9 @@ public extension EmojiPagerContentComponent { tintMode: tintMode ) case let .text(text): + if !areUnicodeEmojiEnabled { + continue + } resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .staticEmoji(text), diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift index d0839eceeb..c370c29594 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift @@ -5,7 +5,6 @@ import ComponentFlow import PagerComponent import TelegramPresentationData import TelegramCore -import Postbox import AnimationCache import MultiAnimationRenderer import AccountContext diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift index 441b19ba9e..871678bb2c 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift @@ -5,7 +5,6 @@ import ComponentFlow import PagerComponent import TelegramPresentationData import TelegramCore -import Postbox import AnimationCache import MultiAnimationRenderer import AccountContext diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 12ec0cfb7f..bb904a961c 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 { @@ -988,6 +1005,14 @@ public final class EntityKeyboardComponent: Component { pagerView.collapseTopPanel() } + public func revealHiddenPanels() { + guard let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent.View else { + return + } + + pagerView.revealHiddenPanels() + } + private func reorderPacks(category: ReorderCategory, items: [EntityKeyboardTopPanelComponent.Item]) { self.component?.reorderItems(category, items) } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift index 83c09abbe0..d5b92ee876 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift @@ -5,7 +5,6 @@ import ComponentFlow import PagerComponent import TelegramPresentationData import TelegramCore -import Postbox public final class EntityKeyboardTopContainerPanelEnvironment: Equatable { let isContentInFocus: Bool @@ -13,7 +12,7 @@ public final class EntityKeyboardTopContainerPanelEnvironment: Equatable { let visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)> let isExpandedUpdated: (Bool, ComponentTransition) -> Void - init( + public init( isContentInFocus: Bool, height: CGFloat, visibilityFractionUpdated: ActionSlot<(CGFloat, ComponentTransition)>, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index a28088ad5d..68918afe3c 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -1218,11 +1218,14 @@ public final class EntityKeyboardTopPanelComponent: Component { let containerSideInset: CGFloat let defaultActiveItemId: AnyHashable? let forceActiveItemId: AnyHashable? + let displayHighlightInExpanded: Bool + let automaticallySelectsFirstItem: Bool + let itemSpacing: CGFloat let activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)> let activeContentItemMapping: [AnyHashable: AnyHashable] let reorderItems: ([Item]) -> Void - init( + public init( id: AnyHashable, theme: PresentationTheme, customTintColor: UIColor?, @@ -1230,6 +1233,9 @@ public final class EntityKeyboardTopPanelComponent: Component { containerSideInset: CGFloat, defaultActiveItemId: AnyHashable? = nil, forceActiveItemId: AnyHashable? = nil, + displayHighlightInExpanded: Bool = false, + automaticallySelectsFirstItem: Bool = true, + itemSpacing: CGFloat = 8.0, activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, ComponentTransition)>, activeContentItemMapping: [AnyHashable: AnyHashable] = [:], reorderItems: @escaping ([Item]) -> Void @@ -1241,6 +1247,9 @@ public final class EntityKeyboardTopPanelComponent: Component { self.containerSideInset = containerSideInset self.defaultActiveItemId = defaultActiveItemId self.forceActiveItemId = forceActiveItemId + self.displayHighlightInExpanded = displayHighlightInExpanded + self.automaticallySelectsFirstItem = automaticallySelectsFirstItem + self.itemSpacing = itemSpacing self.activeContentItemIdUpdated = activeContentItemIdUpdated self.activeContentItemMapping = activeContentItemMapping self.reorderItems = reorderItems @@ -1268,6 +1277,15 @@ public final class EntityKeyboardTopPanelComponent: Component { if lhs.forceActiveItemId != rhs.forceActiveItemId { return false } + if lhs.displayHighlightInExpanded != rhs.displayHighlightInExpanded { + return false + } + if lhs.automaticallySelectsFirstItem != rhs.automaticallySelectsFirstItem { + return false + } + if lhs.itemSpacing != rhs.itemSpacing { + return false + } if lhs.activeContentItemIdUpdated !== rhs.activeContentItemIdUpdated { return false } @@ -1393,7 +1411,7 @@ public final class EntityKeyboardTopPanelComponent: Component { let isExpanded: Bool let items: [Item] - init(isExpanded: Bool, containerSideInset: CGFloat, height: CGFloat, items: [ItemDescription]) { + init(isExpanded: Bool, containerSideInset: CGFloat, height: CGFloat, itemSpacing: CGFloat, items: [ItemDescription]) { self.sideInset = containerSideInset + 7.0 self.isExpanded = isExpanded @@ -1401,7 +1419,7 @@ public final class EntityKeyboardTopPanelComponent: Component { self.staticItemSize = self.itemSize self.staticExpandedItemSize = self.isExpanded ? self.staticItemSize : CGSize(width: 134.0, height: 28.0) self.innerItemSize = self.isExpanded ? CGSize(width: 50.0, height: 62.0) : CGSize(width: 24.0, height: 24.0) - self.itemSpacing = 8.0 + self.itemSpacing = itemSpacing var contentSize = CGSize(width: sideInset, height: height) var resultItems: [Item] = [] @@ -2002,6 +2020,9 @@ public final class EntityKeyboardTopPanelComponent: Component { if let forceActiveItemId = component.forceActiveItemId { self.activeContentItemId = forceActiveItemId + } else if component.defaultActiveItemId == nil && !component.automaticallySelectsFirstItem { + self.activeContentItemId = nil + self.activeSubcontentItemId = nil } else if self.activeContentItemId == nil, let defaultActiveItemId = component.defaultActiveItemId { self.activeContentItemId = defaultActiveItemId } @@ -2045,12 +2066,12 @@ public final class EntityKeyboardTopPanelComponent: Component { } self.items = items - if self.activeContentItemId == nil { + if self.activeContentItemId == nil && component.automaticallySelectsFirstItem { self.activeContentItemId = items.first?.id } let previousItemLayout = self.itemLayout - let itemLayout = ItemLayout(isExpanded: isExpanded, containerSideInset: component.containerSideInset, height: availableSize.height, items: self.items.map { item -> ItemLayout.ItemDescription in + let itemLayout = ItemLayout(isExpanded: isExpanded, containerSideInset: component.containerSideInset, height: availableSize.height, itemSpacing: component.itemSpacing, items: self.items.map { item -> ItemLayout.ItemDescription in let isStatic = item.id == AnyHashable("static") return ItemLayout.ItemDescription( isStatic: isStatic, @@ -2063,7 +2084,14 @@ public final class EntityKeyboardTopPanelComponent: Component { var updatedBounds: CGRect? if wasExpanded != isExpanded, let previousItemLayout = previousItemLayout { - if !isExpanded { + let keepInitialScrollOffset = component.displayHighlightInExpanded && self.scrollView.bounds.origin.x <= 1.0 + if keepInitialScrollOffset { + let maxContentOffsetX = max(0.0, itemLayout.contentSize.width - availableSize.width) + updatedBounds = CGRect( + origin: CGPoint(x: min(max(0.0, self.scrollView.bounds.origin.x), maxContentOffsetX), y: 0.0), + size: availableSize + ) + } else if !isExpanded { if let draggingEndOffset = self.draggingEndOffset { if abs(self.scrollView.contentOffset.x - draggingEndOffset) > 16.0 { self.draggingFocusItemIndex = nil @@ -2073,105 +2101,107 @@ public final class EntityKeyboardTopPanelComponent: Component { } } - var visibleBounds = self.scrollView.bounds - visibleBounds.origin.x -= 280.0 - visibleBounds.size.width += 560.0 - - let previousVisibleRange = previousItemLayout.visibleItemRange(for: visibleBounds) - if previousVisibleRange.minIndex <= previousVisibleRange.maxIndex { - var itemIndex = self.draggingFocusItemIndex ?? ((previousVisibleRange.minIndex + previousVisibleRange.maxIndex) / 2) - if !isExpanded { - if self.scrollView.bounds.maxX >= self.scrollView.contentSize.width { - itemIndex = component.items.count - 1 - } - if self.scrollView.bounds.minX <= 0.0 { - itemIndex = 0 - } - } + if !keepInitialScrollOffset { + var visibleBounds = self.scrollView.bounds + visibleBounds.origin.x -= 280.0 + visibleBounds.size.width += 560.0 - var previousItemFrame = previousItemLayout.containerFrame(at: itemIndex) - var updatedItemFrame = itemLayout.containerFrame(at: itemIndex) - - let previousDistanceToItem = (previousItemFrame.minX - self.scrollView.bounds.minX) - let previousDistanceToItemRight = (previousItemFrame.maxX - self.scrollView.bounds.maxX) - var newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - previousDistanceToItem, y: 0.0), size: availableSize) - var useRightAnchor = false - if newBounds.minX > itemLayout.contentSize.width - self.scrollView.bounds.width { - newBounds.origin.x = itemLayout.contentSize.width - self.scrollView.bounds.width - itemIndex = component.items.count - 1 - useRightAnchor = true - } - if itemIndex == component.items.count - 1 { - useRightAnchor = true - } - if newBounds.minX < 0.0 { - newBounds.origin.x = 0.0 - itemIndex = 0 - useRightAnchor = false - } - - if useRightAnchor { - let _ = previousDistanceToItemRight - newBounds.origin.x = itemLayout.contentSize.width - self.scrollView.bounds.width - } - - previousItemFrame = previousItemLayout.containerFrame(at: itemIndex) - updatedItemFrame = itemLayout.containerFrame(at: itemIndex) - - self.draggingFocusItemIndex = itemIndex - - updatedBounds = newBounds - - var updatedVisibleBounds = newBounds - updatedVisibleBounds.origin.x -= 280.0 - updatedVisibleBounds.size.width += 560.0 - let updatedVisibleRange = itemLayout.visibleItemRange(for: updatedVisibleBounds) - - if useRightAnchor { - let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.maxX - previousItemFrame.width, y: previousItemFrame.minY), size: previousItemFrame.size) - for index in updatedVisibleRange.minIndex ... updatedVisibleRange.maxIndex { - let indexDifference = index - itemIndex - if let itemView = self.itemViews[self.items[index].id] { - let itemContainerMaxX = baseFrame.maxX + CGFloat(indexDifference) * (previousItemLayout.itemSize.width + previousItemLayout.itemSpacing) - let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerMaxX - baseFrame.width, y: baseFrame.minY), size: baseFrame.size) - let itemOuterFrame = previousItemLayout.contentFrame(index: index, containerFrame: itemContainerFrame) - - let itemSize = itemView.bounds.size - itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize) - - if let activeContentItemId = self.activeContentItemId, activeContentItemId == self.items[index].id { - self.highlightedIconBackgroundView.frame = itemOuterFrame - self.highlightedIconTintBackgroundView.frame = itemOuterFrame - } + let previousVisibleRange = previousItemLayout.visibleItemRange(for: visibleBounds) + if previousVisibleRange.minIndex <= previousVisibleRange.maxIndex { + var itemIndex = self.draggingFocusItemIndex ?? ((previousVisibleRange.minIndex + previousVisibleRange.maxIndex) / 2) + if !isExpanded { + if self.scrollView.bounds.maxX >= self.scrollView.contentSize.width { + itemIndex = component.items.count - 1 + } + if self.scrollView.bounds.minX <= 0.0 { + itemIndex = 0 } } - } else { - let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.minX, y: previousItemFrame.minY), size: previousItemFrame.size) - for index in updatedVisibleRange.minIndex ... updatedVisibleRange.maxIndex { - let indexDifference = index - itemIndex - if let itemView = self.itemViews[self.items[index].id] { - var itemContainerOriginX = baseFrame.minX - if indexDifference > 0 { - for i in 0 ..< indexDifference { - itemContainerOriginX += previousItemLayout.itemSpacing - itemContainerOriginX += previousItemLayout.containerFrame(at: itemIndex + i).width - } - } else if indexDifference < 0 { - for i in 0 ..< (-indexDifference) { - itemContainerOriginX -= previousItemLayout.itemSpacing - itemContainerOriginX -= previousItemLayout.containerFrame(at: itemIndex - i - 1).width + + var previousItemFrame = previousItemLayout.containerFrame(at: itemIndex) + var updatedItemFrame = itemLayout.containerFrame(at: itemIndex) + + let previousDistanceToItem = (previousItemFrame.minX - self.scrollView.bounds.minX) + let previousDistanceToItemRight = (previousItemFrame.maxX - self.scrollView.bounds.maxX) + var newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - previousDistanceToItem, y: 0.0), size: availableSize) + var useRightAnchor = false + if newBounds.minX > itemLayout.contentSize.width - self.scrollView.bounds.width { + newBounds.origin.x = itemLayout.contentSize.width - self.scrollView.bounds.width + itemIndex = component.items.count - 1 + useRightAnchor = true + } + if itemIndex == component.items.count - 1 { + useRightAnchor = true + } + if newBounds.minX < 0.0 { + newBounds.origin.x = 0.0 + itemIndex = 0 + useRightAnchor = false + } + + if useRightAnchor { + let _ = previousDistanceToItemRight + newBounds.origin.x = itemLayout.contentSize.width - self.scrollView.bounds.width + } + + previousItemFrame = previousItemLayout.containerFrame(at: itemIndex) + updatedItemFrame = itemLayout.containerFrame(at: itemIndex) + + self.draggingFocusItemIndex = itemIndex + + updatedBounds = newBounds + + var updatedVisibleBounds = newBounds + updatedVisibleBounds.origin.x -= 280.0 + updatedVisibleBounds.size.width += 560.0 + let updatedVisibleRange = itemLayout.visibleItemRange(for: updatedVisibleBounds) + + if useRightAnchor { + let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.maxX - previousItemFrame.width, y: previousItemFrame.minY), size: previousItemFrame.size) + for index in updatedVisibleRange.minIndex ... updatedVisibleRange.maxIndex { + let indexDifference = index - itemIndex + if let itemView = self.itemViews[self.items[index].id] { + let itemContainerMaxX = baseFrame.maxX + CGFloat(indexDifference) * (previousItemLayout.itemSize.width + previousItemLayout.itemSpacing) + let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerMaxX - baseFrame.width, y: baseFrame.minY), size: baseFrame.size) + let itemOuterFrame = previousItemLayout.contentFrame(index: index, containerFrame: itemContainerFrame) + + let itemSize = itemView.bounds.size + itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize) + + if let activeContentItemId = self.activeContentItemId, activeContentItemId == self.items[index].id { + self.highlightedIconBackgroundView.frame = itemOuterFrame + self.highlightedIconTintBackgroundView.frame = itemOuterFrame } } - - let previousContainerFrame = previousItemLayout.containerFrame(at: index) - let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerOriginX, y: previousContainerFrame.minY), size: previousContainerFrame.size) - let itemOuterFrame = previousItemLayout.contentFrame(index: index, containerFrame: itemContainerFrame) - - let itemSize = itemView.bounds.size - itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize) - - if let activeContentItemId = self.activeContentItemId, activeContentItemId == self.items[index].id { - self.highlightedIconBackgroundView.frame = itemOuterFrame + } + } else { + let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.minX, y: previousItemFrame.minY), size: previousItemFrame.size) + for index in updatedVisibleRange.minIndex ... updatedVisibleRange.maxIndex { + let indexDifference = index - itemIndex + if let itemView = self.itemViews[self.items[index].id] { + var itemContainerOriginX = baseFrame.minX + if indexDifference > 0 { + for i in 0 ..< indexDifference { + itemContainerOriginX += previousItemLayout.itemSpacing + itemContainerOriginX += previousItemLayout.containerFrame(at: itemIndex + i).width + } + } else if indexDifference < 0 { + for i in 0 ..< (-indexDifference) { + itemContainerOriginX -= previousItemLayout.itemSpacing + itemContainerOriginX -= previousItemLayout.containerFrame(at: itemIndex - i - 1).width + } + } + + let previousContainerFrame = previousItemLayout.containerFrame(at: index) + let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerOriginX, y: previousContainerFrame.minY), size: previousContainerFrame.size) + let itemOuterFrame = previousItemLayout.contentFrame(index: index, containerFrame: itemContainerFrame) + + let itemSize = itemView.bounds.size + itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize) + + if let activeContentItemId = self.activeContentItemId, activeContentItemId == self.items[index].id { + self.highlightedIconBackgroundView.frame = itemOuterFrame + } } } } @@ -2197,7 +2227,10 @@ public final class EntityKeyboardTopPanelComponent: Component { if let activeContentItemId = self.activeContentItemId { if let index = self.items.firstIndex(where: { $0.id == activeContentItemId }) { - let itemFrame = itemLayout.containerFrame(at: index) + var itemFrame = itemLayout.containerFrame(at: index) + if isExpanded && component.displayHighlightInExpanded { + itemFrame = CGRect(origin: CGPoint(x: itemFrame.midX - itemFrame.height / 2.0, y: itemFrame.minY), size: CGSize(width: itemFrame.height, height: itemFrame.height)) + } var highlightTransition = transition if self.highlightedIconBackgroundView.isHidden { @@ -2228,8 +2261,9 @@ public final class EntityKeyboardTopPanelComponent: Component { self.highlightedIconBackgroundView.isHidden = true self.highlightedIconTintBackgroundView.isHidden = true } - transition.setAlpha(view: self.highlightedIconBackgroundView, alpha: isExpanded ? 0.0 : 1.0) - transition.setAlpha(view: self.highlightedIconTintBackgroundView, alpha: isExpanded ? 0.0 : 1.0) + let highlightAlpha: CGFloat = isExpanded && !component.displayHighlightInExpanded ? 0.0 : 1.0 + transition.setAlpha(view: self.highlightedIconBackgroundView, alpha: highlightAlpha) + transition.setAlpha(view: self.highlightedIconTintBackgroundView, alpha: highlightAlpha) panelEnvironment.visibilityFractionUpdated.connect { [weak self] (fraction, transition) in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift index 05ba06d5b9..a1328d0d09 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntitySearchContentComponent.swift @@ -5,7 +5,6 @@ import ComponentFlow import PagerComponent import TelegramPresentationData import TelegramCore -import Postbox import AnimationCache import MultiAnimationRenderer import AccountContext diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift index 7de681f13f..114955ef56 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift @@ -8,6 +8,7 @@ import Postbox import SwiftSignalKit import MultiAnimationRenderer import AnimationCache +import DCTMultiAnimationRendererImpl import AccountContext import TelegramUIPreferences import GenerateStickerPlaceholderImage @@ -277,7 +278,7 @@ public final class InlineFileIconLayer: MultiAnimationRenderTarget { size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in if !result { - MultiAnimationRendererImpl.firstFrameQueue.async { + DCTMultiAnimationRendererImpl.firstFrameQueue.async { let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) DispatchQueue.main.async { diff --git a/submodules/TelegramUI/Components/FaceScanScreen/Sources/AgeVerification.swift b/submodules/TelegramUI/Components/FaceScanScreen/Sources/AgeVerification.swift index 92288fabc3..2745c356db 100644 --- a/submodules/TelegramUI/Components/FaceScanScreen/Sources/AgeVerification.swift +++ b/submodules/TelegramUI/Components/FaceScanScreen/Sources/AgeVerification.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import SwiftSignalKit -import Postbox import TelegramCore import AccountContext import FileMediaResourceStatus @@ -74,7 +73,7 @@ public func ageVerificationAvailability(context: AccountContext) -> Signal take(1) |> mapToSignal { maybeFileAndMessage -> Signal in if let (file, message) = maybeFileAndMessage { - let fetchedData = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .file, reference: FileMediaReference.message(message: MessageReference(message._asMessage()), media: file).resourceReference(file.resource)) + let fetchedData = context.engine.resources.fetch(reference: FileMediaReference.message(message: MessageReference(message._asMessage()), media: file).resourceReference(file.resource), userLocation: .other, userContentType: .file) enum FetchStatus { case completed(String) @@ -84,8 +83,8 @@ public func ageVerificationAvailability(context: AccountContext) -> Signal { subscriber in let fetchedDisposable = fetchedData.start() - let resourceDataDisposable = context.account.postbox.mediaBox.resourceData(file.resource, attemptSynchronously: false).start(next: { next in - if next.complete { + let resourceDataDisposable = context.engine.resources.data(resource: EngineMediaResource(file.resource)).start(next: { next in + if next.isComplete { SSZipArchive.unzipFile(atPath: next.path, toDestination: NSTemporaryDirectory()) subscriber.putNext(.completed(compiledModelPath)) subscriber.putCompletion() diff --git a/submodules/TelegramUI/Components/FaceScanScreen/Sources/AgeVerificationScreen.swift b/submodules/TelegramUI/Components/FaceScanScreen/Sources/AgeVerificationScreen.swift index 431f4cd4c1..b9d0243ea6 100644 --- a/submodules/TelegramUI/Components/FaceScanScreen/Sources/AgeVerificationScreen.swift +++ b/submodules/TelegramUI/Components/FaceScanScreen/Sources/AgeVerificationScreen.swift @@ -28,7 +28,7 @@ public func requireAgeVerification(context: AccountContext) -> Bool { } public func requireAgeVerification(context: AccountContext, peer: EnginePeer) -> Signal { - if requireAgeVerification(context: context), peer._asPeer().hasSensitiveContent(platform: "ios") { + if requireAgeVerification(context: context), peer.hasSensitiveContent(platform: "ios") { return context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.ContentSettings()) |> map { contentSettings in if !contentSettings.ignoreContentRestrictionReasons.contains("sensitive") { diff --git a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift index 0a0b5a4028..56160337cd 100644 --- a/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift +++ b/submodules/TelegramUI/Components/ForumCreateTopicScreen/Sources/ForumCreateTopicScreen.swift @@ -1020,10 +1020,10 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { switch mode { case .create: title = presentationData.strings.CreateTopic_CreateTitle - doneTitle = presentationData.strings.CreateTopic_Create + doneTitle = "___done" case let .edit(threadId, topic, isHidden): title = presentationData.strings.CreateTopic_EditTitle - doneTitle = presentationData.strings.Common_Done + doneTitle = "___done" self.state = (topic.title, topic.icon, topic.iconColor, threadId == 1 ? isHidden : nil) } @@ -1032,7 +1032,7 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer { self.readyValue.set(componentReady.get() |> timeout(0.3, queue: .mainQueue(), alternate: .single(true))) - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) self.doneBarItem = UIBarButtonItem(title: doneTitle, style: .done, target: self, action: #selector(self.createPressed)) self.navigationItem.rightBarButtonItem = self.doneBarItem diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD index 9579870f15..1569acc58e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index 35cd956873..f4f94e025f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -226,7 +226,7 @@ public final class GiftCompositionComponent: Component { let node = DefaultAnimatedStickerNodeImpl() node.isUserInteractionEnabled = false - let pathPrefix = self.component!.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let pathPrefix = self.component!.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.resource.id)) node.setup( source: AnimatedStickerResourceSource(account: self.component!.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(geom.iconSize.width * 1.6), @@ -345,7 +345,7 @@ public final class GiftCompositionComponent: Component { let node = DefaultAnimatedStickerNodeImpl() node.isUserInteractionEnabled = false - let pathPrefix = self.component!.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let pathPrefix = self.component!.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.resource.id)) node.setup( source: AnimatedStickerResourceSource(account: self.component!.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(iconSize.width * 1.6), @@ -957,7 +957,7 @@ public final class GiftCompositionComponent: Component { node.isUserInteractionEnabled = false self.animationNode = node self.addSubview(node.view) - let pathPrefix = component.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let pathPrefix = component.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.resource.id)) node.setup(source: AnimatedStickerResourceSource(account: component.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(iconSize.width * 1.6), height: Int(iconSize.height * 1.6), playbackMode: loop ? .loop : .once, diff --git a/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/BUILD index 152dfb21de..5804b8f474 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift index 0935ae9f48..6601741a90 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import AccountContext diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD index fc62375e5c..729f8a2dc2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 840df9d0fb..73e329bf11 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -345,6 +345,7 @@ public final class GiftItemComponent: Component { private var selectionLayer: SimpleShapeLayer? private var checkLayer: CheckLayer? private var outlineLayer: SimpleLayer? + private var outlineRenderState: OutlineRenderState? private var animationFile: TelegramMediaFile? @@ -361,6 +362,14 @@ public final class GiftItemComponent: Component { private var giftAuctionTimer: SwiftSignalKit.Timer? + private struct OutlineRenderState: Equatable { + let size: CGSize + let cornerRadius: CGFloat + let outline: GiftItemComponent.Outline + let hasRibbon: Bool + let themeId: ObjectIdentifier + } + public var pattern: UIView? { if let view = self.patternView.view { return view @@ -1537,6 +1546,14 @@ public final class GiftItemComponent: Component { if let outline = component.outline { let lineWidth: CGFloat = 2.0 let outlineFrame = backgroundFrame + let hasRibbon = self.ribbon.layer.superlayer != nil + let outlineRenderState = OutlineRenderState( + size: outlineFrame.size, + cornerRadius: cornerRadius, + outline: outline, + hasRibbon: hasRibbon, + themeId: ObjectIdentifier(component.theme) + ) let outlineLayer: SimpleLayer if let current = self.outlineLayer { @@ -1544,12 +1561,15 @@ public final class GiftItemComponent: Component { } else { outlineLayer = SimpleLayer() self.outlineLayer = outlineLayer - if self.ribbon.layer.superlayer != nil { - self.layer.insertSublayer(outlineLayer, below: self.ribbon.layer) - } else { - self.layer.addSublayer(outlineLayer) - } - + } + + if hasRibbon { + self.layer.insertSublayer(outlineLayer, below: self.ribbon.layer) + } else if outlineLayer.superlayer == nil { + self.layer.addSublayer(outlineLayer) + } + + if self.outlineRenderState != outlineRenderState { let image = generateImage(outlineFrame.size, rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) @@ -1588,11 +1608,13 @@ public final class GiftItemComponent: Component { } }) outlineLayer.contents = image?.cgImage - - outlineLayer.frame = outlineFrame + self.outlineRenderState = outlineRenderState } + + transition.setFrame(layer: outlineLayer, frame: outlineFrame) } else if let outlineLayer = self.outlineLayer { self.outlineLayer = nil + self.outlineRenderState = nil outlineLayer.removeFromSuperlayer() } diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index 5dd8687f81..c094de4738 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -927,7 +927,7 @@ final class GiftOptionsScreenComponent: Component { if let controller = context.sharedContext.makePeerInfoController( context: context, updatedPresentationData: nil, - peer: peer._asPeer(), + peer: peer, mode: .gifts, avatarInitiallyExpanded: false, fromChat: false, diff --git a/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/BUILD b/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/BUILD index 71148481be..1ea1bebc03 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftRemainingCountComponent/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 36637edf47..840e88d024 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -104,6 +104,7 @@ private final class GiftSetupScreenComponent: Component { private let backgroundHandleView: UIImageView private let closeButton = ComponentView() + private var doneButton: ComponentView? private let remainingCount = ComponentView() private let auctionFooter = ComponentView() @@ -766,7 +767,6 @@ private final class GiftSetupScreenComponent: Component { mode: .standard(.default), chatLocation: .peer(id: component.context.account.peerId), subject: nil, - peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, @@ -778,8 +778,6 @@ private final class GiftSetupScreenComponent: Component { businessIntro: nil ) - self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor - let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) let inputNodeHeight = heightAndOverflow.0 let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) @@ -1150,6 +1148,65 @@ private final class GiftSetupScreenComponent: Component { transition.setFrame(view: closeButtonView, frame: closeButtonFrame) } + if environment.inputHeight > 0.0 { + var doneButtonTransition = transition + let doneButton: ComponentView + if let current = self.doneButton { + doneButton = current + } else { + doneButton = ComponentView() + self.doneButton = doneButton + doneButtonTransition = .immediate + } + + let doneButtonSize = doneButton.update( + transition: .immediate, + component: AnyComponent(GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: environment.theme.list.itemCheckColors.fillColor, + isDark: environment.theme.overallDarkAppearance, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "done", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Done", + tintColor: environment.theme.list.itemCheckColors.foregroundColor + ) + )), + action: { [weak self] _ in + self?.proceed() + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let doneButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - rawSideInset - 16.0 - doneButtonSize.width, y: 16.0), size: doneButtonSize) + if let doneButtonView = doneButton.view { + if doneButtonView.superview == nil { + doneButtonView.layer.rasterizationScale = UIScreenScale + self.navigationBarContainer.addSubview(doneButtonView) + + if !transition.animation.isImmediate { + doneButtonView.layer.shouldRasterize = true + transition.animateScale(view: doneButtonView, from: 0.01, to: 1.0, completion: { _ in + doneButtonView.layer.shouldRasterize = false + }) + transition.animateAlpha(view: doneButtonView, from: 0.0, to: 1.0) + } + } + doneButtonTransition.setPosition(view: doneButtonView, position: doneButtonFrame.center) + doneButtonTransition.setBounds(view: doneButtonView, bounds: CGRect(origin: .zero, size: doneButtonFrame.size)) + } + } else if let doneButton = self.doneButton { + self.doneButton = nil + if let doneButtonView = doneButton.view { + doneButtonView.layer.shouldRasterize = true + doneButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + doneButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + doneButtonView.removeFromSuperview() + }) + } + } + let containerInset: CGFloat = environment.statusBarHeight + 10.0 var initialContentHeight = contentHeight @@ -1409,14 +1466,15 @@ private final class GiftSetupScreenComponent: Component { ) )), ], alignment: .left, spacing: 2.0)), - accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: presentationStringsFormattedNumber(Int32(availability.resale), environment.dateTimeFormat.groupingSeparator), font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: theme.list.itemSecondaryTextColor )), - maximumNumberOfLines: 0 - ))), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 16.0))), + maximumNumberOfLines: 1 + )))), + accessory: .arrow, action: { [weak self] _ in guard let self, let component = self.component, let controller = environment.controller() else { return diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/SearchContextItem.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/SearchContextItem.swift index 2ce78ae467..ad05c448c1 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/SearchContextItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/SearchContextItem.swift @@ -4,7 +4,6 @@ import AsyncDisplayKit import Display import ComponentFlow import SwiftSignalKit -import Postbox import TelegramCore import AccountContext import TelegramPresentationData diff --git a/submodules/TelegramUI/Components/Gifts/GiftUnpinScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftUnpinScreen/BUILD index 87a0e849f6..0ff4e96a58 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftUnpinScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftUnpinScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramPresentationData", 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/GiftAuctionInfoScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift index 639cfdbca7..8233b0ae21 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import AccountContext 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/GiftOfferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift index 0c6c076076..3aa503fbd8 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift @@ -4,7 +4,6 @@ import AsyncDisplayKit import Display import ComponentFlow import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import AccountContext diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift index 603c4f3692..0cae6f8d8a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift @@ -4,7 +4,6 @@ import AsyncDisplayKit import Display import ComponentFlow import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift index f461e20c20..179a7259a3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftRemoveInfoAlertController.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import ComponentFlow -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftSaleAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftSaleAlertController.swift index 9d26d18013..322655a19f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftSaleAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftSaleAlertController.swift @@ -4,7 +4,6 @@ import AsyncDisplayKit import Display import ComponentFlow import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift index 1d3fae6d47..93fee375a2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import ComponentFlow -import Postbox import TelegramCore import TelegramPresentationData import AccountContext diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift index 053e762281..9da9a67308 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import AccountContext diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 0939675b93..2e66874c1f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -410,7 +410,7 @@ private final class GiftViewSheetContent: CombinedComponent { if let profileController = context.sharedContext.makePeerInfoController( context: context, updatedPresentationData: nil, - peer: peer._asPeer(), + peer: peer, mode: peer.id == context.account.peerId ? .myProfileGifts : .gifts, avatarInitiallyExpanded: false, fromChat: false, @@ -558,7 +558,7 @@ private final class GiftViewSheetContent: CombinedComponent { if let controller = self.context.sharedContext.makePeerInfoController( context: self.context, updatedPresentationData: nil, - peer: peer._asPeer(), + peer: peer, mode: giftsPeerId == self.context.account.peerId ? .myProfileGifts : .gifts, avatarInitiallyExpanded: false, fromChat: false, @@ -620,7 +620,7 @@ private final class GiftViewSheetContent: CombinedComponent { title: presentationData.strings.Gift_Convert_Title, text: text, actions: [ - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_Convert_Convert, action: { [weak self, weak controller, weak navigationController] in guard let self else { return @@ -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 @@ -2247,7 +2249,7 @@ private final class GiftViewSheetContent: CombinedComponent { if let controller = context.sharedContext.makePeerInfoController( context: context, updatedPresentationData: nil, - peer: peer._asPeer(), + peer: peer, mode: .upgradableGifts, avatarInitiallyExpanded: false, fromChat: false, diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift index e9621f1325..276c4b38b6 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftWithdrawAlertController.swift @@ -4,7 +4,6 @@ import AsyncDisplayKit import Display import ComponentFlow import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/BUILD b/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/BUILD index 363e3ee0c3..9cf0a4c016 100644 --- a/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/BUILD +++ b/submodules/TelegramUI/Components/Gifts/PeerTableCellComponent/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/ComponentFlow", "//submodules/TelegramPresentationData", "//submodules/TelegramCore", diff --git a/submodules/TelegramUI/Components/Gifts/TableComponent/BUILD b/submodules/TelegramUI/Components/Gifts/TableComponent/BUILD index 54dc0f8a77..d785b9a0f7 100644 --- a/submodules/TelegramUI/Components/Gifts/TableComponent/BUILD +++ b/submodules/TelegramUI/Components/Gifts/TableComponent/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/ComponentFlow", "//submodules/TelegramPresentationData", "//submodules/Components/MultilineTextComponent", diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift index 68bfbc9b92..040cc0c9de 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/GlassBackgroundComponent.swift @@ -541,18 +541,7 @@ public class GlassBackgroundView: UIView { } if let shadowView = self.shadowView { - let shadowInnerInset: CGFloat = 0.5 - shadowView.image = generateImage(CGSize(width: shadowInset * 2.0 + outerCornerRadius * 2.0, height: shadowInset * 2.0 + outerCornerRadius * 2.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(UIColor.black.cgColor) - context.setShadow(offset: CGSize(width: 0.0, height: 1.0), blur: 40.0, color: UIColor(white: 0.0, alpha: 0.04).cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowInset + shadowInnerInset, y: shadowInset + shadowInnerInset), size: CGSize(width: size.width - shadowInset * 2.0 - shadowInnerInset * 2.0, height: size.height - shadowInset * 2.0 - shadowInnerInset * 2.0))) - - context.setFillColor(UIColor.clear.cgColor) - context.setBlendMode(.copy) - context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowInset + shadowInnerInset, y: shadowInset + shadowInnerInset), size: CGSize(width: size.width - shadowInset * 2.0 - shadowInnerInset * 2.0, height: size.height - shadowInset * 2.0 - shadowInnerInset * 2.0))) - })?.stretchableImage(withLeftCapWidth: Int(shadowInset + outerCornerRadius), topCapHeight: Int(shadowInset + outerCornerRadius)) + shadowView.image = Self.generateLegacyShadowImage(cornerRadius: outerCornerRadius, shadowInset: shadowInset) transition.setAlpha(view: shadowView, alpha: isVisible ? 1.0 : 0.0) } @@ -867,6 +856,35 @@ private extension CGContext { } public extension GlassBackgroundView { + static func generateLegacyShadowImage(cornerRadius: CGFloat, shadowInset: CGFloat = 32.0, shadowIntensity: CGFloat = 0.04, shadowBlur: CGFloat = 40.0) -> UIImage? { + let shadowInnerInset: CGFloat = 0.5 + let diameter = max(1.0, cornerRadius * 2.0) + let size = CGSize(width: shadowInset * 2.0 + diameter, height: shadowInset * 2.0 + diameter) + + return generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let shadowRect = CGRect( + origin: CGPoint(x: shadowInset + shadowInnerInset, y: shadowInset + shadowInnerInset), + size: CGSize( + width: size.width - shadowInset * 2.0 - shadowInnerInset * 2.0, + height: size.height - shadowInset * 2.0 - shadowInnerInset * 2.0 + ) + ) + + context.setFillColor(UIColor.black.cgColor) + context.setShadow(offset: CGSize(width: 0.0, height: 1.0), blur: shadowBlur, color: UIColor(white: 0.0, alpha: shadowIntensity).cgColor) + context.fillEllipse(in: shadowRect) + + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: shadowRect) + })?.stretchableImage( + withLeftCapWidth: Int(shadowInset + cornerRadius), + topCapHeight: Int(shadowInset + cornerRadius) + ) + } + static func generateLegacyGlassImage(size: CGSize, inset: CGFloat, borderWidthFactor: CGFloat = 1.0, isDark: Bool, fillColor: UIColor) -> UIImage { var size = size if size == .zero { @@ -1147,19 +1165,22 @@ public final class GlassBackgroundComponent: Component { private let isDark: Bool private let tintColor: GlassBackgroundView.TintColor private let isInteractive: Bool + private let isVisible: Bool public init( size: CGSize, cornerRadius: CGFloat, isDark: Bool, tintColor: GlassBackgroundView.TintColor, - isInteractive: Bool = false + isInteractive: Bool = false, + isVisible: Bool = true ) { self.size = size self.cornerRadius = cornerRadius self.isDark = isDark self.tintColor = tintColor self.isInteractive = isInteractive + self.isVisible = isVisible } public static func == (lhs: GlassBackgroundComponent, rhs: GlassBackgroundComponent) -> Bool { @@ -1178,12 +1199,15 @@ public final class GlassBackgroundComponent: Component { if lhs.isInteractive != rhs.isInteractive { return false } + if lhs.isVisible != rhs.isVisible { + return false + } return true } public final class View: GlassBackgroundView { func update(component: GlassBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.update(size: component.size, cornerRadius: component.cornerRadius, isDark: component.isDark, tintColor: component.tintColor, isInteractive: component.isInteractive, transition: transition) + self.update(size: component.size, cornerRadius: component.cornerRadius, isDark: component.isDark, tintColor: component.tintColor, isInteractive: component.isInteractive, isVisible: component.isVisible, transition: transition) return component.size } diff --git a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/TouchEffect.swift b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/TouchEffect.swift index 643b4086aa..f8bd84416b 100644 --- a/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/TouchEffect.swift +++ b/submodules/TelegramUI/Components/GlassBackgroundComponent/Sources/TouchEffect.swift @@ -2,8 +2,8 @@ import Foundation import UIKit import Display -final class GlassHighlightGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { - var highlightContainerView: UIView? +public final class GlassHighlightGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { + public weak var highlightContainerView: UIView? private var touchEffect: TouchEffect? private var initialTouchLocation: CGPoint? @@ -14,7 +14,7 @@ final class GlassHighlightGestureRecognizer: UIGestureRecognizer, UIGestureRecog } } - override init(target: Any?, action: Selector?) { + public override init(target: Any?, action: Selector?) { super.init(target: target, action: action) self.delegate = self @@ -24,19 +24,19 @@ final class GlassHighlightGestureRecognizer: UIGestureRecognizer, UIGestureRecog self.requiresExclusiveTouchType = false } - override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool { + override public func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool { return false } - override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool { + override public func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool { return false } - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } - override func reset() { + override public func reset() { if let touchEffect = self.touchEffect { touchEffect.setIsTracking(false) } @@ -45,7 +45,7 @@ final class GlassHighlightGestureRecognizer: UIGestureRecognizer, UIGestureRecog self.initialTouchLocation = nil } - override func touchesBegan(_ touches: Set, with event: UIEvent) { + override public func touchesBegan(_ touches: Set, with event: UIEvent) { if let view = self.touchEffectView ?? self.view, let touch = touches.first { let touchLocation = touch.location(in: view) let touchEffect = TouchEffect(view: view, highlightContainerView: self.highlightContainerView) @@ -60,21 +60,21 @@ final class GlassHighlightGestureRecognizer: UIGestureRecognizer, UIGestureRecog } } - override func touchesEnded(_ touches: Set, with event: UIEvent) { + override public func touchesEnded(_ touches: Set, with event: UIEvent) { if let touchEffect = self.touchEffect { touchEffect.setIsTracking(false) } self.touchEffect = nil } - override func touchesCancelled(_ touches: Set, with event: UIEvent) { + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { if let touchEffect = self.touchEffect { touchEffect.setIsTracking(false) } self.touchEffect = nil } - override func touchesMoved(_ touches: Set, with event: UIEvent) { + override public func touchesMoved(_ touches: Set, with event: UIEvent) { guard let touchEffect = self.touchEffect, let view = self.touchEffectView ?? self.view, let touch = touches.first, diff --git a/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift b/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift index d42d3910c2..196523ecb5 100644 --- a/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift +++ b/submodules/TelegramUI/Components/GlobalControlPanelsContext/Sources/GlobalControlPanelsContext.swift @@ -316,9 +316,9 @@ public final class GlobalControlPanelsContext { if chatListNotices { let twoStepData: Signal = .single(nil) |> then(context.engine.auth.twoStepVerificationConfiguration() |> map(Optional.init)) - let accountFreezeConfiguration = (context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + let accountFreezeConfiguration = (context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) |> map { view -> AppConfiguration in - let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + let appConfiguration: AppConfiguration = view?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue return appConfiguration } |> distinctUntilChanged diff --git a/submodules/TelegramUI/Components/GroupCallHeaderPanelComponent/Sources/GroupCallHeaderPanelComponent.swift b/submodules/TelegramUI/Components/GroupCallHeaderPanelComponent/Sources/GroupCallHeaderPanelComponent.swift index bd3d0802c4..062981395e 100644 --- a/submodules/TelegramUI/Components/GroupCallHeaderPanelComponent/Sources/GroupCallHeaderPanelComponent.swift +++ b/submodules/TelegramUI/Components/GroupCallHeaderPanelComponent/Sources/GroupCallHeaderPanelComponent.swift @@ -8,7 +8,6 @@ import AccountContext import TelegramCore import GlobalControlPanelsContext import SwiftSignalKit -import Postbox import PresentationDataUtils public final class GroupCallHeaderPanelComponent: Component { diff --git a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerSearchNavigationContentNode.swift b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerSearchNavigationContentNode.swift index 591d1b55dc..41816a0de4 100644 --- a/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerSearchNavigationContentNode.swift +++ b/submodules/TelegramUI/Components/GroupStickerPackSetupController/Sources/GroupStickerSearchNavigationContentNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import ItemListUI diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index 596a7eb76b..25153b3fe8 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -127,8 +127,9 @@ private final class InteractiveTextNodeLine { var embeddedItems: [InteractiveTextNodeEmbeddedItem] var attachments: [InteractiveTextNodeAttachment] let additionalTrailingLine: (CTLine, Double)? - - init(line: CTLine, constrainedWidth: CGFloat, frame: CGRect, intrinsicWidth: CGFloat, ascent: CGFloat, descent: CGFloat, range: NSRange?, isTruncated: Bool, isRTL: Bool, strikethroughs: [InteractiveTextNodeStrikethrough], underlines: [InteractiveTextNodeStrikethrough], spoilers: [InteractiveTextNodeSpoiler], spoilerWords: [InteractiveTextNodeSpoiler], embeddedItems: [InteractiveTextNodeEmbeddedItem], attachments: [InteractiveTextNodeAttachment], additionalTrailingLine: (CTLine, Double)?) { + let characterRects: [CGRect]? + + init(line: CTLine, constrainedWidth: CGFloat, frame: CGRect, intrinsicWidth: CGFloat, ascent: CGFloat, descent: CGFloat, range: NSRange?, isTruncated: Bool, isRTL: Bool, strikethroughs: [InteractiveTextNodeStrikethrough], underlines: [InteractiveTextNodeStrikethrough], spoilers: [InteractiveTextNodeSpoiler], spoilerWords: [InteractiveTextNodeSpoiler], embeddedItems: [InteractiveTextNodeEmbeddedItem], attachments: [InteractiveTextNodeAttachment], additionalTrailingLine: (CTLine, Double)?, characterRects: [CGRect]?) { self.line = line self.constrainedWidth = constrainedWidth self.frame = frame @@ -145,6 +146,7 @@ private final class InteractiveTextNodeLine { self.embeddedItems = embeddedItems self.attachments = attachments self.additionalTrailingLine = additionalTrailingLine + self.characterRects = characterRects } } @@ -295,7 +297,9 @@ public final class InteractiveTextNodeLayoutArguments { public let displayContentsUnderSpoilers: Bool public let customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)? public let expandedBlocks: Set - + public let computeCharacterRects: Bool + public let minWidth: CGFloat? + public init( attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, @@ -314,7 +318,9 @@ public final class InteractiveTextNodeLayoutArguments { textStroke: (UIColor, CGFloat)? = nil, displayContentsUnderSpoilers: Bool = false, customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)? = nil, - expandedBlocks: Set = Set() + expandedBlocks: Set = Set(), + computeCharacterRects: Bool = false, + minWidth: CGFloat? = nil ) { self.attributedString = attributedString self.backgroundColor = backgroundColor @@ -334,8 +340,10 @@ public final class InteractiveTextNodeLayoutArguments { self.displayContentsUnderSpoilers = displayContentsUnderSpoilers self.customTruncationToken = customTruncationToken self.expandedBlocks = expandedBlocks + self.computeCharacterRects = computeCharacterRects + self.minWidth = minWidth } - + public func withAttributedString(_ attributedString: NSAttributedString?) -> InteractiveTextNodeLayoutArguments { return InteractiveTextNodeLayoutArguments( attributedString: attributedString, @@ -355,7 +363,9 @@ public final class InteractiveTextNodeLayoutArguments { textStroke: self.textStroke, displayContentsUnderSpoilers: self.displayContentsUnderSpoilers, customTruncationToken: self.customTruncationToken, - expandedBlocks: self.expandedBlocks + expandedBlocks: self.expandedBlocks, + computeCharacterRects: self.computeCharacterRects, + minWidth: self.minWidth ) } } @@ -418,6 +428,7 @@ public final class InteractiveTextNodeLayout: NSObject { fileprivate let textStroke: (UIColor, CGFloat)? public let displayContentsUnderSpoilers: Bool fileprivate let expandedBlocks: Set + public let minWidth: CGFloat? fileprivate init( attributedString: NSAttributedString?, @@ -441,7 +452,8 @@ public final class InteractiveTextNodeLayout: NSObject { textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displayContentsUnderSpoilers: Bool, - expandedBlocks: Set + expandedBlocks: Set, + minWidth: CGFloat? ) { self.attributedString = attributedString self.maximumNumberOfLines = maximumNumberOfLines @@ -465,6 +477,7 @@ public final class InteractiveTextNodeLayout: NSObject { self.textStroke = textStroke self.displayContentsUnderSpoilers = displayContentsUnderSpoilers self.expandedBlocks = expandedBlocks + self.minWidth = minWidth } func withUpdatedDisplayContentsUnderSpoilers(_ displayContentsUnderSpoilers: Bool) -> InteractiveTextNodeLayout { @@ -490,7 +503,8 @@ public final class InteractiveTextNodeLayout: NSObject { textShadowBlur: self.textShadowBlur, textStroke: self.textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, - expandedBlocks: self.expandedBlocks + expandedBlocks: self.expandedBlocks, + minWidth: self.minWidth ) } @@ -1040,74 +1054,133 @@ public final class InteractiveTextNodeLayout: NSObject { return nil } - public func layoutForGlyphCount(glyphCount: Int) -> TextNodeLayout.LayoutInfo { + public func layoutForCharacterCount(characterCount: Int) -> TextNodeLayout.LayoutInfo { var height: CGFloat = 0.0 if !self.segments.isEmpty, let line = self.segments[0].lines.first { height = line.frame.maxY } + var width: CGFloat = 0.0 - var count = 0 var trailingLineWidth: CGFloat = 0.0 + var lineCount = 0 + var remainingCharacters = characterCount + for segment in self.segments { for line in segment.lines { - if count >= glyphCount { + if remainingCharacters <= 0 { break } - let lineWidth: CGFloat - let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray - var maxGlyphUpperIndex: CFIndex = 0 - for run in glyphRuns { - let run = run as! CTRun - let rangeGlyphCount = CTRunGetGlyphCount(run) - let stringRange = CTRunGetStringRange(run) - if count + Int(rangeGlyphCount) > glyphCount { - if count < glyphCount { - let remainingGlyphCount = stringRange.location + glyphCount - count - if remainingGlyphCount > 0 { - var indices: [CFIndex] = Array(repeating: 0, count: remainingGlyphCount) - CTRunGetStringIndices(run, CFRangeMake(stringRange.location, stringRange.location + glyphCount - count), &indices) - if let maxIndex = indices.max() { - maxGlyphUpperIndex = max(maxIndex, maxGlyphUpperIndex) - } - } else { - assertionFailure() - } - } - } else { - if stringRange.length != 0 { - maxGlyphUpperIndex = max(stringRange.location + stringRange.length, maxGlyphUpperIndex) - } - } - count += Int(rangeGlyphCount) - } - height = max(height, line.frame.maxY) - if maxGlyphUpperIndex != 0 { - let lineRange = CTLineGetStringRange(line.line) - if maxGlyphUpperIndex < lineRange.location + lineRange.length { - let rightOffset = CTLineGetOffsetForStringIndex(line.line, maxGlyphUpperIndex + 1, nil) - lineWidth = rightOffset - } else { - lineWidth = line.frame.width - } + + let lineCharCount: Int + if let characterRects = line.characterRects { + lineCharCount = characterRects.count + } else if let range = line.range { + lineCharCount = range.length } else { - lineWidth = line.frame.width + let ctRange = CTLineGetStringRange(line.line) + lineCharCount = ctRange.length } - + + let lineWidth: CGFloat + if remainingCharacters >= lineCharCount { + lineWidth = line.frame.width + } else { + let lineRange: NSRange + if let range = line.range { + lineRange = range + } else { + let ctRange = CTLineGetStringRange(line.line) + lineRange = NSRange(location: ctRange.location, length: ctRange.length) + } + let cutStringIndex = lineRange.location + remainingCharacters + lineWidth = CGFloat(CTLineGetOffsetForStringIndex(line.line, cutStringIndex, nil)) + } + + height = max(height, line.frame.maxY) width = max(width, lineWidth) - trailingLineWidth = lineWidth + trailingLineWidth = lineWidth + self.insets.left + self.insets.right + 4.0 + lineCount += 1 + remainingCharacters -= lineCharCount } } - 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 + + if let minWidth = self.minWidth { + width = max(width, minWidth) + trailingLineWidth = max(trailingLineWidth, minWidth) + } + return TextNodeLayout.LayoutInfo( - size: CGSize(width: self.size.width, height: ceil(height)), + size: CGSize(width: width, height: ceil(height)), trailingLineWidth: trailingLineWidth ) } - - public func sizeForGlyphCount(glyphCount: Int) -> CGSize { - return self.layoutForGlyphCount(glyphCount: glyphCount).size + + public func sizeForCharacterCount(characterCount: Int) -> CGSize { + return self.layoutForCharacterCount(characterCount: characterCount).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.. [CGRect] { + var result = [CGRect](repeating: CGRect.zero, count: lineRange.length) + + let glyphRuns = CTLineGetGlyphRuns(line) as NSArray + for run in glyphRuns { + let run = run as! CTRun + let glyphCount = CTRunGetGlyphCount(run) + if glyphCount == 0 { + continue + } + + var glyphs = [CGGlyph](repeating: 0, count: glyphCount) + CTRunGetGlyphs(run, CFRangeMake(0, glyphCount), &glyphs) + + var positions = [CGPoint](repeating: CGPoint.zero, count: glyphCount) + CTRunGetPositions(run, CFRangeMake(0, glyphCount), &positions) + + var stringIndices = [CFIndex](repeating: 0, count: glyphCount) + CTRunGetStringIndices(run, CFRangeMake(0, glyphCount), &stringIndices) + + let attributes = CTRunGetAttributes(run) as NSDictionary + guard let font = attributes[kCTFontAttributeName] as! CTFont? else { + continue + } + + var boundingRects = [CGRect](repeating: CGRect.zero, count: glyphCount) + CTFontGetBoundingRectsForGlyphs(font, .default, &glyphs, &boundingRects, glyphCount) + + for i in 0 ..< glyphCount { + let charIndex = stringIndices[i] - lineRange.location + if charIndex >= 0 && charIndex < lineRange.length { + let pos = positions[i] + let bbox = boundingRects[i] + result[charIndex] = CGRect( + x: pos.x + bbox.origin.x, + y: pos.y + bbox.origin.y, + width: bbox.width, + height: bbox.height + ) + } + } + } + + return result +} + open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecognizerDelegate { public final class ApplyArguments { public let animation: ListViewItemUpdateAnimation @@ -1234,7 +1353,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn } public internal(set) var cachedLayout: InteractiveTextNodeLayout? - public internal(set) var revealGlyphCount: Int? + public internal(set) var revealCharacterCount: Int? public var renderContentTypes: RenderContentTypes = .all private var contentItemLayers: [Int: TextContentItemLayer] = [:] @@ -1368,7 +1487,9 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn textStroke: (UIColor, CGFloat)?, displayContentsUnderSpoilers: Bool, customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)?, - expandedBlocks: Set + expandedBlocks: Set, + computeCharacterRects: Bool = false, + minWidth: CGFloat? ) -> InteractiveTextNodeLayout { let blockQuoteLeftInset: CGFloat = 9.0 let blockQuoteRightInset: CGFloat = 0.0 @@ -1552,7 +1673,8 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn spoilerWords: [], embeddedItems: [], attachments: [], - additionalTrailingLine: nil + additionalTrailingLine: nil, + characterRects: nil ) additionalSegmentRightInset = 0.0 } @@ -1594,9 +1716,10 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn spoilerWords: [], embeddedItems: [], attachments: [], - additionalTrailingLine: nil + additionalTrailingLine: nil, + characterRects: computeCharacterRects ? computeCharacterRectsForLine(line: line, lineRange: NSRange(location: currentLineStartIndex, length: lineCharacterCount)) : nil )) - + remainingLines -= 1 if remainingLines <= 0 { break @@ -1653,7 +1776,8 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn spoilerWords: [], embeddedItems: [], attachments: [], - additionalTrailingLine: (truncationToken, 0.0) + additionalTrailingLine: (truncationToken, 0.0), + characterRects: computeCharacterRects ? computeCharacterRectsForLine(line: updatedLine, lineRange: lastLine.range ?? NSRange()) : nil ) } } @@ -1708,7 +1832,8 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn spoilerWords: [], embeddedItems: [], attachments: [], - additionalTrailingLine: (truncationToken, truncationTokenWidth) + additionalTrailingLine: (truncationToken, truncationTokenWidth), + characterRects: computeCharacterRects ? computeCharacterRectsForLine(line: updatedLine, lineRange: lastLine.range ?? NSRange()) : nil ) } } @@ -1936,6 +2061,10 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn size.width += insets.left + insets.right size.height += insets.top + insets.bottom + if let minWidth { + size.width = max(size.width, minWidth) + } + return InteractiveTextNodeLayout( attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, @@ -1958,16 +2087,17 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, - expandedBlocks: expandedBlocks + expandedBlocks: expandedBlocks, + minWidth: minWidth ) } - static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displayContentsUnderSpoilers: Bool, customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)?, expandedBlocks: Set) -> InteractiveTextNodeLayout { + static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textShadowBlur: CGFloat?, textStroke: (UIColor, CGFloat)?, displayContentsUnderSpoilers: Bool, customTruncationToken: ((UIFont, Bool) -> NSAttributedString?)?, expandedBlocks: Set, computeCharacterRects: Bool = false, minWidth: CGFloat? = nil) -> InteractiveTextNodeLayout { guard let attributedString else { - return InteractiveTextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, segments: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, expandedBlocks: expandedBlocks) + return InteractiveTextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, segments: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, expandedBlocks: expandedBlocks, minWidth: minWidth) } - return calculateLayoutV2(attributedString: attributedString, minimumNumberOfLines: minimumNumberOfLines, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, verticalAlignment: verticalAlignment, lineSpacingFactor: lineSpacingFactor, cutout: cutout, insets: insets, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, customTruncationToken: customTruncationToken, expandedBlocks: expandedBlocks) + return calculateLayoutV2(attributedString: attributedString, minimumNumberOfLines: minimumNumberOfLines, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, verticalAlignment: verticalAlignment, lineSpacingFactor: lineSpacingFactor, cutout: cutout, insets: insets, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, customTruncationToken: customTruncationToken, expandedBlocks: expandedBlocks, computeCharacterRects: computeCharacterRects, minWidth: minWidth) } private func updateContentItems(arguments: ApplyArguments) { @@ -2112,7 +2242,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn return { arguments in var layout: InteractiveTextNodeLayout - if let existingLayout = existingLayout, existingLayout.constrainedSize == arguments.constrainedSize && existingLayout.maximumNumberOfLines == arguments.maximumNumberOfLines && existingLayout.truncationType == arguments.truncationType && existingLayout.cutout == arguments.cutout && existingLayout.explicitAlignment == arguments.alignment && existingLayout.lineSpacing.isEqual(to: arguments.lineSpacing) && existingLayout.expandedBlocks == arguments.expandedBlocks { + if let existingLayout = existingLayout, existingLayout.constrainedSize == arguments.constrainedSize && existingLayout.maximumNumberOfLines == arguments.maximumNumberOfLines && existingLayout.truncationType == arguments.truncationType && existingLayout.cutout == arguments.cutout && existingLayout.explicitAlignment == arguments.alignment && existingLayout.lineSpacing.isEqual(to: arguments.lineSpacing) && existingLayout.expandedBlocks == arguments.expandedBlocks && existingLayout.minWidth == arguments.minWidth { let stringMatch: Bool var colorMatch: Bool = true @@ -2140,10 +2270,10 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn layout = layout.withUpdatedDisplayContentsUnderSpoilers(arguments.displayContentsUnderSpoilers) } } else { - layout = InteractiveTextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displayContentsUnderSpoilers: arguments.displayContentsUnderSpoilers, customTruncationToken: arguments.customTruncationToken, expandedBlocks: arguments.expandedBlocks) + layout = InteractiveTextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displayContentsUnderSpoilers: arguments.displayContentsUnderSpoilers, customTruncationToken: arguments.customTruncationToken, expandedBlocks: arguments.expandedBlocks, computeCharacterRects: arguments.computeCharacterRects, minWidth: arguments.minWidth) } } else { - layout = InteractiveTextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displayContentsUnderSpoilers: arguments.displayContentsUnderSpoilers, customTruncationToken: arguments.customTruncationToken, expandedBlocks: arguments.expandedBlocks) + layout = InteractiveTextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textShadowBlur: arguments.textShadowBlur, textStroke: arguments.textStroke, displayContentsUnderSpoilers: arguments.displayContentsUnderSpoilers, customTruncationToken: arguments.customTruncationToken, expandedBlocks: arguments.expandedBlocks, computeCharacterRects: arguments.computeCharacterRects, minWidth: arguments.minWidth) } let node = maybeNode ?? InteractiveTextNode() @@ -2179,41 +2309,43 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn return count } - - public func updateRevealGlyphCount(count: Int?) { + + public func updateRevealCharacterCount(count: Int?, animated: Bool) { + self.revealCharacterCount = count + guard let cachedLayout = self.cachedLayout else { return } - - self.revealGlyphCount = count - + if let count { var nextItemId = 0 var currentCount = 0 for segment in cachedLayout.segments { let itemId = nextItemId nextItemId += 1 - - var segmentGlyphCount = 0 + + var segmentCharCount = 0 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) - segmentGlyphCount += Int(glyphCount) + if let characterRects = line.characterRects { + segmentCharCount += characterRects.count + } else if let range = line.range { + segmentCharCount += range.length + } else { + let ctRange = CTLineGetStringRange(line.line) + segmentCharCount += ctRange.length } } - - let segmentInnerCount = min(max(0, count - currentCount), segmentGlyphCount) + + let segmentInnerCount = min(max(0, count - currentCount), segmentCharCount) if let item = self.contentItemLayers[itemId] { - item.updateMaxGlyphDrawCount(value: segmentInnerCount) + item.updateMaxCharacterDrawCount(value: segmentInnerCount, animated: animated) } - - currentCount += segmentGlyphCount + + currentCount += segmentCharCount } } else { for (_, item) in self.contentItemLayers { - item.updateMaxGlyphDrawCount(value: nil) + item.updateMaxCharacterDrawCount(value: nil, animated: animated) } } } @@ -2297,14 +2429,12 @@ final class TextContentItemLayer: SimpleLayer { let size: CGSize let item: TextContentItem let mask: RenderMask? - let maxGlyphDrawCount: Int? - - init(size: CGSize, item: TextContentItem, mask: RenderMask?, maxGlyphDrawCount: Int?) { + + init(size: CGSize, item: TextContentItem, mask: RenderMask?) { self.size = size self.item = item self.mask = mask - self.maxGlyphDrawCount = maxGlyphDrawCount - + super.init() } } @@ -2383,8 +2513,6 @@ final class TextContentItemLayer: SimpleLayer { let offset = params.item.contentOffset let alignment: NSTextAlignment = .left - var drawnGlyphCount = 0 - for i in 0 ..< params.item.segment.lines.count { let line = params.item.segment.lines[i] @@ -2415,14 +2543,6 @@ final class TextContentItemLayer: SimpleLayer { let run = run as! CTRun let glyphCount = CTRunGetGlyphCount(run) - var runDrawGlyphCount = glyphCount - if let maxGlyphDrawCount = params.maxGlyphDrawCount { - if drawnGlyphCount >= maxGlyphDrawCount { - break - } - runDrawGlyphCount = CFIndex(max(0, min(Int(glyphCount), maxGlyphDrawCount - drawnGlyphCount))) - } - let attributes = CTRunGetAttributes(run) as NSDictionary if attributes["Attribute__EmbeddedItem"] != nil { continue @@ -2472,14 +2592,12 @@ final class TextContentItemLayer: SimpleLayer { let stringRange = CTRunGetStringRange(run) if line.attachments.contains(where: { $0.range.contains(stringRange.location) }) { } else { - CTRunDraw(run, context, CFRangeMake(0, runDrawGlyphCount)) + CTRunDraw(run, context, CFRangeMake(0, glyphCount)) } } else { - CTRunDraw(run, context, CFRangeMake(0, runDrawGlyphCount)) + CTRunDraw(run, context, CFRangeMake(0, glyphCount)) } - - drawnGlyphCount += Int(glyphCount) - + if fixDoubleEmoji { context.setBlendMode(.normal) } @@ -2646,9 +2764,23 @@ final class TextContentItemLayer: SimpleLayer { } } - private(set) var params: Params? + private final class SnippetLayer: SimpleLayer { + let characterIndex: Int + + init(characterIndex: Int) { + self.characterIndex = characterIndex + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + private(set) var params: Params? + let renderNode: RenderNode + private let renderNodeContainer: SimpleLayer private var contentMaskNode: ASImageNode? private var overlayContentLayer: SimpleLayer? @@ -2662,21 +2794,28 @@ final class TextContentItemLayer: SimpleLayer { private var currentAnimationId: Int = 0 private var isAnimating: Bool = false private var currentContentMask: RenderMask? - - private var maxGlyphDrawCount: Int? - + private var revealMaskLayer: SimpleLayer? + private var revealLineMaskLayers: [SimpleLayer] = [] + + private var maxCharacterDrawCount: Int? + private var previousMaxCharacterDrawCount: Int = 0 + private var animatingSnippetLayers: [SnippetLayer] = [] + init(displaysAsynchronously: Bool) { self.renderNode = RenderNode() self.renderNode.displaysAsynchronously = displaysAsynchronously - + self.renderNodeContainer = SimpleLayer() + super.init() - - self.addSublayer(self.renderNode.layer) + + self.renderNodeContainer.addSublayer(self.renderNode.layer) + self.addSublayer(self.renderNodeContainer) } - + override init(layer: Any) { self.renderNode = RenderNode() - + self.renderNodeContainer = SimpleLayer() + super.init(layer: layer) } @@ -2684,17 +2823,294 @@ final class TextContentItemLayer: SimpleLayer { fatalError("init(coder:) has not been implemented") } - func updateMaxGlyphDrawCount(value: Int?) { - if self.maxGlyphDrawCount != value { - self.maxGlyphDrawCount = value - - if let renderParams = self.renderNode.params { - self.renderNode.params = RenderParams(size: renderParams.size, item: renderParams.item, mask: renderParams.mask, maxGlyphDrawCount: self.maxGlyphDrawCount) - self.renderNode.displayImmediately() + func updateMaxCharacterDrawCount(value: Int?, animated: Bool) { + if self.maxCharacterDrawCount == value { + return + } + self.maxCharacterDrawCount = value + self.updateRevealMask(animateNewSegments: animated) + } + + private struct RevealLineInfo { + let lineFrame: CGRect + let lineHeight: CGFloat + let revealedWidth: CGFloat + let isFull: Bool + let isRTL: Bool + } + + private func computeRevealedLines(lines: [InteractiveTextNodeLine], layerSize: CGSize, offset: CGPoint, characterLimit: Int) -> [RevealLineInfo] { + var result: [RevealLineInfo] = [] + var remainingCharacters = characterLimit + + for i in 0 ..< lines.count { + let line = lines[i] + + var lineFrame = line.frame + lineFrame.origin.y += offset.y + + if line.isRTL { + lineFrame.origin.x = offset.x + floor(layerSize.width - lineFrame.width) + lineFrame = displayLineFrame(frame: lineFrame, isRTL: true, boundingRect: CGRect(origin: CGPoint(), size: layerSize), cutout: nil) + } else { + lineFrame.origin.x += offset.x + } + + let lineHeight = line.ascent + line.descent + + guard let characterRects = line.characterRects else { + result.append(RevealLineInfo(lineFrame: lineFrame, lineHeight: lineHeight, revealedWidth: remainingCharacters > 0 ? lineFrame.width : 0.0, isFull: remainingCharacters > 0, isRTL: line.isRTL)) + continue + } + + if remainingCharacters <= 0 { + result.append(RevealLineInfo(lineFrame: lineFrame, lineHeight: lineHeight, revealedWidth: 0.0, isFull: false, isRTL: line.isRTL)) + continue + } + + let revealCount = min(characterRects.count, remainingCharacters) + var revealedWidth: CGFloat = 0.0 + if line.isRTL { + // Logical index 0 is the visually rightmost glyph in an RTL line, + // so the revealed extent grows leftward from the right edge. + var minX: CGFloat = .greatestFiniteMagnitude + for j in 0 ..< revealCount { + let rect = characterRects[j] + if !rect.isEmpty { + minX = min(minX, rect.minX) + } + } + if minX != .greatestFiniteMagnitude { + revealedWidth = ceil(lineFrame.width - minX) + } + } else { + for j in 0 ..< revealCount { + let rect = characterRects[j] + if !rect.isEmpty { + revealedWidth = max(revealedWidth, rect.maxX) + } + } + revealedWidth = ceil(revealedWidth) + } + + remainingCharacters -= characterRects.count + let isFull = remainingCharacters >= 0 + + result.append(RevealLineInfo(lineFrame: lineFrame, lineHeight: lineHeight, revealedWidth: revealedWidth, isFull: isFull, isRTL: line.isRTL)) + } + + return result + } + + private func updateRevealMask(animateNewSegments: Bool) { + guard let params = self.params else { + return + } + + let item = params.item + let lines = item.segment.lines + let layerSize = item.size + let offset = item.contentOffset + + let effectiveCharacterDrawCount: Int + if let maxCharacterDrawCount = self.maxCharacterDrawCount { + effectiveCharacterDrawCount = maxCharacterDrawCount + } else { + if self.previousMaxCharacterDrawCount > 0 || !self.animatingSnippetLayers.isEmpty { + // Reveal finished — compute total character count so mask and snippets + // can continue updating until all snippet animations complete + var totalCharCount = 0 + for line in lines { + if let characterRects = line.characterRects { + totalCharCount += characterRects.count + } else if let range = line.range { + totalCharCount += range.length + } else { + let ctRange = CTLineGetStringRange(line.line) + totalCharCount += ctRange.length + } + } + effectiveCharacterDrawCount = totalCharCount + } else { + // Nothing left to animate — remove the mask + if let _ = self.revealMaskLayer { + self.renderNodeContainer.mask = nil + self.revealMaskLayer = nil + self.revealLineMaskLayers.removeAll() + } + self.previousMaxCharacterDrawCount = 0 + return } } + + // Create or reuse the container mask layer + let revealMaskLayer: SimpleLayer + if let existing = self.revealMaskLayer { + revealMaskLayer = existing + } else { + revealMaskLayer = SimpleLayer() + revealMaskLayer.backgroundColor = UIColor.clear.cgColor + self.revealMaskLayer = revealMaskLayer + self.renderNodeContainer.mask = revealMaskLayer + } + revealMaskLayer.frame = CGRect(origin: CGPoint(), size: layerSize) + + // Compute current and previous reveal states + let currentLineInfos = self.computeRevealedLines(lines: lines, layerSize: layerSize, offset: offset, characterLimit: effectiveCharacterDrawCount) + + // Create snippet layers for newly revealed character rects + if self.previousMaxCharacterDrawCount < effectiveCharacterDrawCount, let contents = self.renderNode.layer.contents { + let containerOrigin = self.renderNodeContainer.frame.origin + + var previousRemaining = self.previousMaxCharacterDrawCount + var currentRemaining = effectiveCharacterDrawCount + var globalCharIndex = 0 + + for i in 0 ..< lines.count { + let line = lines[i] + let lineInfo = currentLineInfos[i] + + guard let characterRects = line.characterRects else { + continue + } + + let lineCharCount = characterRects.count + let prevCount = min(max(0, previousRemaining), lineCharCount) + let curCount = min(max(0, currentRemaining), lineCharCount) + + previousRemaining -= lineCharCount + currentRemaining -= lineCharCount + + if curCount <= prevCount { + globalCharIndex += lineCharCount + continue + } + + for j in prevCount ..< curCount { + let charRect = characterRects[j] + if charRect.isEmpty { + continue + } + + let snippetRect = CGRect( + x: lineInfo.lineFrame.minX + charRect.origin.x, + y: lineInfo.lineFrame.minY, + width: charRect.width, + height: lineInfo.lineHeight + ) + + if snippetRect.width < 0.5 { + continue + } + + let contentsRect = CGRect( + x: snippetRect.minX / layerSize.width, + y: snippetRect.minY / layerSize.height, + width: snippetRect.width / layerSize.width, + height: snippetRect.height / layerSize.height + ) + + let snippetLayer = SnippetLayer(characterIndex: globalCharIndex + j) + snippetLayer.contents = contents + snippetLayer.contentsRect = contentsRect + snippetLayer.contentsScale = self.renderNode.layer.contentsScale + snippetLayer.contentsGravity = self.renderNode.layer.contentsGravity + snippetLayer.frame = snippetRect.offsetBy(dx: containerOrigin.x, dy: containerOrigin.y) + + self.addSublayer(snippetLayer) + self.animatingSnippetLayers.append(snippetLayer) + + ComponentTransition(animation: .curve(duration: 0.22, curve: .easeInOut)).animateBlur(layer: snippetLayer, fromRadius: 2.0, toRadius: 0.0) + snippetLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + snippetLayer.animatePosition(from: CGPoint(x: 0.0, y: 6.0), to: CGPoint(), duration: 0.2, additive: true) + snippetLayer.animateScale(from: 0.5, to: 1.0, duration: 0.2, completion: { [weak self, weak snippetLayer] _ in + guard let self, let snippetLayer else { + return + } + snippetLayer.removeFromSuperlayer() + self.animatingSnippetLayers.removeAll(where: { $0 === snippetLayer }) + self.updateRevealMask(animateNewSegments: false) + }) + } + globalCharIndex += lineCharCount + } + } + + // Build mask rects — use the lowest animating snippet index as the mask limit + // so the mask never extends past any character still being animated + let maskCharacterLimit: Int + if let lowestAnimating = self.animatingSnippetLayers.min(by: { $0.characterIndex < $1.characterIndex })?.characterIndex { + maskCharacterLimit = lowestAnimating + } else { + maskCharacterLimit = self.previousMaxCharacterDrawCount + } + let maskLineInfos = self.computeRevealedLines(lines: lines, layerSize: layerSize, offset: offset, characterLimit: maskCharacterLimit) + + var maskRects: [CGRect] = [] + var mergeStartY: CGFloat? + var mergeEndY: CGFloat? + + for info in maskLineInfos { + if info.revealedWidth <= 0.0 { + continue + } + + let maskFrame: CGRect + if info.isFull { + maskFrame = CGRect(x: 0.0, y: info.lineFrame.minY, width: layerSize.width, height: info.lineHeight) + } else if info.isRTL { + maskFrame = CGRect(x: info.lineFrame.maxX - info.revealedWidth, y: info.lineFrame.minY, width: info.revealedWidth, height: info.lineHeight) + } else { + maskFrame = CGRect(x: info.lineFrame.minX, y: info.lineFrame.minY, width: info.revealedWidth, height: info.lineHeight) + } + + if info.isFull { + if mergeStartY != nil { + mergeEndY = maskFrame.maxY + } else { + mergeStartY = maskFrame.minY + mergeEndY = maskFrame.maxY + } + } else { + if let startY = mergeStartY, let endY = mergeEndY { + maskRects.append(CGRect(x: 0.0, y: startY, width: layerSize.width, height: endY - startY)) + mergeStartY = nil + mergeEndY = nil + } + maskRects.append(maskFrame) + } + } + if let startY = mergeStartY, let endY = mergeEndY { + maskRects.append(CGRect(x: 0.0, y: startY, width: layerSize.width, height: endY - startY)) + } + + // Update mask child layers + while self.revealLineMaskLayers.count < maskRects.count { + let childLayer = SimpleLayer() + childLayer.backgroundColor = UIColor.white.cgColor + revealMaskLayer.addSublayer(childLayer) + self.revealLineMaskLayers.append(childLayer) + } + while self.revealLineMaskLayers.count > maskRects.count { + let removed = self.revealLineMaskLayers.removeLast() + removed.removeFromSuperlayer() + } + + for i in 0 ..< maskRects.count { + self.revealLineMaskLayers[i].frame = maskRects[i] + } + + self.previousMaxCharacterDrawCount = effectiveCharacterDrawCount + + // If maxCharacterDrawCount is nil and all snippet animations have finished, clean up + if self.maxCharacterDrawCount == nil && self.animatingSnippetLayers.isEmpty { + self.renderNodeContainer.mask = nil + self.revealMaskLayer = nil + self.revealLineMaskLayers.removeAll() + self.previousMaxCharacterDrawCount = 0 + } } - + func update( params: Params, animation: ListViewItemUpdateAnimation, @@ -2830,7 +3246,8 @@ final class TextContentItemLayer: SimpleLayer { } } - animation.animator.updateFrame(layer: self.renderNode.layer, frame: effectiveContentFrame, completion: nil) + animation.animator.updateFrame(layer: self.renderNodeContainer, frame: effectiveContentFrame, completion: nil) + animation.animator.updateFrame(layer: self.renderNode.layer, frame: CGRect(origin: CGPoint(), size: effectiveContentFrame.size), completion: nil) var staticContentMask = contentMask if let contentMask, self.isAnimating { @@ -2947,10 +3364,11 @@ final class TextContentItemLayer: SimpleLayer { self.currentContentMask = contentMask - self.renderNode.params = RenderParams(size: contentFrame.size, item: params.item, mask: staticContentMask, maxGlyphDrawCount: self.maxGlyphDrawCount) + self.renderNode.params = RenderParams(size: contentFrame.size, item: params.item, mask: staticContentMask) + self.updateRevealMask(animateNewSegments: false) if synchronously { if let spoilerExpandRect, animation.isAnimated { - let localSpoilerExpandRect = spoilerExpandRect.offsetBy(dx: -self.renderNode.frame.minX, dy: -self.renderNode.frame.minY) + let localSpoilerExpandRect = spoilerExpandRect.offsetBy(dx: -self.renderNodeContainer.frame.minX, dy: -self.renderNodeContainer.frame.minY) let revealAnimationDuration: CGFloat = 0.55 @@ -2958,7 +3376,7 @@ final class TextContentItemLayer: SimpleLayer { let previousContents = self.renderNode.layer.contents let copyContentsLayer = SimpleLayer() - copyContentsLayer.frame = self.renderNode.frame + copyContentsLayer.frame = self.renderNodeContainer.frame copyContentsLayer.contents = previousContents copyContentsLayer.masksToBounds = self.renderNode.layer.masksToBounds copyContentsLayer.contentsGravity = self.renderNode.layer.contentsGravity @@ -2980,7 +3398,7 @@ final class TextContentItemLayer: SimpleLayer { copyContentsLayer.addSublayer(copySublayer) } - self.renderNode.layer.superlayer?.insertSublayer(copyContentsLayer, below: self.renderNode.layer) + self.renderNodeContainer.superlayer?.insertSublayer(copyContentsLayer, below: self.renderNodeContainer) self.renderNode.displayImmediately() diff --git a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift index 7c60e22cc6..145b14eac8 100644 --- a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift +++ b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift @@ -317,7 +317,7 @@ private final class JoinSubjectScreenComponent: Component { if let peerInfoController = context.sharedContext.makePeerInfoController( context: context, updatedPresentationData: nil, - peer: peer._asPeer(), + peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, diff --git a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift index 930f3250e0..0eb250bf4d 100644 --- a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift @@ -11,7 +11,7 @@ import ShareController import LegacyUI import LegacyMediaPickerUI -public func presentedLegacyCamera(context: AccountContext, peer: Peer?, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendPaidMessageStars: Int64 = 0, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ChatSendMessageActionSheetController.SendParameters?) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { +public func presentedLegacyCamera(context: AccountContext, peer: Peer?, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendPaidMessageStars: Int64 = 0, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ChatSendMessageActionSheetController.SendParameters?) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) @@ -45,8 +45,8 @@ public func presentedLegacyCamera(context: AccountContext, peer: Peer?, chatLoca } controller.presentScheduleController = { _, done in - presentSchedulePicker(true, { time in - done?(time) + presentSchedulePicker(true, { time, silentPosting in + done?(time, silentPosting) }) } controller.presentTimerController = { done in @@ -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/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift index 798088a4d5..8e8c81eb1d 100644 --- a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift +++ b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift @@ -133,7 +133,7 @@ public func legacyInputMicPalette(from theme: PresentationTheme) -> TGModernConv return TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: theme.rootController.navigationBar.opaqueBackgroundColor, borderColor: inputPanelTheme.panelSeparatorColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor) } -public func legacyInstantVideoController(theme: PresentationTheme, forStory: Bool, panelFrame: CGRect, context: AccountContext, peerId: PeerId, slowmodeState: ChatSlowmodeState?, hasSchedule: Bool, send: @escaping (InstantVideoController, EnqueueMessage?) -> Void, displaySlowmodeTooltip: @escaping (UIView, CGRect) -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void) -> InstantVideoController { +public func legacyInstantVideoController(theme: PresentationTheme, forStory: Bool, panelFrame: CGRect, context: AccountContext, peerId: PeerId, slowmodeState: ChatSlowmodeState?, hasSchedule: Bool, send: @escaping (InstantVideoController, EnqueueMessage?) -> Void, displaySlowmodeTooltip: @escaping (UIView, CGRect) -> Void, presentSchedulePicker: @escaping (@escaping (Int32, Bool) -> Void) -> Void) -> InstantVideoController { let isSecretChat = peerId.namespace == Namespaces.Peer.SecretChat let legacyController = InstantVideoController(presentation: .custom, theme: theme) @@ -166,8 +166,8 @@ public func legacyInstantVideoController(theme: PresentationTheme, forStory: Boo return node }, canSendSilently: !isSecretChat, canSchedule: hasSchedule, reminder: peerId == context.account.peerId)! controller.presentScheduleController = { done in - presentSchedulePicker { time in - done?(time) + presentSchedulePicker { time, silentPosting in + done?(time, silentPosting) } } controller.finishedWithVideo = { [weak legacyController] videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp in @@ -189,7 +189,7 @@ public func legacyInstantVideoController(theme: PresentationTheme, forStory: Boo let thumbnailSize = finalDimensions.aspectFitted(CGSize(width: 320.0, height: 320.0)) let thumbnailImage = TGScaleImageToPixelSize(previewImage, thumbnailSize)! if let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { - context.account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnailData) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: thumbnailData) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } } @@ -216,7 +216,7 @@ public func legacyInstantVideoController(theme: PresentationTheme, forStory: Boo let resource: TelegramMediaResource if let liveUploadData = liveUploadData as? LegacyLiveUploadInterfaceResult, resourceAdjustments == nil, let data = try? Data(contentsOf: videoUrl) { resource = LocalFileMediaResource(fileId: liveUploadData.id) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) } else { resource = LocalFileVideoMediaResource(randomId: Int64.random(in: Int64.min ... Int64.max), path: videoUrl.path, adjustments: resourceAdjustments) } diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD b/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD index 97f4042d3c..fe0a6b4c34 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/BUILD @@ -17,13 +17,14 @@ swift_library( "//submodules/Postbox", "//submodules/AccountContext", "//submodules/LegacyComponents", + "//submodules/ChatPresentationInterfaceState", "//submodules/ComponentFlow", "//submodules/TelegramPresentationData", "//submodules/ContextUI", "//submodules/TooltipUI", "//submodules/UndoUI", + "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/TelegramUI/Components/MessageInputPanelComponent", - "//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView", "//submodules/TelegramNotices", "//submodules/TextFormat", "//submodules/TelegramUIPreferences", diff --git a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift index 2bf5c939d3..10605a67f4 100644 --- a/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift +++ b/submodules/TelegramUI/Components/LegacyMessageInputPanel/Sources/LegacyMessageInputPanel.swift @@ -7,18 +7,18 @@ import TelegramCore import Postbox import SwiftSignalKit import AccountContext -import LegacyComponents import ComponentFlow import MessageInputPanelComponent import TelegramPresentationData import ContextUI import TooltipUI -import LegacyMessageInputPanelInputView import UndoUI import TelegramNotices import TextFormat import TelegramUIPreferences import Pasteboard +import ChatEntityKeyboardInputNode +import ChatPresentationInterfaceState public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { private let context: AccountContext @@ -26,78 +26,181 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { private let isScheduledMessages: Bool private let isFile: Bool private let hasTimer: Bool + private let customEmojiAvailable: Bool private let pushViewController: (ViewController) -> Void private let present: (ViewController) -> Void - private let presentInGlobalOverlay: (ViewController) -> Void - private let makeEntityInputView: () -> LegacyMessageInputPanelInputView? - + private let presentInGlobalOverlay: (ViewController) -> Void + private let getNavigationController: () -> NavigationController? + private let state = ComponentState() private let inputPanelExternalState = MessageInputPanelComponent.ExternalState() private let inputPanel = ComponentView() - + private var currentTimeout: Int32? private var currentIsEditing = false private var currentHeight: CGFloat? private var currentIsVideo = false private var currentIsCaptionAbove = false - + private var currentInputMode: MessageInputPanelComponent.InputMode = .text + private var currentAdditionalInputHeight: CGFloat = 0.0 + private var currentSafeAreaInset: UIEdgeInsets = .zero + private var currentContainerBottomInset: CGFloat = 0.0 + private var usesContainerLayout = false + private let hapticFeedback = HapticFeedback() - - private var inputView: LegacyMessageInputPanelInputView? - private var isEmojiKeyboardActive = false - + + private let inputMediaNodeDataPromise = Promise() + private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? + private var inputMediaNodeDataDisposable: Disposable? + private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext() + private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction? + private var inputMediaNode: ChatEntityKeyboardInputNode? + public var sendPressed: ((NSAttributedString?) -> Void)? public var focusUpdated: ((Bool) -> Void)? public var heightUpdated: ((Bool) -> Void)? public var timerUpdated: ((NSNumber?) -> Void)? public var captionIsAboveUpdated: ((Bool) -> Void)? - + + public var additionalInputHeight: CGFloat { + return self.currentAdditionalInputHeight + } + private weak var undoController: UndoOverlayController? private weak var tooltipController: TooltipScreen? - + private var isAIEnabled: Bool = false - + private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, metrics: LayoutMetrics)? - + public init( context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, hasTimer: Bool, + customEmojiAvailable: Bool, pushViewController: @escaping (ViewController) -> Void, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void, - makeEntityInputView: @escaping () -> LegacyMessageInputPanelInputView? + getNavigationController: @escaping () -> NavigationController? ) { self.context = context self.chatLocation = chatLocation self.isScheduledMessages = isScheduledMessages self.isFile = isFile self.hasTimer = hasTimer + self.customEmojiAvailable = customEmojiAvailable self.pushViewController = pushViewController self.present = present self.presentInGlobalOverlay = presentInGlobalOverlay - self.makeEntityInputView = makeEntityInputView - + self.getNavigationController = getNavigationController + 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 } - + + self.inputMediaNodeDataPromise.set( + ChatEntityKeyboardInputNode.inputData( + context: context, + chatPeerId: nil, + areCustomEmojiEnabled: customEmojiAvailable, + hasTrending: false, + hasStickers: false, + hasGifs: false, + sendGif: nil + ) + ) + self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + self.inputMediaNodeData = value + if case .emoji = self.currentInputMode { + self.update(transition: .immediate) + } + }) + + self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( + sendSticker: { _, _, _, _, _, _, _, _, _ in + return false + }, + sendEmoji: { _, _, _ in + }, + sendGif: { _, _, _, _, _ in + return false + }, + sendBotContextResultAsGif: { _, _, _, _, _, _ in + return false + }, + editGif: { _, _ in + }, + updateChoosingSticker: { _ in + }, + switchToTextInput: { [weak self] in + self?.activateInput() + }, + dismissTextInput: { + }, + insertText: { [weak self] text in + self?.inputPanelExternalState.insertText(text) + }, + backwardsDeleteText: { [weak self] in + self?.inputPanelExternalState.deleteBackward() + }, + openStickerEditor: { + }, + presentController: { [weak self] controller, _ in + guard let self else { + return + } + self.prepareForPresentedController(controller) + self.present(controller) + }, + presentGlobalOverlayController: { [weak self] controller, _ in + guard let self else { + return + } + self.prepareForPresentedController(controller) + self.presentInGlobalOverlay(controller) + }, + getNavigationController: getNavigationController, + requestLayout: { [weak self] transition in + self?.update(transition: transition) + } + ) + self.inputMediaInteraction?.forceTheme = defaultDarkColorPresentationTheme + self.state._updated = { [weak self] transition, _ in if let self { self.update(transition: transition.containedViewLayoutTransition) } } } - - public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat { - return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false) + + deinit { + self.inputMediaNodeDataDisposable?.dispose() } - + + public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat { + self.currentSafeAreaInset = .zero + self.currentContainerBottomInset = 0.0 + self.usesContainerLayout = false + return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false) + } + + @objc(updateContainerLayoutSize:safeAreaInset:bottomInset:keyboardHeight:animated:) + public func updateContainerLayoutSize(_ size: CGSize, safeAreaInset: UIEdgeInsets, bottomInset: CGFloat, keyboardHeight: CGFloat, animated: Bool) -> CGFloat { + self.currentSafeAreaInset = safeAreaInset + self.currentContainerBottomInset = bottomInset + self.usesContainerLayout = true + return self.updateLayout(width: size.width, leftInset: 0.0, rightInset: 0.0, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false) + } + public func caption() -> NSAttributedString { if let view = self.inputPanel.view as? MessageInputPanelComponent.View, case let .text(caption) = view.getSendMessageInput() { return caption @@ -105,7 +208,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { return NSAttributedString() } } - + private var scheduledMessageInput: MessageInputPanelComponent.SendMessageInput? public func setCaption(_ caption: NSAttributedString?) { let sendMessageInput = MessageInputPanelComponent.SendMessageInput.text(caption ?? NSAttributedString()) @@ -115,12 +218,12 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { self.scheduledMessageInput = sendMessageInput } } - + public func animate(_ view: UIView, frame: CGRect) { let transition = ComponentTransition.spring(duration: 0.4) transition.setFrame(view: view, frame: frame) } - + public func setTimeout(_ timeout: Int32, isVideo: Bool, isCaptionAbove: Bool) { self.dismissAllTooltips() var timeout: Int32? = timeout @@ -131,33 +234,56 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { self.currentIsVideo = isVideo self.currentIsCaptionAbove = isCaptionAbove } - + public func activateInput() { + let transition: ContainedViewLayoutTransition + if self.currentInputMode != .text { + transition = .animated(duration: 0.4, curve: .spring) + } else { + transition = .immediate + } + self.currentInputMode = .text + self.update(transition: transition) if let view = self.inputPanel.view as? MessageInputPanelComponent.View { view.activateInput() } } - + public func dismissInput() -> Bool { if let view = self.inputPanel.view as? MessageInputPanelComponent.View { if view.canDeactivateInput() { - self.isEmojiKeyboardActive = false - self.inputView = nil - view.deactivateInput(force: true) + let inputModeTransition: ContainedViewLayoutTransition + if self.currentInputMode != .text { + self.currentInputMode = .text + inputModeTransition = .animated(duration: 0.4, curve: .spring) + } else { + inputModeTransition = .immediate + } + if view.isActive { + view.deactivateInput(force: true) + } + if !inputModeTransition.isAnimated { + return true + } + self.update(transition: inputModeTransition) return true } else { view.animateError() return false } } else { + if self.currentInputMode != .text { + self.currentInputMode = .text + self.update(transition: .animated(duration: 0.4, curve: .spring)) + } return true } } - + public func onAnimateOut() { self.dismissAllTooltips() } - + public func baseHeight() -> CGFloat { return 52.0 } @@ -165,25 +291,41 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func update(transition: ContainedViewLayoutTransition) { if let (width, leftInset, rightInset, bottomInset, keyboardHeight, additionalSideInsets, maxHeight, isSecondary, metrics) = self.validLayout { let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: keyboardHeight, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, metrics: metrics, isMediaInputExpanded: false) } } - - public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + + public func updateLayout( + width: CGFloat, + leftInset: CGFloat, + rightInset: CGFloat, + bottomInset: CGFloat, + keyboardHeight: CGFloat, + additionalSideInsets: UIEdgeInsets, + maxHeight: CGFloat, + isSecondary: Bool, + transition: ContainedViewLayoutTransition, + metrics: LayoutMetrics, + isMediaInputExpanded: Bool + ) -> CGFloat { let previousLayout = self.validLayout self.validLayout = (width, leftInset, rightInset, bottomInset, keyboardHeight, additionalSideInsets, maxHeight, isSecondary, metrics) - + var transition = transition if keyboardHeight.isZero, let previousKeyboardHeight = previousLayout?.keyboardHeight, previousKeyboardHeight > 0.0, !transition.isAnimated { transition = .animated(duration: 0.4, curve: .spring) } - + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let theme = defaultDarkColorPresentationTheme - + let isLandscape = width > maxHeight + let deviceMetrics = DeviceMetrics(screenSize: CGSize(width: width, height: maxHeight), scale: UIScreen.main.scale, statusBarHeight: 0.0, onScreenNavigationHeight: nil) + let standardInputHeight = deviceMetrics.standardInputHeight(inLandscape: isLandscape) + let keyboardWasHidden = self.inputPanelExternalState.isKeyboardHidden + var timeoutValue: String? var timeoutSelected = false if self.isFile { @@ -200,25 +342,34 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { timeoutValue = "1" } } - - var maxInputPanelHeight = maxHeight - if keyboardHeight.isZero { + + let reservedKeyboardHeight: CGFloat + if case .emoji = self.currentInputMode { + reservedKeyboardHeight = max(keyboardHeight, standardInputHeight) + } else if self.inputPanelExternalState.isEditing && keyboardHeight.isZero && keyboardWasHidden { + reservedKeyboardHeight = standardInputHeight + } else { + reservedKeyboardHeight = keyboardHeight + } + + let maxInputPanelHeight: CGFloat + if keyboardHeight.isZero, case .text = self.currentInputMode, !keyboardWasHidden { maxInputPanelHeight = 60.0 } else { - maxInputPanelHeight = maxHeight - keyboardHeight - 100.0 + maxInputPanelHeight = max(60.0, maxHeight - reservedKeyboardHeight - 100.0) } - + var resetInputContents: MessageInputPanelComponent.SendMessageInput? if let scheduledMessageInput = self.scheduledMessageInput { resetInputContents = scheduledMessageInput self.scheduledMessageInput = nil } - + var hasTimer = self.hasTimer && self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser && !self.isScheduledMessages if self.chatLocation.peerId?.isRepliesOrSavedMessages(accountPeerId: self.context.account.peerId) == true { hasTimer = false } - + self.inputPanel.parentState = self.state let inputPanelSize = self.inputPanel.update( transition: ComponentTransition(transition), @@ -236,9 +387,15 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { alwaysDarkWhenHasText: false, resetInputContents: resetInputContents, nextInputMode: { [weak self] _ in - if self?.isEmojiKeyboardActive == true { + guard let self else { + return .emoji + } + switch self.currentInputMode { + case .text: + return .emoji + case .emoji: return .text - } else { + default: return .emoji } }, @@ -258,31 +415,23 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { stopAndPreviewMediaRecording: nil, discardMediaRecordingPreview: nil, attachmentAction: { [weak self] in - if let self { - self.toggleIsCaptionAbove() - } + self?.toggleIsCaptionAbove() }, attachmentButtonMode: self.currentIsCaptionAbove ? .captionDown : .captionUp, myReaction: nil, likeAction: nil, likeOptionsAction: nil, inputModeAction: { [weak self] in - if let self { - self.toggleInputMode() - } + self?.toggleInputMode() }, timeoutAction: hasTimer ? { [weak self] sourceView, gesture in - if let self { - self.presentTimeoutSetup(sourceView: sourceView, gesture: gesture) - } + self?.presentTimeoutSetup(sourceView: sourceView, gesture: gesture) } : nil, forwardAction: nil, paidMessageAction: nil, moreAction: nil, presentCaptionPositionTooltip: { [weak self] sourceView in - if let self { - self.presentCaptionPositionTooltip(sourceView: sourceView) - } + self?.presentCaptionPositionTooltip(sourceView: sourceView) }, presentVoiceMessagesUnavailableTooltip: nil, presentTextLengthLimitTooltip: nil, @@ -300,63 +449,246 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { displayGradient: false, bottomInset: 0.0, isFormattingLocked: false, - hideKeyboard: false, - customInputView: self.inputView, - forceIsEditing: false, + hideKeyboard: self.currentInputMode == .emoji, + customInputView: nil, + forceIsEditing: self.currentInputMode == .emoji, disabledPlaceholder: nil, header: nil, isChannel: false, storyItem: nil, chatLocation: self.chatLocation, aiCompose: self.isAIEnabled ? { [weak self] in - guard let self else { - return - } - self.openAICompose() + self?.openAICompose() } : nil ) ), environment: {}, containerSize: CGSize(width: width, height: maxInputPanelHeight) ) + let inputPanelHeight = inputPanelSize.height - 8.0 + var totalHeight = inputPanelHeight + var inputMediaHeight: CGFloat = 0.0 + self.currentAdditionalInputHeight = 0.0 + var inputMediaNodeForLayout: ChatEntityKeyboardInputNode? + var isNewInputMediaNode = false + var retainedInputHeight = keyboardHeight + var shouldRetainHiddenInputHeight = false + + if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData { + let inputMediaNode: ChatEntityKeyboardInputNode + if let current = self.inputMediaNode { + inputMediaNode = current + } else { + isNewInputMediaNode = true + inputMediaNode = ChatEntityKeyboardInputNode( + context: self.context, + currentInputData: inputData, + updatedInputData: self.inputMediaNodeDataPromise.get(), + defaultToEmojiTab: true, + opaqueTopPanelBackground: false, + useOpaqueTheme: false, + interaction: self.inputMediaInteraction, + chatPeerId: nil, + stateContext: self.inputMediaNodeStateContext + ) + inputMediaNode.clipsToBounds = true + inputMediaNode.externalTopPanelContainerImpl = nil + inputMediaNode.useExternalSearchContainer = true + self.inputMediaNode = inputMediaNode + } + + if inputMediaNode.view.superview == nil { + if let inputPanelView = self.inputPanel.view { + self.view.insertSubview(inputMediaNode.view, belowSubview: inputPanelView) + } else { + self.view.addSubview(inputMediaNode.view) + } + } + inputMediaNodeForLayout = inputMediaNode + + let inputPresentationData = self.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) + let presentationInterfaceState = ChatPresentationInterfaceState( + chatWallpaper: .builtin(WallpaperSettings()), + theme: inputPresentationData.theme, + preferredGlassType: .default, + strings: inputPresentationData.strings, + dateTimeFormat: inputPresentationData.dateTimeFormat, + nameDisplayOrder: inputPresentationData.nameDisplayOrder, + limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, + fontSize: inputPresentationData.chatFontSize, + bubbleCorners: inputPresentationData.chatBubbleCorners, + accountPeerId: self.context.account.peerId, + mode: .standard(.default), + chatLocation: .peer(id: self.context.account.peerId), + subject: nil, + greetingData: nil, + pendingUnpinnedAllMessages: false, + activeGroupCallInfo: nil, + hasActiveGroupCall: false, + threadData: nil, + isGeneralThreadClosed: nil, + replyMessage: nil, + accountPeerColor: nil, + businessIntro: nil + ) + + let heightAndOverflow = inputMediaNode.updateLayout( + width: width, + leftInset: 0.0, + rightInset: 0.0, + bottomInset: 0.0, + standardInputHeight: standardInputHeight, + inputHeight: 0.0, + maximumHeight: maxHeight, + inputPanelHeight: 0.0, + transition: .immediate, + interfaceState: presentationInterfaceState, + layoutMetrics: metrics, + deviceMetrics: deviceMetrics, + isVisible: true, + isExpanded: false + ) + inputMediaHeight = heightAndOverflow.0 + self.currentAdditionalInputHeight = inputMediaHeight + totalHeight += inputMediaHeight + } else if let inputMediaNode = self.inputMediaNode { + self.inputMediaNode = nil + + if transition.isAnimated { + var dismissingInputHeight = keyboardHeight + if self.inputPanelExternalState.isEditing && (dismissingInputHeight.isZero && keyboardWasHidden) { + dismissingInputHeight = max(dismissingInputHeight, standardInputHeight) + } + let targetOriginY: CGFloat + if self.usesContainerLayout { + if dismissingInputHeight > 0.0 { + targetOriginY = maxHeight - dismissingInputHeight + } else { + targetOriginY = maxHeight + } + } else { + if dismissingInputHeight > 0.0 { + targetOriginY = inputPanelHeight + } else { + targetOriginY = inputPanelHeight + inputMediaNode.frame.height + } + } + let targetFrame = CGRect( + origin: CGPoint(x: inputMediaNode.frame.minX, y: targetOriginY), + size: inputMediaNode.frame.size + ) + transition.updateFrame(view: inputMediaNode.view, frame: targetFrame) + inputMediaNode.view.layer.animateAlpha(from: inputMediaNode.view.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in + inputMediaNode?.view.removeFromSuperview() + }) + } else { + inputMediaNode.view.removeFromSuperview() + } + } + + if self.inputPanelExternalState.isEditing { + if case .emoji = self.currentInputMode { + retainedInputHeight = max(retainedInputHeight, standardInputHeight) + shouldRetainHiddenInputHeight = true + } else if retainedInputHeight.isZero && keyboardWasHidden { + retainedInputHeight = max(retainedInputHeight, standardInputHeight) + shouldRetainHiddenInputHeight = true + } + } + if self.currentAdditionalInputHeight.isZero && retainedInputHeight > 0.0 && shouldRetainHiddenInputHeight { + self.currentAdditionalInputHeight = retainedInputHeight + totalHeight += retainedInputHeight + } + + let isLandscapePhone = width > maxHeight && UIDevice.current.userInterfaceIdiom != .pad + let collapsedCaptionTopInset = self.currentSafeAreaInset.top + 48.0 + let expandedCaptionTopInset = self.currentSafeAreaInset.top + 8.0 + + var inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: inputPanelSize) + var inputMediaFrame = CGRect(origin: CGPoint(x: 0.0, y: inputPanelHeight), size: CGSize(width: width, height: inputMediaHeight)) + + if self.usesContainerLayout { + if isLandscapePhone { + inputPanelFrame.origin.y = maxHeight + 16.0 - 8.0 + inputMediaFrame.origin.y = maxHeight + 16.0 + } else if case .emoji = self.currentInputMode { + inputMediaFrame.origin.y = maxHeight - inputMediaHeight + if self.currentIsCaptionAbove { + inputPanelFrame.origin.y = expandedCaptionTopInset - 8.0 + } else { + inputPanelFrame.origin.y = inputMediaFrame.minY - inputPanelHeight - 8.0 + } + } else { + if self.currentIsCaptionAbove { + inputPanelFrame.origin.y = (retainedInputHeight > 0.0 ? expandedCaptionTopInset : collapsedCaptionTopInset) - 8.0 + } else { + let bottomOffset = max(self.currentContainerBottomInset, retainedInputHeight) + inputPanelFrame.origin.y = maxHeight - inputPanelHeight - bottomOffset - 8.0 + } + inputMediaFrame.origin.y = maxHeight + } + } + if let view = self.inputPanel.view { if view.superview == nil { self.view.addSubview(view) } - let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: inputPanelSize) transition.updateFrame(view: view, frame: inputPanelFrame) } - + + if let inputMediaNode = inputMediaNodeForLayout { + if isNewInputMediaNode && transition.isAnimated { + inputMediaNode.view.frame = inputMediaFrame.offsetBy(dx: 0.0, dy: inputMediaHeight) + inputMediaNode.view.alpha = 0.0 + inputMediaNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + inputMediaNode.view.alpha = 1.0 + transition.updateFrame(view: inputMediaNode.view, frame: inputMediaFrame) + } + if self.currentIsEditing != self.inputPanelExternalState.isEditing { self.currentIsEditing = self.inputPanelExternalState.isEditing self.focusUpdated?(self.currentIsEditing) } - - if self.currentHeight != inputPanelSize.height { - self.currentHeight = inputPanelSize.height + + if self.currentHeight != totalHeight { + self.currentHeight = totalHeight self.heightUpdated?(transition.isAnimated) } - - return inputPanelSize.height - 8.0 + + return totalHeight } - + + private func prepareForPresentedController(_ controller: ViewController) { + if controller is UndoOverlayController { + return + } + if let view = self.inputPanel.view as? MessageInputPanelComponent.View, view.isActive { + view.deactivateInput(force: true) + } + if self.currentInputMode != .text { + self.currentInputMode = .text + self.update(transition: .immediate) + } + } + private func openAICompose() { Task { @MainActor [weak self] in guard let self else { return } - + let effectiveInputText: NSAttributedString = self.caption() if effectiveInputText.length == 0 { return } - + let inputText = trimChatInputText(effectiveInputText) var entities: [MessageTextEntity] = [] if inputText.length != 0 { entities = generateTextEntities(inputText.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(inputText, maxAnimatedEmojisInText: 0)) } - + self.pushViewController(await self.context.sharedContext.makeTextProcessingScreen( context: self.context, theme: defaultDarkColorPresentationTheme, @@ -372,86 +704,57 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { sendContextActions: nil ), inputText: TextWithEntities(text: inputText.string, entities: entities), - copyResult: { [weak self] text in - guard let self else { - return - } - let _ = self + copyResult: { text in storeMessageTextInPasteboard(text.text, entities: text.entities) }, translateChat: nil )) } } - + private func toggleInputMode() { - self.isEmojiKeyboardActive = !self.isEmojiKeyboardActive - - if self.isEmojiKeyboardActive { - let inputView = self.makeEntityInputView() - inputView?.insertText = { [weak self] text in - if let self { - self.inputPanelExternalState.insertText(text) - } + switch self.currentInputMode { + case .text: + self.currentInputMode = .emoji + self.update(transition: .animated(duration: 0.4, curve: .spring)) + if let view = self.inputPanel.view as? MessageInputPanelComponent.View, !view.isActive { + view.activateInput() } - inputView?.deleteBackwards = { [weak self] in - if let self { - self.inputPanelExternalState.deleteBackward() - } - } - inputView?.switchToKeyboard = { [weak self] in - if let self { - self.isEmojiKeyboardActive = false - self.inputView = nil - self.update(transition: .immediate) - } - } - inputView?.presentController = { [weak self] c in - if let self { - if !(c is UndoOverlayController) { - self.isEmojiKeyboardActive = false - if let view = self.inputPanel.view as? MessageInputPanelComponent.View { - view.deactivateInput(force: true) - } - } - self.present(c) - } - } - self.inputView = inputView - self.update(transition: .immediate) - } else { - self.inputView = nil - self.update(transition: .immediate) + case .emoji: + self.activateInput() + default: + self.currentInputMode = .emoji + self.update(transition: .animated(duration: 0.4, curve: .spring)) } } - + private func toggleIsCaptionAbove() { self.currentIsCaptionAbove = !self.currentIsCaptionAbove self.captionIsAboveUpdated?(self.currentIsCaptionAbove) self.update(transition: .animated(duration: 0.3, curve: .spring)) - + self.dismissAllTooltips() - + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - + let title = self.currentIsCaptionAbove ? presentationData.strings.MediaPicker_InvertCaption_Updated_Up_Title : presentationData.strings.MediaPicker_InvertCaption_Updated_Down_Title let text = self.currentIsCaptionAbove ? presentationData.strings.MediaPicker_InvertCaption_Updated_Up_Text : presentationData.strings.MediaPicker_InvertCaption_Updated_Down_Text let animationName = self.currentIsCaptionAbove ? "message_preview_sort_above" : "message_preview_sort_below" - + let controller = UndoOverlayController( presentationData: presentationData, content: .universal(animation: animationName, scale: 1.0, colors: ["__allcolors__": UIColor.white], title: title, text: text, customUndoText: nil, timeout: 2.0), elevatedLayout: false, position: self.currentIsCaptionAbove ? .bottom : .top, - action: { _ in return false } + action: { _ in return false } ) self.present(controller) self.undoController = controller } - + private func presentTimeoutSetup(sourceView: UIView, gesture: ContextGesture?) { self.hapticFeedback.impact(.light) - + var items: [ContextMenuItem] = [] let updateTimeout: (Int32?) -> Void = { [weak self] timeout in @@ -465,46 +768,46 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { } } } - + let currentValue = self.currentTimeout let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) let title = presentationData.strings.MediaPicker_Timer_Description let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil - - items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) + + items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in nil }, action: emptyAction))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_ViewOnce, icon: { theme in return currentValue == viewOnceTimeout ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : UIImage() - }, action: { _, a in - a(.default) - + }, action: { _, action in + action(.default) + updateTimeout(viewOnceTimeout) }))) - + let values: [Int32] = [3, 10, 30] - + for value in values { items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_Seconds(value), icon: { theme in return currentValue == value ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : UIImage() - }, action: { _, a in - a(.default) - + }, action: { _, action in + action(.default) + updateTimeout(value) }))) } - + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_DoNotDelete, icon: { theme in return currentValue == nil ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : UIImage() - }, action: { _, a in - a(.default) - + }, action: { _, action in + action(.default) + updateTimeout(nil) }))) - + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(sourceView: sourceView, position: self.currentIsCaptionAbove ? .bottom : .top)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) self.present(contextController) } - + private func dismissAllTooltips() { if let undoController = self.undoController { self.undoController = nil @@ -515,17 +818,17 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { tooltipController.dismiss() } } - + private func presentTimeoutTooltip(sourceView: UIView, timeout: Int32?) { guard let superview = self.view.superview?.superview else { return } self.dismissAllTooltips() - + let parentFrame = superview.convert(superview.bounds, to: nil) let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 2.0), size: CGSize()) - + let isVideo = self.currentIsVideo let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let text: String @@ -540,7 +843,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_KeepTooltip : presentationData.strings.MediaPicker_Timer_Photo_KeepTooltip iconName = "anim_autoremove_off" } - + let tooltipController = TooltipScreen( account: self.context.account, sharedContext: self.context.sharedContext, @@ -559,14 +862,13 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { self.tooltipController = tooltipController self.present(tooltipController) } - + private func presentCaptionPositionTooltip(sourceView: UIView) { guard let superview = self.view.superview?.superview else { return } self.dismissAllTooltips() - - + let _ = (ApplicationSpecificNotice.getCaptionAboveMediaTooltip(accountManager: self.context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { [weak self] count in guard let self else { @@ -575,13 +877,13 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { if count > 2 { return } - + let parentFrame = superview.convert(superview.bounds, to: nil) let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX + 2.0, y: absoluteFrame.minY + 6.0), size: CGSize()) - + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - + let tooltipController = TooltipScreen( account: self.context.account, sharedContext: self.context.sharedContext, @@ -600,16 +902,22 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView { ) self.tooltipController = tooltipController self.present(tooltipController) - + let _ = ApplicationSpecificNotice.incrementCaptionAboveMediaTooltip(accountManager: self.context.sharedContext.accountManager).start() }) } - + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let result = super.hitTest(point, with: event) if let view = self.inputPanel.view, let panelResult = view.hitTest(self.view.convert(point, to: view), with: event) { return panelResult } + if let inputMediaNode = self.inputMediaNode, let inputMediaResult = inputMediaNode.view.hitTest(self.view.convert(point, to: inputMediaNode.view), with: event) { + return inputMediaResult + } + let result = super.hitTest(point, with: event) + if result === self.view { + return nil + } return result } } @@ -619,7 +927,7 @@ private final class HeaderContextReferenceContentSource: ContextReferenceContent var keepInPlace: Bool { return true } - + let position: ContextControllerReferenceViewInfo.ActionsPosition init(sourceView: UIView, position: ContextControllerReferenceViewInfo.ActionsPosition) { diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index d4ca89a2b5..7736448cde 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -93,15 +93,23 @@ public final class ListActionItemComponent: Component { public enum LeftIcon: Equatable { public final class Check: Equatable { + public enum Style { + case round + case rectangle + } + + public let style: Style public let isSelected: Bool public let isEnabled: Bool public let toggle: (() -> Void)? public init( + style: Style = .round, isSelected: Bool, isEnabled: Bool = true, toggle: (() -> Void)? ) { + self.style = style self.isSelected = isSelected self.isEnabled = isEnabled self.toggle = toggle @@ -111,6 +119,9 @@ public final class ListActionItemComponent: Component { if lhs === rhs { return true } + if lhs.style != rhs.style { + return false + } if lhs.isSelected != rhs.isSelected { return false } @@ -318,12 +329,12 @@ public final class ListActionItemComponent: Component { self.action?() } - func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, transition: ComponentTransition) { + func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, isRectangle: Bool = false, transition: ComponentTransition) { let checkLayer: CheckLayer if let current = self.checkLayer { checkLayer = current } else { - checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check(isRectangle: false)) + checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check(isRectangle: isRectangle)) self.checkLayer = checkLayer self.layer.addSublayer(checkLayer) } @@ -625,11 +636,11 @@ public final class ListActionItemComponent: Component { leftCheckView.frame = CGRect(origin: CGPoint(x: -checkSize.width, y: self.bounds.height == 0.0 ? checkFrame.minY : floor((self.bounds.height - checkSize.height) * 0.5)), size: checkFrame.size) transition.setPosition(view: leftCheckView, position: checkFrame.center) transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size)) - leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, transition: .immediate) + leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, isRectangle: check.style == .rectangle, transition: .immediate) } else { transition.setPosition(view: leftCheckView, position: checkFrame.center) transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size)) - leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, transition: transition) + leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, isRectangle: check.style == .rectangle, transition: transition) } case let .custom(customLeftIcon, adjustLeftInset): var resetLeftIcon = false @@ -716,7 +727,8 @@ public final class ListActionItemComponent: Component { } } - if case .arrow = component.accessory { + switch component.accessory { + case .arrow: let arrowView: UIImageView var arrowTransition = transition if let current = self.arrowView { @@ -735,14 +747,7 @@ public final class ListActionItemComponent: Component { let arrowFrame = CGRect(origin: CGPoint(x: availableSize.width - 7.0 - image.size.width, y: floor((contentHeight - image.size.height) * 0.5)), size: image.size) arrowTransition.setFrame(view: arrowView, frame: arrowFrame) } - } else { - if let arrowView = self.arrowView { - self.arrowView = nil - arrowView.removeFromSuperview() - } - } - - if case .expandArrows = component.accessory { + case .expandArrows: let arrowView: UIImageView var arrowTransition = transition if let current = self.arrowView { @@ -761,7 +766,7 @@ public final class ListActionItemComponent: Component { let arrowFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - image.size.width, y: floor((contentHeight - image.size.height) * 0.5)), size: image.size) arrowTransition.setFrame(view: arrowView, frame: arrowFrame) } - } else { + default: if let arrowView = self.arrowView { self.arrowView = nil arrowView.removeFromSuperview() diff --git a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift index 8ca1510769..7a36bed129 100644 --- a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift +++ b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift @@ -22,11 +22,6 @@ import EmojiTextAttachmentView import TextFormat public final class ListComposePollOptionComponent: Component { - public enum Style { - case glass - case legacy - } - public final class ResetText: Equatable { public let value: NSAttributedString @@ -117,7 +112,6 @@ public final class ListComposePollOptionComponent: Component { public let externalState: TextFieldComponent.ExternalState? public let context: AccountContext - public let style: Style public let theme: PresentationTheme public let strings: PresentationStrings public let placeholder: NSAttributedString? @@ -130,6 +124,7 @@ public final class ListComposePollOptionComponent: Component { public let canReorder: Bool public let canAdd: Bool public let attachment: Attachment? + public let formattingAvailable: Bool public let emptyLineHandling: TextFieldComponent.EmptyLineHandling public let returnKeyType: UIReturnKeyType public let returnKeyAction: (() -> Void)? @@ -141,12 +136,12 @@ public final class ListComposePollOptionComponent: Component { public let attachAction: (() -> Void)? public let deleteAction: (() -> Void)? public let paste: ((TextFieldComponent.PasteData) -> Void)? + public let present: ((ViewController) -> Void)? public let tag: AnyObject? public init( externalState: TextFieldComponent.ExternalState?, context: AccountContext, - style: Style = .legacy, theme: PresentationTheme, strings: PresentationStrings, placeholder: NSAttributedString? = nil, @@ -159,6 +154,7 @@ public final class ListComposePollOptionComponent: Component { canReorder: Bool = false, canAdd: Bool = false, attachment: Attachment? = nil, + formattingAvailable: Bool = false, emptyLineHandling: TextFieldComponent.EmptyLineHandling, returnKeyType: UIReturnKeyType = .next, returnKeyAction: (() -> Void)? = nil, @@ -170,11 +166,11 @@ public final class ListComposePollOptionComponent: Component { attachAction: (() -> Void)? = nil, deleteAction: (() -> Void)? = nil, paste: ((TextFieldComponent.PasteData) -> Void)? = nil, + present: ((ViewController) -> Void)? = nil, tag: AnyObject? = nil ) { self.externalState = externalState self.context = context - self.style = style self.theme = theme self.strings = strings self.placeholder = placeholder @@ -187,6 +183,7 @@ public final class ListComposePollOptionComponent: Component { self.canReorder = canReorder self.canAdd = canAdd self.attachment = attachment + self.formattingAvailable = formattingAvailable self.emptyLineHandling = emptyLineHandling self.returnKeyType = returnKeyType self.returnKeyAction = returnKeyAction @@ -198,6 +195,7 @@ public final class ListComposePollOptionComponent: Component { self.attachAction = attachAction self.deleteAction = deleteAction self.paste = paste + self.present = present self.tag = tag } @@ -208,9 +206,6 @@ public final class ListComposePollOptionComponent: Component { if lhs.context !== rhs.context { return false } - if lhs.style != rhs.style { - return false - } if lhs.theme !== rhs.theme { return false } @@ -247,6 +242,9 @@ public final class ListComposePollOptionComponent: Component { if lhs.attachment != rhs.attachment { return false } + if lhs.formattingAvailable != rhs.formattingAvailable { + return false + } if lhs.emptyLineHandling != rhs.emptyLineHandling { return false } @@ -681,10 +679,7 @@ public final class ListComposePollOptionComponent: Component { self.component = component self.state = state - var verticalInset: CGFloat = 12.0 - if case .glass = component.style { - verticalInset = 16.0 - } + let verticalInset: CGFloat = 16.0 var leftInset: CGFloat = 16.0 var rightInset: CGFloat = 16.0 let modeSelectorSize = CGSize(width: 32.0, height: 32.0) @@ -732,11 +727,15 @@ public final class ListComposePollOptionComponent: Component { enableInlineAnimations: component.enableInlineAnimations, emptyLineHandling: component.emptyLineHandling, externalHandlingForMultilinePaste: true, - formatMenuAvailability: .none, + formatMenuAvailability: component.formattingAvailable ? .available([.bold, .italic, .strikethrough, .underline, .monospace, .spoiler, .link]) : .none, returnKeyType: component.returnKeyType, lockedFormatAction: { }, - present: { _ in + present: { [weak self] c in + guard let self, let component = self.component else { + return + } + component.present?(c) }, paste: { [weak self] data in guard let self, let component = self.component else { @@ -1359,10 +1358,7 @@ public final class ListComposePollOptionComponent: Component { return } - var verticalInset: CGFloat = 12.0 - if case .glass = component.style { - verticalInset = 16.0 - } + let verticalInset: CGFloat = 16.0 var leftInset: CGFloat = 16.0 let rightInset: CGFloat = 16.0 diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift index 8be6a995f9..ca3ab4dff2 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift @@ -89,6 +89,7 @@ public final class ListMultilineTextFieldItemComponent: Component { public let initialText: String public let resetText: ResetText? public let placeholder: String + public let placeholderDefinesMinHeight: Bool public let autocapitalizationType: UITextAutocapitalizationType public let autocorrectionType: UITextAutocorrectionType public let keyboardType: UIKeyboardType @@ -117,6 +118,7 @@ public final class ListMultilineTextFieldItemComponent: Component { initialText: String, resetText: ResetText? = nil, placeholder: String, + placeholderDefinesMinHeight: Bool = false, autocapitalizationType: UITextAutocapitalizationType = .sentences, autocorrectionType: UITextAutocorrectionType = .default, keyboardType: UIKeyboardType = .default, @@ -144,6 +146,7 @@ public final class ListMultilineTextFieldItemComponent: Component { self.initialText = initialText self.resetText = resetText self.placeholder = placeholder + self.placeholderDefinesMinHeight = placeholderDefinesMinHeight self.autocapitalizationType = autocapitalizationType self.autocorrectionType = autocorrectionType self.keyboardType = keyboardType @@ -186,6 +189,9 @@ public final class ListMultilineTextFieldItemComponent: Component { if lhs.placeholder != rhs.placeholder { return false } + if lhs.placeholderDefinesMinHeight != rhs.placeholderDefinesMinHeight { + return false + } if lhs.autocapitalizationType != rhs.autocapitalizationType { return false } @@ -469,7 +475,20 @@ public final class ListMultilineTextFieldItemComponent: Component { containerSize: CGSize(width: availableSize.width - textFieldRightInset, height: availableSize.height) ) - let size = CGSize(width: availableSize.width, height: textFieldSize.height - 1.0) + let placeholderSize = self.placeholder.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor)), + maximumNumberOfLines: component.placeholderDefinesMinHeight ? 0 : 1 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + + var size = CGSize(width: availableSize.width, height: textFieldSize.height - 1.0) + if component.placeholderDefinesMinHeight { + size.height = max(size.height, placeholderSize.height + verticalInset * 2.0 - 1.0) + } let textFieldFrame = CGRect(origin: CGPoint(), size: textFieldSize) if let textFieldView = self.textField.view { @@ -480,14 +499,6 @@ public final class ListMultilineTextFieldItemComponent: Component { transition.setFrame(view: textFieldView, frame: textFieldFrame) } - let placeholderSize = self.placeholder.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.placeholder.isEmpty ? " " : component.placeholder, font: Font.regular(17.0), textColor: component.theme.list.itemPlaceholderTextColor)) - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) - ) let placeholderFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: placeholderSize) if let placeholderView = self.placeholder.view { if placeholderView.superview == nil { diff --git a/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LocationBroadcastNavigationAccessoryPanel.swift b/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LocationBroadcastNavigationAccessoryPanel.swift index c0f86a677d..6bc4d95cee 100644 --- a/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LocationBroadcastNavigationAccessoryPanel.swift +++ b/submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent/Sources/LocationBroadcastNavigationAccessoryPanel.swift @@ -178,8 +178,8 @@ final class LocationBroadcastNavigationAccessoryPanel: ASDisplayNode { let minimizedTitleOffset: CGFloat = subtitleString == nil ? 6.0 : 0.0 - let minimizedTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleLayout.size.width) / 2.0), y: 4.0 + minimizedTitleOffset), size: titleLayout.size) - let minimizedSubtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleLayout.size.width) / 2.0), y: 20.0), size: subtitleLayout.size) + let minimizedTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleLayout.size.width) / 2.0), y: 6.0 + minimizedTitleOffset), size: titleLayout.size) + let minimizedSubtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleLayout.size.width) / 2.0), y: 22.0), size: subtitleLayout.size) if let image = self.iconNode.image { transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: 7.0 + leftInset, y: 9.0), size: image.size)) diff --git a/submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation/BUILD b/submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation/BUILD index 0fc27dc460..190756ccf1 100644 --- a/submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation/BUILD +++ b/submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AccountContext", diff --git a/submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation/Sources/ImageObjectSeparation.swift b/submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation/Sources/ImageObjectSeparation.swift index 7c1cb21b1e..6a91bf131a 100644 --- a/submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation/Sources/ImageObjectSeparation.swift +++ b/submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation/Sources/ImageObjectSeparation.swift @@ -6,7 +6,6 @@ import CoreImage import CoreImage.CIFilterBuiltins import VideoToolbox import SwiftSignalKit -import Postbox import TelegramCore import AccountContext import FileMediaResourceStatus @@ -66,7 +65,7 @@ public func cutoutAvailability(context: AccountContext) -> Signal take(1) |> mapToSignal { maybeFileAndMessage -> Signal in if let (file, message) = maybeFileAndMessage { - let fetchedData = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .file, reference: FileMediaReference.message(message: MessageReference(message._asMessage()), media: file).resourceReference(file.resource)) + let fetchedData = context.engine.resources.fetch(reference: FileMediaReference.message(message: MessageReference(message._asMessage()), media: file).resourceReference(file.resource), userLocation: .other, userContentType: .file) enum FetchStatus { case completed(String) @@ -76,8 +75,8 @@ public func cutoutAvailability(context: AccountContext) -> Signal { subscriber in let fetchedDisposable = fetchedData.start() - let resourceDataDisposable = context.account.postbox.mediaBox.resourceData(file.resource, attemptSynchronously: false).start(next: { next in - if next.complete { + let resourceDataDisposable = context.engine.resources.data(resource: EngineMediaResource(file.resource)).start(next: { next in + if next.isComplete { SSZipArchive.unzipFile(atPath: next.path, toDestination: NSTemporaryDirectory()) subscriber.putNext(.completed(compiledModelPath)) subscriber.putCompletion() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 190e1983e7..0cdc45a66f 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -837,7 +837,7 @@ public final class MediaEditor { |> mapToSignal { message in var player: AVPlayer? if let message, !"".isEmpty { - if let maybeFile = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, maybeFile.isVideo, let path = self.context.account.postbox.mediaBox.completedResourcePath(maybeFile.resource, pathExtension: "mp4") { + if let maybeFile = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, maybeFile.isVideo, let path = self.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(maybeFile.resource.id), pathExtension: "mp4") { let asset = AVURLAsset(url: URL(fileURLWithPath: path)) player = self.makePlayer(asset: asset) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoFFMpegWriter.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoFFMpegWriter.swift index 9f9557616c..9f59ca4ac7 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoFFMpegWriter.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoFFMpegWriter.swift @@ -105,7 +105,9 @@ final class MediaEditorVideoFFMpegWriter: MediaEditorVideoExportWriter { let height = CVPixelBufferGetHeight(buffer) let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer) - let frame = FFMpegAVFrame(pixelFormat: .YUVA, width: Int32(width), height: Int32(height)) + guard let frame = FFMpegAVFrame(pixelFormat: .YUVA, width: Int32(width), height: Int32(height)) else { + return false + } CVPixelBufferLockBaseAddress(buffer, CVPixelBufferLockFlags.readOnly) let src = CVPixelBufferGetBaseAddress(buffer) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift index 6e33cd4f93..60ace4c4be 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift @@ -90,7 +90,7 @@ private func linkOptions(context: AccountContext, selfController: CreateLinkScre if let image = snapshotImage { let wallpaperResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) if let wallpaperData = image.jpegData(compressionQuality: 0.87) { - context.account.postbox.mediaBox.storeResourceData(wallpaperResource.id, data: wallpaperData, synchronous: true) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(wallpaperResource.id), data: wallpaperData, synchronous: true) } let wallpaperRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: wallpaperResource, progressiveSizes: [], immediateThumbnailData: nil) wallpaper = .image([wallpaperRepresentation], WallpaperSettings()) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index e8cc73e516..2f2b0b57ea 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -24,7 +24,7 @@ public extension MediaEditorScreenImpl { willDismiss: @escaping () -> Void = {}, update: @escaping (Disposable?) -> Void ) -> MediaEditorScreenImpl? { - guard let peerReference = PeerReference(peer._asPeer()) else { + guard let peerReference = PeerReference(peer) else { return nil } let subject: Signal @@ -34,9 +34,9 @@ public extension MediaEditorScreenImpl { return .single(.draft(source, Int64(storyItem.id))) } else { let media = storyItem.media._asMedia() - return fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .peer(peerReference.id), customUserContentType: .story, mediaReference: .story(peer: peerReference, id: storyItem.id, media: media)) + return fetchMediaData(context: context, userLocation: .peer(peerReference.id), customUserContentType: .story, mediaReference: .story(peer: peerReference, id: storyItem.id, media: media)) |> mapToSignal { (value, isImage) -> Signal in - guard case let .data(data) = value, data.complete else { + guard case let .data(data) = value, data.isComplete else { return .complete() } if let image = UIImage(contentsOfFile: data.path) { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index f9f3e01ef5..bf33c6725c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1257,7 +1257,6 @@ final class MediaEditorScreenComponent: Component { mode: .standard(.default), chatLocation: .peer(id: component.context.account.peerId), subject: nil, - peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, @@ -3750,7 +3749,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return } var messageFile: TelegramMediaFile? - if let maybeFile = messages.first?.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, maybeFile.isVideo, let _ = self.context.account.postbox.mediaBox.completedResourcePath(maybeFile.resource, pathExtension: nil) { + if let maybeFile = messages.first?.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, maybeFile.isVideo, let _ = self.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(maybeFile.resource.id), pathExtension: nil) { messageFile = maybeFile } if "".isEmpty { @@ -5135,7 +5134,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } let _ = (fetchMediaData( context: self.context, - postbox: self.context.account.postbox, userLocation: .other, mediaReference: file ) |> deliverOnMainQueue).start(next: { [weak self] state, _ in @@ -7733,10 +7731,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID let imagesReady = ValuePromise(false, ignoreRepeated: true) Queue.concurrentDefaultQueue().async { if !isVideo, let data = try? WebP.convert(toWebP: image, quality: 90.0) { - self.context.account.postbox.mediaBox.storeResourceData(isVideo ? thumbnailResource.id : resource.id, data: data, synchronous: true) + self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(isVideo ? thumbnailResource.id : resource.id), data: data, synchronous: true) } if let thumbnailImage = generateScaledImage(image: image, size: CGSize(width: 320.0, height: 320.0), opaque: false, scale: 1.0), let data = try? WebP.convert(toWebP: thumbnailImage, quality: 90.0) { - self.context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: data, synchronous: true) + self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(thumbnailResource.id), data: data, synchronous: true) } imagesReady.set(true) } @@ -8095,15 +8093,16 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return .single((.progress(progress * 0.5), nil)) case let .complete(resource): if let resource = resource as? CloudDocumentMediaResource { - return .single((.progress(1.0), nil)) |> then(.single((.complete(resource, mimeType), nil))) + return .single((.progress(1.0), nil)) |> then(.single((.complete(EngineMediaResource(resource), mimeType), nil))) } else { - return context.engine.stickers.uploadSticker(peer: peer._asPeer(), resource: resource, thumbnail: file.previewRepresentations.first?.resource, alt: "", dimensions: dimensions, duration: duration, mimeType: mimeType) + return context.engine.stickers.uploadSticker(peer: peer, resource: EngineMediaResource(resource), thumbnail: file.previewRepresentations.first.flatMap { EngineMediaResource($0.resource) }, alt: "", dimensions: dimensions, duration: duration, mimeType: mimeType) |> mapToSignal { status -> Signal<(UploadStickerStatus, (StickerPackReference, String)?), UploadStickerError> in switch status { case let .progress(progress): return .single((.progress(isVideo ? 0.5 + progress * 0.5 : progress), nil)) case let .complete(resource, _): - let file = stickerFile(resource: resource, thumbnailResource: file.previewRepresentations.first?.resource, size: file.size ?? 0, dimensions: dimensions, duration: file.duration, isVideo: isVideo) + let rawResource = resource._asResource() as! TelegramMediaResource + let file = stickerFile(resource: rawResource, thumbnailResource: file.previewRepresentations.first?.resource, size: file.size ?? 0, dimensions: dimensions, duration: file.duration, isVideo: isVideo) switch action { case .send: return .single((status, nil)) @@ -8117,7 +8116,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } case let .createStickerPack(title): let sticker = ImportSticker( - resource: .standalone(resource: resource), + resource: .standalone(resource: rawResource), emojis: emojis, dimensions: dimensions, duration: duration, @@ -8137,7 +8136,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } case let .addToStickerPack(pack, title): let sticker = ImportSticker( - resource: .standalone(resource: resource), + resource: .standalone(resource: rawResource), emojis: emojis, dimensions: dimensions, duration: duration, @@ -8175,14 +8174,15 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self?.stickerUploadDisposable.set(nil) }) case let .complete(resource, _): + let rawResource = resource._asResource() as! TelegramMediaResource let navigationController = self.effectiveNavigationController as? NavigationController - + let result: MediaEditorScreenImpl.Result switch action { case .update: result = MediaEditorScreenImpl.Result(media: .sticker(file: file, emoji: emojis)) case .upload, .send: - let file = stickerFile(resource: resource, thumbnailResource: file.previewRepresentations.first?.resource, size: resource.size ?? 0, dimensions: dimensions, duration: self.preferredStickerDuration(), isVideo: isVideo) + let file = stickerFile(resource: rawResource, thumbnailResource: file.previewRepresentations.first?.resource, size: rawResource.size ?? 0, dimensions: dimensions, duration: self.preferredStickerDuration(), isVideo: isVideo) result = MediaEditorScreenImpl.Result(media: .sticker(file: file, emoji: emojis)) default: result = MediaEditorScreenImpl.Result() @@ -8443,7 +8443,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.videoExport = nil if let toStickerResource { if let data = try? Data(contentsOf: URL(fileURLWithPath: outputPath)) { - self.context.account.postbox.mediaBox.storeResourceData(toStickerResource.id, data: data, synchronous: true) + self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(toStickerResource.id), data: data, synchronous: true) } } else { saveToPhotos(outputPath, true) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StorySource.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StorySource.swift index bc7c5b5fdc..8c4c21630c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StorySource.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StorySource.swift @@ -1,6 +1,5 @@ import Foundation import SwiftSignalKit -import Postbox import TelegramCore import TelegramUIPreferences import MediaEditor diff --git a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaPlaybackHeaderPanelComponent.swift b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaPlaybackHeaderPanelComponent.swift index c8275e6994..3582eae61c 100644 --- a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaPlaybackHeaderPanelComponent.swift +++ b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaPlaybackHeaderPanelComponent.swift @@ -21,19 +21,22 @@ public final class MediaPlaybackHeaderPanelComponent: Component { public let strings: PresentationStrings public let data: GlobalControlPanelsContext.MediaPlayback public let controller: () -> ViewController? + public let shouldPerformAction: ((@escaping () -> Void) -> Void)? public init( context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, data: GlobalControlPanelsContext.MediaPlayback, - controller: @escaping () -> ViewController? + controller: @escaping () -> ViewController?, + shouldPerformAction: ((@escaping () -> Void) -> Void)? = nil ) { self.context = context self.theme = theme self.strings = strings self.data = data self.controller = controller + self.shouldPerformAction = shouldPerformAction } public static func ==(lhs: MediaPlaybackHeaderPanelComponent, rhs: MediaPlaybackHeaderPanelComponent) -> Bool { @@ -71,6 +74,17 @@ public final class MediaPlaybackHeaderPanelComponent: Component { deinit { self.playlistPreloadDisposable?.dispose() } + + private func performAction(_ action: @escaping () -> Void) { + guard let component = self.component else { + return + } + if let shouldPerformAction = component.shouldPerformAction { + shouldPerformAction(action) + } else { + action() + } + } func update(component: MediaPlaybackHeaderPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme @@ -220,100 +234,110 @@ public final class MediaPlaybackHeaderPanelComponent: Component { }) } panel.togglePlayPause = { [weak self] in - guard let self, let component = self.component else { - return - } - component.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: component.data.kind) + self?.performAction({ [weak self] in + guard let self, let component = self.component else { + return + } + component.context.sharedContext.mediaManager.playlistControl(.playback(.togglePlayPause), type: component.data.kind) + }) } panel.playPrevious = { [weak self] in - guard let self, let component = self.component else { - return - } - component.context.sharedContext.mediaManager.playlistControl(.next, type: component.data.kind) + self?.performAction({ [weak self] in + guard let self, let component = self.component else { + return + } + component.context.sharedContext.mediaManager.playlistControl(.next, type: component.data.kind) + }) } panel.playNext = { [weak self] in - guard let self, let component = self.component else { - return - } - component.context.sharedContext.mediaManager.playlistControl(.previous, type: component.data.kind) + self?.performAction({ [weak self] in + guard let self, let component = self.component else { + return + } + component.context.sharedContext.mediaManager.playlistControl(.previous, type: component.data.kind) + }) } panel.tapAction = { [weak self] in - guard let self, let component = self.component, let controller = component.controller(), let navigationController = controller.navigationController as? NavigationController else { - return - } - - if let id = component.data.item.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = component.data.playlistLocation as? PeerMessagesPlaylistLocation { - if case .music = component.data.kind { - switch playlistLocation { - case .custom, .savedMusic: - let controllerContext: AccountContext - if component.data.account.id == component.context.account.id { - controllerContext = component.context - } else { - controllerContext = component.context.sharedContext.makeTempAccountContext(account: component.data.account) - } - 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)) - 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)) - - var cancelImpl: (() -> Void)? - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let progressSignal = Signal { [weak self] subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - self?.component?.controller()?.present(controller, in: .window(.root)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = MetaDisposable() - var progressStarted = false - self.playlistPreloadDisposable?.dispose() - self.playlistPreloadDisposable = (signal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - |> deliverOnMainQueue).start(next: { [weak self] index in - guard let self, let component = self.component else { - return - } - if let _ = index.0 { - let controllerContext: AccountContext - if component.data.account.id == component.context.account.id { - controllerContext = component.context - } else { - controllerContext = component.context.sharedContext.makeTempAccountContext(account: component.data.account) - } - 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)) - } else if index.1 { - if !progressStarted { - progressStarted = true - progressDisposable.set(progressSignal.start()) - } - } - }, completed: { - }) - cancelImpl = { [weak self] in - self?.playlistPreloadDisposable?.dispose() - } - default: - break - } - } else { - component.context.sharedContext.navigateToChat(accountId: component.context.account.id, peerId: id.messageId.peerId, messageId: id.messageId) + self?.performAction({ [weak self] in + guard let self, let component = self.component, let controller = component.controller(), let navigationController = controller.navigationController as? NavigationController else { + return } - } + + if let id = component.data.item.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = component.data.playlistLocation as? PeerMessagesPlaylistLocation { + if case .music = component.data.kind { + switch playlistLocation { + case .custom, .savedMusic: + let controllerContext: AccountContext + if component.data.account.id == component.context.account.id { + controllerContext = component.context + } else { + controllerContext = component.context.sharedContext.makeTempAccountContext(account: component.data.account) + } + 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) + 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)) + + var cancelImpl: (() -> Void)? + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let progressSignal = Signal { [weak self] subscriber in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + cancelImpl?() + })) + self?.component?.controller()?.present(controller, in: .window(.root)) + return ActionDisposable { [weak controller] in + Queue.mainQueue().async() { + controller?.dismiss() + } + } + } + |> runOn(Queue.mainQueue()) + |> delay(0.15, queue: Queue.mainQueue()) + let progressDisposable = MetaDisposable() + var progressStarted = false + self.playlistPreloadDisposable?.dispose() + self.playlistPreloadDisposable = (signal + |> afterDisposed { + Queue.mainQueue().async { + progressDisposable.dispose() + } + } + |> deliverOnMainQueue).start(next: { [weak self] index in + guard let self, let component = self.component else { + return + } + if let _ = index.0 { + let controllerContext: AccountContext + if component.data.account.id == component.context.account.id { + controllerContext = component.context + } else { + controllerContext = component.context.sharedContext.makeTempAccountContext(account: component.data.account) + } + 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) + playerController.navigationPresentation = .flatModal + controller.push(playerController) + } else if index.1 { + if !progressStarted { + progressStarted = true + progressDisposable.set(progressSignal.start()) + } + } + }, completed: { + }) + cancelImpl = { [weak self] in + self?.playlistPreloadDisposable?.dispose() + } + default: + break + } + } else { + component.context.sharedContext.navigateToChat(accountId: component.context.account.id, peerId: id.messageId.peerId, messageId: id.messageId) + } + } + }) } } diff --git a/submodules/TelegramUI/Components/MessageFeeHeaderPanelComponent/BUILD b/submodules/TelegramUI/Components/MessageFeeHeaderPanelComponent/BUILD index 1791648614..5f9887c618 100644 --- a/submodules/TelegramUI/Components/MessageFeeHeaderPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/MessageFeeHeaderPanelComponent/BUILD @@ -17,7 +17,6 @@ swift_library( "//submodules/ComponentFlow", "//submodules/Components/ComponentDisplayAdapters", "//submodules/PresentationDataUtils", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramUIPreferences", diff --git a/submodules/TelegramUI/Components/MessageFeeHeaderPanelComponent/Sources/ChatFeePanelNode.swift b/submodules/TelegramUI/Components/MessageFeeHeaderPanelComponent/Sources/ChatFeePanelNode.swift index a4a3262b68..2fda8c1f76 100644 --- a/submodules/TelegramUI/Components/MessageFeeHeaderPanelComponent/Sources/ChatFeePanelNode.swift +++ b/submodules/TelegramUI/Components/MessageFeeHeaderPanelComponent/Sources/ChatFeePanelNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift index 13cfb235cb..bfc4b5bec8 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift @@ -7,7 +7,6 @@ import ComponentFlow import SwiftSignalKit import AccountContext import TelegramCore -import Postbox import MultilineTextComponent import AvatarNode import TelegramPresentationData diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift index a5c7b4d4b3..7ee5afa086 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/InputContextQueries.swift @@ -82,9 +82,9 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, cha signal = .single({ _ in return .stickers([]) }) } - let stickerConfiguration = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + let stickerConfiguration = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) |> map { preferencesView -> StickersSearchConfiguration in - let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue + let appConfiguration: AppConfiguration = preferencesView?.get(AppConfiguration.self) ?? .defaultValue return StickersSearchConfiguration.with(appConfiguration: appConfiguration) } let stickerSettings = context.sharedContext.accountManager.transaction { transaction -> StickerSettings in diff --git a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift index 7e7f049436..c299a531f1 100644 --- a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift +++ b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift @@ -227,7 +227,7 @@ final class MiniAppListScreenComponent: Component { return } - if let peerInfoScreen = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let peerInfoScreen = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { peerInfoScreen.navigationPresentation = .modal controller.push(peerInfoScreen) } @@ -504,7 +504,9 @@ final class MiniAppListScreenComponent: Component { let timingFunction: String switch curve { case .easeInOut: - timingFunction = CAMediaTimingFunctionName.easeOut.rawValue + timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue + case .easeIn: + timingFunction = CAMediaTimingFunctionName.easeIn.rawValue case .linear: timingFunction = CAMediaTimingFunctionName.linear.rawValue case .spring: diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift index 71f461ab79..5395153fb8 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift @@ -330,6 +330,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll private var expandedTapGestureRecoginzer: UITapGestureRecognizer? private var currentTransition: Transition? + private var pendingAction: PendingAction? private var isApplyingTransition = false private var validLayout: ContainerViewLayout? @@ -446,6 +447,9 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll return } if let result = self.scrollView.hitTest(gestureRecognizer.location(in: self.scrollView), with: nil), result === self.scrollView { + guard self.canStartMutatingTransition() else { + return + } self.collapse() } } @@ -596,6 +600,62 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } } + private enum PendingAction: Equatable { + case maximize(itemId: AnyHashable) + } + + private func canStartMutatingTransition() -> Bool { + return self.currentTransition == nil && !self.isApplyingTransition + } + + private func performMaximize(for item: Item) { + guard let navigationController = self.navigationController else { + return + } + item.beforeMaximize(navigationController, { [weak self] in + self?.navigationController?.maximizeViewController(item.controller, animated: true) + }) + } + + private func requestOrQueueMaximize(for item: Item) { + if self.canStartMutatingTransition() { + self.performMaximize(for: item) + return + } + if case let .dismiss(dismissingItemId)? = self.currentTransition, dismissingItemId != item.id { + self.pendingAction = .maximize(itemId: item.id) + } + } + + private func drainPendingAction() { + guard self.canStartMutatingTransition(), let pendingAction = self.pendingAction else { + return + } + self.pendingAction = nil + + switch pendingAction { + case let .maximize(itemId): + guard let item = self.items.first(where: { $0.id == itemId }) else { + return + } + self.performMaximize(for: item) + } + } + + private func completeTransition( + _ completedTransition: Transition, + completion: @escaping (Transition) -> Void, + cleanup: () -> Void = {} + ) { + self.isApplyingTransition = false + if self.currentTransition == completedTransition { + self.currentTransition = nil + } + cleanup() + completion(completedTransition) + self.drainPendingAction() + } + public func maximizeController(_ viewController: MinimizableController, animated: Bool, completion: @escaping (Bool) -> Void) { guard let item = self.items.first(where: { $0.controller === viewController }) else { completion(self.items.count == 0) @@ -634,11 +694,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } if self.items.count == 1, let item = self.items.first { - if let navigationController = self.navigationController { - item.beforeMaximize(navigationController, { [weak self] in - self?.navigationController?.maximizeViewController(item.controller, animated: true) - }) - } + self.performMaximize(for: item) } else { let contentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.bounds.height) self.scrollView.contentOffset = CGPoint(x: 0.0, y: contentOffset) @@ -662,6 +718,9 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll return } self.requestUpdate(transition: .immediate) + guard self.canStartMutatingTransition() else { + return + } let contentOffset = scrollView.contentOffset if scrollView.contentOffset.y < -64.0, let lastItemId = self.items.last?.id, let itemNode = self.itemNodes[lastItemId] { @@ -798,6 +857,9 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll return } if self.isExpanded { + guard self.canStartMutatingTransition() else { + return + } let proceed = { [weak self] in guard let self else { return @@ -864,13 +926,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll return } if self.isExpanded, let itemNode { - if let navigationController = self.navigationController { - itemNode.item.beforeMaximize(navigationController, { [weak self, weak itemNode] in - if let item = itemNode?.item { - self?.navigationController?.maximizeViewController(item.controller, animated: true) - } - }) - } + self.requestOrQueueMaximize(for: itemNode.item) } else { self.expand() } @@ -1032,11 +1088,7 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } transition.animatePosition(node: itemNode, from: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + initialOffset), completion: { _ in - self.isApplyingTransition = false - if self.currentTransition == currentTransition { - self.currentTransition = nil - } - completion(currentTransition) + self.completeTransition(currentTransition, completion: completion) }) case let .maximize(itemId): let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) @@ -1075,31 +1127,26 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + maximizeTopInset + self.scrollView.contentOffset.y), completion: { _ in - self.isApplyingTransition = false - if self.currentTransition == currentTransition { - self.currentTransition = nil - } - - completion(currentTransition) - - if let _ = itemNode.snapshotView { - let snapshotContainerView = itemNode.snapshotContainerView - snapshotContainerView.isUserInteractionEnabled = true - snapshotContainerView.layer.allowsGroupOpacity = true - snapshotContainerView.center = CGPoint(x: itemNode.item.controller.displayNode.view.bounds.width / 2.0, y: snapshotContainerView.bounds.height / 2.0) - itemNode.item.controller.displayNode.view.addSubview(snapshotContainerView) - Queue.mainQueue().after(0.35, { - snapshotContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in - snapshotContainerView.removeFromSuperview() + self.completeTransition(currentTransition, completion: completion, cleanup: { + if let _ = itemNode.snapshotView { + let snapshotContainerView = itemNode.snapshotContainerView + snapshotContainerView.isUserInteractionEnabled = true + snapshotContainerView.layer.allowsGroupOpacity = true + snapshotContainerView.center = CGPoint(x: itemNode.item.controller.displayNode.view.bounds.width / 2.0, y: snapshotContainerView.bounds.height / 2.0) + itemNode.item.controller.displayNode.view.addSubview(snapshotContainerView) + Queue.mainQueue().after(0.35, { + snapshotContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + snapshotContainerView.removeFromSuperview() + }) }) - }) - } - - self.itemNodes[itemId] = nil - itemNode.removeFromSupernode() - dimView.removeFromSuperview() - - self.requestUpdate(transition: .immediate) + } + + self.itemNodes[itemId] = nil + itemNode.removeFromSupernode() + dimView.removeFromSuperview() + + self.requestUpdate(transition: .immediate) + }) }) case let .dismiss(itemId): let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) @@ -1129,18 +1176,15 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in - self.isApplyingTransition = false - if self.currentTransition == currentTransition { - self.currentTransition = nil - } - completion(currentTransition) - self.itemNodes[itemId] = nil - itemNode.removeFromSupernode() - dimView.removeFromSuperview() - - self.navigationController?.maximizeViewController(itemNode.item.controller, animated: false) - - self.requestUpdate(transition: .immediate) + self.completeTransition(currentTransition, completion: completion, cleanup: { + self.itemNodes[itemId] = nil + itemNode.removeFromSupernode() + dimView.removeFromSuperview() + + self.navigationController?.maximizeViewController(itemNode.item.controller, animated: false) + + self.requestUpdate(transition: .immediate) + }) }) }) } @@ -1148,18 +1192,14 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll } else { let isLast = self.items.isEmpty transition.updatePosition(node: dismissedItemNode, position: CGPoint(x: -layout.size.width, y: dismissedItemNode.position.y), completion: { _ in - self.isApplyingTransition = false - if self.currentTransition == currentTransition { - self.currentTransition = nil - } - completion(currentTransition) - - self.itemNodes[itemId] = nil - dismissedItemNode.removeFromSupernode() - - if isLast { - self.didDismiss?(self) - } + self.completeTransition(currentTransition, completion: completion, cleanup: { + self.itemNodes[itemId] = nil + dismissedItemNode.removeFromSupernode() + + if isLast { + self.didDismiss?(self) + } + }) }) if isLast { let dismissOffset = collapsedHeight(layout: layout) @@ -1169,20 +1209,12 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll case .dismissAll: let dismissOffset = collapsedHeight(layout: layout) transition.updatePosition(layer: self.bottomEdgeView.layer, position: self.bottomEdgeView.layer.position.offsetBy(dx: 0.0, dy: dismissOffset), completion: { _ in - self.isApplyingTransition = false - if self.currentTransition == currentTransition { - self.currentTransition = nil - } - completion(currentTransition) + self.completeTransition(currentTransition, completion: completion) }) transition.updatePosition(layer: self.scrollView.layer, position: self.scrollView.center.offsetBy(dx: 0.0, dy: dismissOffset)) case .collapse: transition.updateBounds(layer: self.scrollView.layer, bounds: CGRect(origin: .zero, size: self.scrollView.bounds.size), completion: { _ in - self.isApplyingTransition = false - if self.currentTransition == currentTransition { - self.currentTransition = nil - } - completion(currentTransition) + self.completeTransition(currentTransition, completion: completion) }) default: break diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD b/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD index 4f5a66267d..e5a853e167 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD @@ -1,44 +1,4 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") -load( - "@build_bazel_rules_apple//apple:resources.bzl", - "apple_resource_bundle", - "apple_resource_group", -) -load("//build-system/bazel-utils:plist_fragment.bzl", - "plist_fragment", -) - -filegroup( - name = "MultiAnimationRendererMetalResources", - srcs = glob([ - "Resources/**/*.metal", - ]), - visibility = ["//visibility:public"], -) - -plist_fragment( - name = "WallpaperBackgroundNodeBundleInfoPlist", - extension = "plist", - template = - """ - CFBundleIdentifier - org.telegram.MultiAnimationRenderer - CFBundleDevelopmentRegion - en - CFBundleName - MultiAnimationRenderer - """ -) - -apple_resource_bundle( - name = "MultiAnimationRendererBundle", - infoplists = [ - ":WallpaperBackgroundNodeBundleInfoPlist", - ], - resources = [ - ":MultiAnimationRendererMetalResources", - ], -) swift_library( name = "MultiAnimationRenderer", @@ -46,9 +6,6 @@ swift_library( srcs = glob([ "Sources/**/*.swift", ]), - data = [ - ":MultiAnimationRendererBundle", - ], copts = [ "-warnings-as-errors", ], diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Resources/MultiAnimationRendererShaders.metal b/submodules/TelegramUI/Components/MultiAnimationRenderer/Resources/MultiAnimationRendererShaders.metal deleted file mode 100644 index 8343f753f0..0000000000 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Resources/MultiAnimationRendererShaders.metal +++ /dev/null @@ -1,46 +0,0 @@ -#include -using namespace metal; - -typedef struct { - packed_float2 position; - packed_float2 texCoord; -} Vertex; - -typedef struct { - float4 position[[position]]; - float2 texCoord; -} Varyings; - -vertex Varyings multiAnimationVertex( - unsigned int vid[[vertex_id]], - constant Vertex *verticies[[buffer(0)]], - constant uint2 &resolution[[buffer(1)]], - constant uint2 &slotSize[[buffer(2)]], - constant uint2 &slotPosition[[buffer(3)]] -) { - Varyings out; - constant Vertex &v = verticies[vid]; - - out.position = float4(float2(v.position), 0.0, 1.0); - out.texCoord = v.texCoord; - - return out; -} - -fragment half4 multiAnimationFragment( - Varyings in[[stage_in]], - texture2d textureY[[texture(0)]], - texture2d textureU[[texture(1)]], - texture2d textureV[[texture(2)]], - texture2d textureA[[texture(3)]] -) { - constexpr sampler s(address::clamp_to_edge, filter::linear); - - half y = textureY.sample(s, in.texCoord).r; - half u = textureU.sample(s, in.texCoord).r - 0.5; - half v = textureV.sample(s, in.texCoord).r - 0.5; - half a = textureA.sample(s, in.texCoord).r; - - half4 out = half4(1.5748 * v + y, -0.1873 * v + y, 1.8556 * u + y, a); - return half4(out.b, out.g, out.r, out.a); -} diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift deleted file mode 100644 index 8b13789179..0000000000 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift +++ /dev/null @@ -1 +0,0 @@ - diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index e0671fd539..6bf0029d65 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -3,8 +3,6 @@ import UIKit import SwiftSignalKit import Display import AnimationCache -import Accelerate -import IOSurface public protocol MultiAnimationRenderer: AnyObject { func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable @@ -19,10 +17,10 @@ private var nextRenderTargetId: Int64 = 1 open class MultiAnimationRenderTarget: SimpleLayer { public let id: Int64 public var numFrames: Int? - - let deinitCallbacks = Bag<() -> Void>() - let updateStateCallbacks = Bag<() -> Void>() - + + public let deinitCallbacks = Bag<() -> Void>() + public let updateStateCallbacks = Bag<() -> Void>() + public final var shouldBeAnimating: Bool = false { didSet { if self.shouldBeAnimating != oldValue { @@ -32,7 +30,7 @@ open class MultiAnimationRenderTarget: SimpleLayer { } } } - + public var blurredRepresentationBackgroundColor: UIColor? public var blurredRepresentationTarget: CALayer? { didSet { @@ -43,1109 +41,39 @@ open class MultiAnimationRenderTarget: SimpleLayer { } } } - + public override init() { assert(Thread.isMainThread) - + self.id = nextRenderTargetId nextRenderTargetId += 1 - + super.init() } - + public override init(layer: Any) { guard let layer = layer as? MultiAnimationRenderTarget else { preconditionFailure() } - + self.id = layer.id - + super.init(layer: layer) } - + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + deinit { for f in self.deinitCallbacks.copyItems() { f() } } - + open func updateDisplayPlaceholder(displayPlaceholder: Bool) { } - + open func transitionToContents(_ contents: AnyObject, didLoop: Bool) { } } - -private final class LoadFrameGroupTask { - let task: () -> () -> Void - let queueAffinity: Int - - init(task: @escaping () -> () -> Void, queueAffinity: Int) { - self.task = task - self.queueAffinity = queueAffinity - } -} - -private var yuvToRgbConversion: vImage_YpCbCrToARGB = { - var info = vImage_YpCbCrToARGB() - var pixelRange = vImage_YpCbCrPixelRange(Yp_bias: 16, CbCr_bias: 128, YpRangeMax: 235, CbCrRangeMax: 240, YpMax: 255, YpMin: 0, CbCrMax: 255, CbCrMin: 0) - vImageConvert_YpCbCrToARGB_GenerateConversion(kvImage_YpCbCrToARGBMatrix_ITU_R_709_2, &pixelRange, &info, kvImage420Yp8_Cb8_Cr8, kvImageARGB8888, 0) - return info -}() - -private final class ItemAnimationContext { - fileprivate final class Frame { - let frame: AnimationCacheItemFrame - let duration: Double - - let contentsAsImage: UIImage? - let contentsAsCVPixelBuffer: CVPixelBuffer? - - let size: CGSize - - var remainingDuration: Double - - private var blurredRepresentationValue: UIImage? - - init?(frame: AnimationCacheItemFrame) { - self.frame = frame - self.duration = frame.duration - self.remainingDuration = frame.duration - - switch frame.format { - case let .rgba(data, width, height, bytesPerRow): - guard let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) else { - return nil - } - - data.withUnsafeBytes { bytes -> Void in - memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) - } - - guard let image = context.generateImage() else { - return nil - } - - self.contentsAsImage = image - self.contentsAsCVPixelBuffer = nil - self.size = CGSize(width: CGFloat(width), height: CGFloat(height)) - case let .yuva(y, u, v, a): - var pixelBuffer: CVPixelBuffer? = nil - let _ = CVPixelBufferCreate(kCFAllocatorDefault, y.width, y.height, kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar, [ - kCVPixelBufferIOSurfacePropertiesKey: NSDictionary() - ] as CFDictionary, &pixelBuffer) - guard let pixelBuffer else { - return nil - } - - CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) - defer { - CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) - } - guard let baseAddressY = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else { - return nil - } - guard let baseAddressCbCr = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else { - return nil - } - guard let baseAddressA = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2) else { - return nil - } - - let dstBufferY = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressY), height: vImagePixelCount(y.height), width: vImagePixelCount(y.width), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)) - let dstBufferCbCr = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressCbCr), height: vImagePixelCount(y.height / 2), width: vImagePixelCount(y.width / 2), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)) - let dstBufferA = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddressA), height: vImagePixelCount(y.height), width: vImagePixelCount(y.width), rowBytes: CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 2)) - - y.data.withUnsafeBytes { (yBytes: UnsafeRawBufferPointer) -> Void in - if dstBufferY.rowBytes == y.bytesPerRow { - memcpy(dstBufferY.data, yBytes.baseAddress!, yBytes.count) - } else { - for i in 0 ..< y.height { - memcpy(dstBufferY.data.advanced(by: dstBufferY.rowBytes * i), yBytes.baseAddress!.advanced(by: y.bytesPerRow * i), y.bytesPerRow) - } - } - } - - a.data.withUnsafeBytes { (aBytes: UnsafeRawBufferPointer) -> Void in - if dstBufferA.rowBytes == a.bytesPerRow { - memcpy(dstBufferA.data, aBytes.baseAddress!, aBytes.count) - } else { - for i in 0 ..< y.height { - memcpy(dstBufferA.data.advanced(by: dstBufferA.rowBytes * i), aBytes.baseAddress!.advanced(by: a.bytesPerRow * i), a.bytesPerRow) - } - } - } - - u.data.withUnsafeBytes { (uBytes: UnsafeRawBufferPointer) -> Void in - v.data.withUnsafeBytes { (vBytes: UnsafeRawBufferPointer) -> Void in - let sourceU = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: uBytes.baseAddress!), - height: vImagePixelCount(u.height), - width: vImagePixelCount(u.width), - rowBytes: u.bytesPerRow - ) - let sourceV = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: vBytes.baseAddress!), - height: vImagePixelCount(v.height), - width: vImagePixelCount(v.width), - rowBytes: v.bytesPerRow - ) - - withUnsafePointer(to: sourceU, { sourceU in - withUnsafePointer(to: sourceV, { sourceV in - var srcPlanarBuffers: [ - UnsafePointer? - ] = [sourceU, sourceV] - var destChannels: [UnsafeMutableRawPointer?] = [ - dstBufferCbCr.data.advanced(by: 1), - dstBufferCbCr.data - ] - - let channelCount = 2 - - vImageConvert_PlanarToChunky8( - &srcPlanarBuffers, - &destChannels, - UInt32(channelCount), - MemoryLayout.stride * channelCount, - vImagePixelCount(u.width), - vImagePixelCount(u.height), - dstBufferCbCr.rowBytes, - vImage_Flags(kvImageDoNotTile) - ) - }) - }) - } - } - - self.contentsAsImage = nil - self.contentsAsCVPixelBuffer = pixelBuffer - self.size = CGSize(width: CGFloat(y.width), height: CGFloat(y.height)) - } - } - - func blurredRepresentation(color: UIColor?) -> UIImage? { - if let blurredRepresentationValue = self.blurredRepresentationValue { - return blurredRepresentationValue - } - - switch frame.format { - case let .rgba(data, width, height, bytesPerRow): - let blurredWidth = 12 - let blurredHeight = 12 - guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: bytesPerRow) else { - return nil - } - - let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)) - - data.withUnsafeBytes { bytes -> Void in - if let dataProvider = CGDataProvider(dataInfo: nil, data: bytes.baseAddress!, size: bytes.count, releaseData: { _, _, _ in }) { - let image = CGImage( - width: width, - height: height, - bitsPerComponent: 8, - bitsPerPixel: 32, - bytesPerRow: bytesPerRow, - space: DeviceGraphicsContextSettings.shared.colorSpace, - bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo, - provider: dataProvider, - decode: nil, - shouldInterpolate: true, - intent: .defaultIntent - ) - if let image = image { - context.withFlippedContext { c in - c.setFillColor((color ?? .white).cgColor) - c.fill(CGRect(origin: CGPoint(), size: size)) - c.draw(image, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8))) - } - } - } - - var destinationBuffer = vImage_Buffer() - destinationBuffer.width = UInt(blurredWidth) - destinationBuffer.height = UInt(blurredHeight) - destinationBuffer.data = context.bytes - destinationBuffer.rowBytes = context.bytesPerRow - - vImageBoxConvolve_ARGB8888(&destinationBuffer, - &destinationBuffer, - nil, - 0, 0, - UInt32(15), - UInt32(15), - nil, - vImage_Flags(kvImageTruncateKernel)) - - let divisor: Int32 = 0x1000 - - let rwgt: CGFloat = 0.3086 - let gwgt: CGFloat = 0.6094 - let bwgt: CGFloat = 0.0820 - - let adjustSaturation: CGFloat = 1.7 - - let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation - let b = (1.0 - adjustSaturation) * rwgt - let c = (1.0 - adjustSaturation) * rwgt - let d = (1.0 - adjustSaturation) * gwgt - let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation - let f = (1.0 - adjustSaturation) * gwgt - let g = (1.0 - adjustSaturation) * bwgt - let h = (1.0 - adjustSaturation) * bwgt - let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation - - let satMatrix: [CGFloat] = [ - a, b, c, 0, - d, e, f, 0, - g, h, i, 0, - 0, 0, 0, 1 - ] - - var matrix: [Int16] = satMatrix.map { value in - return Int16(value * CGFloat(divisor)) - } - - vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) - - context.withFlippedContext { c in - c.setFillColor((color ?? .white).withMultipliedAlpha(0.6).cgColor) - c.fill(CGRect(origin: CGPoint(), size: size)) - } - } - - self.blurredRepresentationValue = context.generateImage() - return self.blurredRepresentationValue - case let .yuva(y, u, v, a): - let blurredWidth = 12 - let blurredHeight = 12 - let size = CGSize(width: blurredWidth, height: blurredHeight) - - var sourceY = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: y.data.withUnsafeBytes { $0.baseAddress! }), - height: vImagePixelCount(y.height), - width: vImagePixelCount(y.width), - rowBytes: y.bytesPerRow - ) - - var sourceU = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: u.data.withUnsafeBytes { $0.baseAddress! }), - height: vImagePixelCount(u.height), - width: vImagePixelCount(u.width), - rowBytes: u.bytesPerRow - ) - - var sourceV = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: v.data.withUnsafeBytes { $0.baseAddress! }), - height: vImagePixelCount(v.height), - width: vImagePixelCount(v.width), - rowBytes: v.bytesPerRow - ) - - var sourceA = vImage_Buffer( - data: UnsafeMutableRawPointer(mutating: a.data.withUnsafeBytes { $0.baseAddress! }), - height: vImagePixelCount(a.height), - width: vImagePixelCount(a.width), - rowBytes: a.bytesPerRow - ) - - let scaledYData = malloc(blurredWidth * blurredHeight)! - defer { - free(scaledYData) - } - - let scaledUData = malloc(blurredWidth * blurredHeight / 4)! - defer { - free(scaledUData) - } - - let scaledVData = malloc(blurredWidth * blurredHeight / 4)! - defer { - free(scaledVData) - } - - let scaledAData = malloc(blurredWidth * blurredHeight)! - defer { - free(scaledAData) - } - - var scaledY = vImage_Buffer( - data: scaledYData, - height: vImagePixelCount(blurredHeight), - width: vImagePixelCount(blurredWidth), - rowBytes: blurredWidth - ) - - var scaledU = vImage_Buffer( - data: scaledUData, - height: vImagePixelCount(blurredHeight / 2), - width: vImagePixelCount(blurredWidth / 2), - rowBytes: blurredWidth / 2 - ) - - var scaledV = vImage_Buffer( - data: scaledVData, - height: vImagePixelCount(blurredHeight / 2), - width: vImagePixelCount(blurredWidth / 2), - rowBytes: blurredWidth / 2 - ) - - var scaledA = vImage_Buffer( - data: scaledAData, - height: vImagePixelCount(blurredHeight), - width: vImagePixelCount(blurredWidth), - rowBytes: blurredWidth - ) - - vImageScale_Planar8(&sourceY, &scaledY, nil, vImage_Flags(kvImageHighQualityResampling)) - vImageScale_Planar8(&sourceU, &scaledU, nil, vImage_Flags(kvImageHighQualityResampling)) - vImageScale_Planar8(&sourceV, &scaledV, nil, vImage_Flags(kvImageHighQualityResampling)) - vImageScale_Planar8(&sourceA, &scaledA, nil, vImage_Flags(kvImageHighQualityResampling)) - - guard let context = DrawingContext(size: size, scale: 1.0, clear: true) else { - return nil - } - - var destinationBuffer = vImage_Buffer( - data: context.bytes, - height: vImagePixelCount(blurredHeight), - width: vImagePixelCount(blurredWidth), - rowBytes: context.bytesPerRow - ) - - var result = kvImageNoError - - var permuteMap: [UInt8] = [1, 2, 3, 0] - result = vImageConvert_420Yp8_Cb8_Cr8ToARGB8888(&scaledY, &scaledU, &scaledV, &destinationBuffer, &yuvToRgbConversion, &permuteMap, 255, vImage_Flags(kvImageDoNotTile)) - if result != kvImageNoError { - return nil - } - - result = vImageOverwriteChannels_ARGB8888(&scaledA, &destinationBuffer, &destinationBuffer, 1 << 0, vImage_Flags(kvImageDoNotTile)); - if result != kvImageNoError { - return nil - } - - vImageBoxConvolve_ARGB8888(&destinationBuffer, - &destinationBuffer, - nil, - 0, 0, - UInt32(15), - UInt32(15), - nil, - vImage_Flags(kvImageTruncateKernel)) - - let divisor: Int32 = 0x1000 - - let rwgt: CGFloat = 0.3086 - let gwgt: CGFloat = 0.6094 - let bwgt: CGFloat = 0.0820 - - let adjustSaturation: CGFloat = 1.7 - - let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation - let b = (1.0 - adjustSaturation) * rwgt - let c = (1.0 - adjustSaturation) * rwgt - let d = (1.0 - adjustSaturation) * gwgt - let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation - let f = (1.0 - adjustSaturation) * gwgt - let g = (1.0 - adjustSaturation) * bwgt - let h = (1.0 - adjustSaturation) * bwgt - let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation - - let satMatrix: [CGFloat] = [ - a, b, c, 0, - d, e, f, 0, - g, h, i, 0, - 0, 0, 0, 1 - ] - - var matrix: [Int16] = satMatrix.map { value in - return Int16(value * CGFloat(divisor)) - } - - vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) - - context.withFlippedContext { c in - c.setFillColor((color ?? .white).withMultipliedAlpha(0.6).cgColor) - c.fill(CGRect(origin: CGPoint(), size: size)) - } - - self.blurredRepresentationValue = context.generateImage() - return self.blurredRepresentationValue - } - } - } - - static let queue0 = Queue(name: "ItemAnimationContext-0", qos: .default) - static let queue1 = Queue(name: "ItemAnimationContext-1", qos: .default) - - private let useYuvA: Bool - - private let cache: AnimationCache - let queueAffinity: Int - private let stateUpdated: () -> Void - - private var disposable: Disposable? - private var displayLink: ConstantDisplayLinkAnimator? - private var item: Atomic? - private var itemPlaceholderAndFrameIndex: (UIImage, Int)? - - private var currentFrame: Frame? - private var loadingFrameTaskId: Int? - private var nextLoadingFrameTaskId: Int = 0 - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - self.stateUpdated() - } - } - } - - let targets = Bag>() - - init(cache: AnimationCache, queueAffinity: Int, itemId: String, size: CGSize, useYuvA: Bool, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, stateUpdated: @escaping () -> Void) { - self.cache = cache - self.queueAffinity = queueAffinity - self.useYuvA = useYuvA - self.stateUpdated = stateUpdated - - self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - if let item = result.item { - strongSelf.item = Atomic(value: item) - } - if let (placeholder, index) = strongSelf.itemPlaceholderAndFrameIndex { - strongSelf.itemPlaceholderAndFrameIndex = nil - strongSelf.setFrameIndex(index: index, placeholder: placeholder) - } - strongSelf.updateIsPlaying() - } - }) - } - - deinit { - self.disposable?.dispose() - self.displayLink?.invalidate() - } - - func setFrameIndex(index: Int, placeholder: UIImage) { - if let item = self.item { - let nextFrame = item.with { item -> AnimationCacheItemFrame? in - item.reset() - for i in 0 ... index { - let result = item.advance(advance: .frames(1), requestedFormat: .rgba) - if i == index { - return result?.frame - } - } - return nil - } - - self.loadingFrameTaskId = nil - - if let nextFrame = nextFrame, let currentFrame = Frame(frame: nextFrame) { - self.currentFrame = currentFrame - - for target in self.targets.copyItems() { - if let target = target.value { - if let image = currentFrame.contentsAsImage { - target.transitionToContents(image.cgImage!, didLoop: false) - } else if let pixelBuffer = currentFrame.contentsAsCVPixelBuffer { - target.transitionToContents(pixelBuffer, didLoop: false) - } - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - } - } - } - } else { - for target in self.targets.copyItems() { - if let target = target.value { - target.transitionToContents(placeholder.cgImage!, didLoop: false) - } - } - - self.itemPlaceholderAndFrameIndex = (placeholder, index) - } - } - - func updateAddedTarget(target: MultiAnimationRenderTarget) { - if let currentFrame = self.currentFrame { - if let cgImage = currentFrame.contentsAsImage?.cgImage { - target.transitionToContents(cgImage, didLoop: false) - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - } else if let pixelBuffer = currentFrame.contentsAsCVPixelBuffer { - target.transitionToContents(pixelBuffer, didLoop: false) - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - } - } - - self.updateIsPlaying() - } - - func updateIsPlaying() { - var isPlaying = true - if self.item == nil { - isPlaying = false - } - - var shouldBeAnimating = false - for target in self.targets.copyItems() { - if let target = target.value { - if target.shouldBeAnimating { - shouldBeAnimating = true - break - } - } - } - if !shouldBeAnimating { - isPlaying = false - } - - self.isPlaying = isPlaying - } - - func animationTick(advanceTimestamp: Double) -> LoadFrameGroupTask? { - return self.update(advanceTimestamp: advanceTimestamp) - } - - private func update(advanceTimestamp: Double) -> LoadFrameGroupTask? { - guard let item = self.item else { - return nil - } - - var frameAdvance: AnimationCacheItem.Advance? - if self.loadingFrameTaskId == nil { - if let currentFrame = self.currentFrame, advanceTimestamp > 0.0 { - let divisionFactor = advanceTimestamp / currentFrame.remainingDuration - let wholeFactor = round(divisionFactor) - if abs(wholeFactor - divisionFactor) < 0.005 { - currentFrame.remainingDuration = 0.0 - frameAdvance = .frames(Int(wholeFactor)) - } else { - currentFrame.remainingDuration -= advanceTimestamp - if currentFrame.remainingDuration <= 0.0 { - frameAdvance = .duration(currentFrame.duration + max(0.0, -currentFrame.remainingDuration)) - } - } - } else if self.currentFrame == nil { - frameAdvance = .frames(1) - } - } - - if let frameAdvance = frameAdvance, self.loadingFrameTaskId == nil { - let taskId = self.nextLoadingFrameTaskId - self.nextLoadingFrameTaskId += 1 - - self.loadingFrameTaskId = taskId - let useYuvA = self.useYuvA - - return LoadFrameGroupTask(task: { [weak self] in - let currentFrame: (frame: Frame, didLoop: Bool)? - do { - if let (frame, didLoop) = try item.tryWith({ item -> (AnimationCacheItemFrame, Bool)? in - let defaultFormat: AnimationCacheItemFrame.RequestedFormat - if useYuvA { - defaultFormat = .yuva(rowAlignment: 1) - } else { - defaultFormat = .rgba - } - - if let result = item.advance(advance: frameAdvance, requestedFormat: defaultFormat) { - return (result.frame, result.didLoop) - } else { - return nil - } - }), let mappedFrame = Frame(frame: frame) { - currentFrame = (mappedFrame, didLoop) - } else { - currentFrame = nil - } - } catch { - assertionFailure() - currentFrame = nil - } - - return { - guard let strongSelf = self else { - return - } - - if strongSelf.loadingFrameTaskId != taskId { - return - } - - strongSelf.loadingFrameTaskId = nil - - if let currentFrame = currentFrame { - strongSelf.currentFrame = currentFrame.frame - for target in strongSelf.targets.copyItems() { - if let target = target.value { - if let image = currentFrame.frame.contentsAsImage { - target.transitionToContents(image.cgImage!, didLoop: currentFrame.didLoop) - } else if let pixelBuffer = currentFrame.frame.contentsAsCVPixelBuffer { - target.transitionToContents(pixelBuffer, didLoop: currentFrame.didLoop) - } - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = currentFrame.frame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - } - } - } - } - }, queueAffinity: self.queueAffinity) - } - - if let _ = self.currentFrame { - for target in self.targets.copyItems() { - if let target = target.value { - target.updateDisplayPlaceholder(displayPlaceholder: false) - } - } - } - - return nil - } -} - -public final class MultiAnimationRendererImpl: MultiAnimationRenderer { - private final class GroupContext { - private let firstFrameQueue: Queue - private let stateUpdated: () -> Void - - private struct ItemKey: Hashable { - var id: String - var width: Int - var height: Int - var uniqueId: Int - } - - private var itemContexts: [ItemKey: ItemAnimationContext] = [:] - private var nextQueueAffinity: Int = 0 - private var nextUniqueId: Int = 1 - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - self.stateUpdated() - } - } - } - - init(firstFrameQueue: Queue, stateUpdated: @escaping () -> Void) { - self.firstFrameQueue = firstFrameQueue - self.stateUpdated = stateUpdated - } - - func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, useYuvA: Bool, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { - var uniqueId = 0 - if unique { - uniqueId = self.nextUniqueId - self.nextUniqueId += 1 - } - - let itemKey = ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: uniqueId) - let itemContext: ItemAnimationContext - if let current = self.itemContexts[itemKey] { - itemContext = current - } else { - let queueAffinity = self.nextQueueAffinity - self.nextQueueAffinity += 1 - itemContext = ItemAnimationContext(cache: cache, queueAffinity: queueAffinity, itemId: itemId, size: size, useYuvA: useYuvA, fetch: fetch, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.itemContexts[itemKey] = itemContext - } - - let index = itemContext.targets.add(Weak(target)) - itemContext.updateAddedTarget(target: target) - - let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in - Queue.mainQueue().async { - guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { - return - } - itemContext.targets.remove(index) - if itemContext.targets.isEmpty { - strongSelf.itemContexts.removeValue(forKey: itemKey) - } - } - } - - let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in - guard let itemContext = itemContext else { - return - } - itemContext.updateIsPlaying() - } - - return ActionDisposable { [weak self, weak itemContext, weak target] in - guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { - return - } - if let target = target { - target.deinitCallbacks.remove(deinitIndex) - target.updateStateCallbacks.remove(updateStateIndex) - } - itemContext.targets.remove(index) - if itemContext.targets.isEmpty { - strongSelf.itemContexts.removeValue(forKey: itemKey) - } - }.strict() - } - - func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { - if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) { - guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else { - return false - } - guard let loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) else { - return false - } - - if let image = loadedFrame.contentsAsImage { - target.contents = image.cgImage - } else if let pixelBuffer = loadedFrame.contentsAsCVPixelBuffer { - target.contents = pixelBuffer - } - target.numFrames = item.numFrames - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - - return true - } else { - return false - } - } - - func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { - var hadIntermediateUpdate = false - return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { [weak target] item in - guard let item = item.item else { - let isFinal = item.isFinal - hadIntermediateUpdate = true - Queue.mainQueue().async { - completion(false, isFinal) - } - return - } - - let loadedFrame: ItemAnimationContext.Frame? - if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { - loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) - } else { - loadedFrame = nil - } - - Queue.mainQueue().async { - guard let target = target else { - completion(false, true) - return - } - target.numFrames = item.numFrames - if let loadedFrame = loadedFrame { - if let cgImage = loadedFrame.contentsAsImage?.cgImage { - if hadIntermediateUpdate { - target.transitionToContents(cgImage, didLoop: false) - } else { - target.contents = cgImage - } - } else if let pixelBuffer = loadedFrame.contentsAsCVPixelBuffer { - if hadIntermediateUpdate { - target.transitionToContents(pixelBuffer, didLoop: false) - } else { - target.contents = pixelBuffer - } - } - - if let blurredRepresentationTarget = target.blurredRepresentationTarget { - blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage - } - - completion(true, true) - } else { - completion(false, true) - } - } - }).strict() - } - - func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { - return cache.getFirstFrame(queue: self.firstFrameQueue, sourceId: itemId, size: size, fetch: fetch, completion: { item in - guard let item = item.item else { - Queue.mainQueue().async { - completion(nil) - } - return - } - - let loadedFrame: ItemAnimationContext.Frame? - if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { - loadedFrame = ItemAnimationContext.Frame(frame: frame.frame) - } else { - loadedFrame = nil - } - - Queue.mainQueue().async { - if let loadedFrame = loadedFrame { - if let cgImage = loadedFrame.contentsAsImage?.cgImage { - completion(cgImage) - } else { - completion(nil) - } - } else { - completion(nil) - } - } - }).strict() - } - - func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { - if let itemContext = self.itemContexts[ItemKey(id: itemId, width: Int(size.width), height: Int(size.height), uniqueId: 0)] { - itemContext.setFrameIndex(index: frameIndex, placeholder: placeholder) - } - } - - private func updateIsPlaying() { - var isPlaying = false - for (_, itemContext) in self.itemContexts { - if itemContext.isPlaying { - isPlaying = true - break - } - } - - self.isPlaying = isPlaying - } - - func animationTick(advanceTimestamp: Double) -> [LoadFrameGroupTask] { - var tasks: [LoadFrameGroupTask] = [] - for (_, itemContext) in self.itemContexts { - if itemContext.isPlaying { - if let task = itemContext.animationTick(advanceTimestamp: advanceTimestamp) { - tasks.append(task) - } - } - } - - return tasks - } - } - - public static let firstFrameQueue = Queue(name: "MultiAnimationRenderer-FirstFrame", qos: .userInteractive) - - public var useYuvA: Bool = false - private var groupContext: GroupContext? - private var frameSkip: Int - private var displayTimer: Foundation.Timer? - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - if self.isPlaying { - if self.displayTimer == nil { - final class TimerTarget: NSObject { - private let f: () -> Void - - init(_ f: @escaping () -> Void) { - self.f = f - } - - @objc func timerEvent() { - self.f() - } - } - let frameInterval = Double(self.frameSkip) / 60.0 - let displayTimer = Foundation.Timer(timeInterval: frameInterval, target: TimerTarget { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.animationTick(frameInterval: frameInterval) - }, selector: #selector(TimerTarget.timerEvent), userInfo: nil, repeats: true) - self.displayTimer = displayTimer - RunLoop.main.add(displayTimer, forMode: .common) - } - } else { - if let displayTimer = self.displayTimer { - self.displayTimer = nil - displayTimer.invalidate() - } - } - } - } - } - - public init() { - if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.processorCount > 2 { - self.frameSkip = 1 - } else { - self.frameSkip = 2 - } - } - - public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { - let groupContext: GroupContext - if let current = self.groupContext { - groupContext = current - } else { - groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContext = groupContext - } - - let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, unique: unique, size: size, useYuvA: self.useYuvA, fetch: fetch) - - return ActionDisposable { - disposable.dispose() - }.strict() - } - - public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { - let groupContext: GroupContext - if let current = self.groupContext { - groupContext = current - } else { - groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContext = groupContext - } - - return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size) - } - - public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { - let groupContext: GroupContext - if let current = self.groupContext { - groupContext = current - } else { - groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContext = groupContext - } - - return groupContext.loadFirstFrame(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() - } - - public func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { - let groupContext: GroupContext - if let current = self.groupContext { - groupContext = current - } else { - groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContext = groupContext - } - - return groupContext.loadFirstFrameAsImage(cache: cache, itemId: itemId, size: size, fetch: fetch, completion: completion).strict() - } - - public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { - if let groupContext = self.groupContext { - groupContext.setFrameIndex(itemId: itemId, size: size, frameIndex: frameIndex, placeholder: placeholder) - } - } - - private func updateIsPlaying() { - var isPlaying = false - if let groupContext = self.groupContext { - if groupContext.isPlaying { - isPlaying = true - } - } - - self.isPlaying = isPlaying - } - - private func animationTick(frameInterval: Double) { - let secondsPerFrame = frameInterval - - var tasks: [LoadFrameGroupTask] = [] - if let groupContext = self.groupContext { - if groupContext.isPlaying { - tasks.append(contentsOf: groupContext.animationTick(advanceTimestamp: secondsPerFrame)) - } - } - - if !tasks.isEmpty { - let tasks0 = tasks.filter { $0.queueAffinity % 2 == 0 } - let tasks1 = tasks.filter { $0.queueAffinity % 2 == 1 } - let allTasks = [tasks0, tasks1] - - let taskCompletions = Atomic<[Int: [() -> Void]]>(value: [:]) - let queues: [Queue] = [ItemAnimationContext.queue0, ItemAnimationContext.queue1] - - for i in 0 ..< 2 { - let partTasks = allTasks[i] - let id = i - queues[i].async { - var completions: [() -> Void] = [] - for task in partTasks { - let complete = task.task() - completions.append(complete) - } - - var complete = false - let _ = taskCompletions.modify { current in - var current = current - current[id] = completions - if current.count == 2 { - complete = true - } - return current - } - - if complete { - Queue.mainQueue().async { - let allCompletions = taskCompletions.with { $0 } - for (_, fs) in allCompletions { - for f in fs { - f() - } - } - } - } - } - } - } - } -} diff --git a/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/SoftwareVideoThumbnailLayer.swift b/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/SoftwareVideoThumbnailLayer.swift index bf26679418..026641a2a6 100644 --- a/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/SoftwareVideoThumbnailLayer.swift +++ b/submodules/TelegramUI/Components/MultiplexedVideoNode/Sources/SoftwareVideoThumbnailLayer.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import TelegramCore -import Postbox import SwiftSignalKit import Display import PhotoResources diff --git a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationBarImpl.swift index bf2eaacaa4..9d217ebf3f 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 { @@ -579,11 +583,14 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { public var secondaryContentHeight: CGFloat private var edgeEffectExtension: CGFloat = 0.0 - private var edgeEffectView: EdgeEffectView? + private var edgeEffectViewImpl: EdgeEffectView? + public var edgeEffectView: UIView? { + return self.edgeEffectViewImpl + } private var backgroundContainer: GlassBackgroundContainerView? public var backgroundView: UIView { - if let edgeEffectView = self.edgeEffectView { + if let edgeEffectView = self.edgeEffectViewImpl { return edgeEffectView } else { return self.backgroundNode.view @@ -631,10 +638,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) @@ -654,7 +657,7 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { if case .glass = presentationData.theme.style { let edgeEffectView = EdgeEffectView() edgeEffectView.isUserInteractionEnabled = false - self.edgeEffectView = edgeEffectView + self.edgeEffectViewImpl = edgeEffectView self.view.addSubview(edgeEffectView) let backgroundContainer = GlassBackgroundContainerView() @@ -721,6 +724,12 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { } } } + self.leftButtonNodeImpl.requestUpdate = { [weak self] in + guard let self else { + return + } + self.requestLayout() + } self.rightButtonNodeImpl.pressed = { [weak self] index in if let item = self?.item { @@ -733,6 +742,12 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { } } } + self.rightButtonNodeImpl.requestUpdate = { [weak self] in + guard let self else { + return + } + self.requestLayout() + } } public var isBackgroundVisible: Bool { @@ -753,10 +768,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) @@ -810,7 +821,7 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { backgroundContainer.update(size: backgroundContainerFrame.size, isDark: self.presentationData.theme.overallDarkAppearance, transition: ComponentTransition(transition)) } - if let edgeEffectView = self.edgeEffectView { + if let edgeEffectView = self.edgeEffectViewImpl { if let edgeEffectColor = self.presentationData.theme.edgeEffectColor, edgeEffectColor.alpha == 0.0 { edgeEffectView.isHidden = true } else { @@ -906,6 +917,18 @@ 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 .accentDisabled: + 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 +960,18 @@ 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 .accentDisabled: + 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 +1010,18 @@ 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 .accentDisabled: + leftButtonsColor = .init(kind: .custom(style: self.presentationData.theme.glassStyle == .clear ? .clear : .default, color: self.presentationData.theme.accentDisabledButtonColor)) + 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 +1047,18 @@ 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 .accentDisabled: + rightButtonsColor = .init(kind: .custom(style: self.presentationData.theme.glassStyle == .clear ? .clear : .default, color: self.presentationData.theme.accentDisabledButtonColor)) + 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 } @@ -1017,17 +1073,18 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { } if self.titleNode.view.superview != nil { - let titleSize = self.titleNode.updateLayout(CGSize(width: max(1.0, size.width - max(leftTitleInset, rightTitleInset) * 2.0), height: nominalHeight)) + var transition = transition + if self.titleNode.frame.width.isZero { + transition = .immediate + } + self.titleNode.alpha = 1.0 - do { - var transition = transition - if self.titleNode.frame.width.isZero { - transition = .immediate - } - self.titleNode.alpha = 1.0 - - let titleOffset: CGFloat = 0.0 - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + titleOffset + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize)) + let titleSize = self.titleNode.updateLayout(CGSize(width: max(1.0, size.width - leftTitleInset - rightTitleInset), height: nominalHeight)) + + if titleSize.width <= size.width - max(leftTitleInset, rightTitleInset) * 2.0 { + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize)) + } else { + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftTitleInset + floor((size.width - leftTitleInset - rightTitleInset - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize)) } } @@ -1038,7 +1095,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 +1107,17 @@ 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) + leftTitleInset = max(leftTitleInset, rightTitleInset) + rightTitleInset = leftTitleInset + + 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 @@ -1072,7 +1138,7 @@ public final class NavigationBarImpl: ASDisplayNode, NavigationBar { } private func applyEdgeEffectExtension(transition: ContainedViewLayoutTransition) { - if let edgeEffectView = self.edgeEffectView { + if let edgeEffectView = self.edgeEffectViewImpl { transition.updateTransform(layer: edgeEffectView.layer, transform: CATransform3DMakeTranslation(0.0, max(0.0, min(20.0, self.edgeEffectExtension)), 0.0)) } } diff --git a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationButtonNode.swift b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationButtonNode.swift index 0446f196a0..9970050187 100644 --- a/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationButtonNode.swift +++ b/submodules/TelegramUI/Components/NavigationBarImpl/Sources/NavigationButtonNode.swift @@ -211,6 +211,7 @@ private final class ItemComponent: Component { private final class NavigationButtonItemNode: ImmediateTextNode { private let isGlass: Bool + var requestUpdate: (() -> Void)? private func fontForCurrentState() -> UIFont { return self.bold ? Font.semibold(17.0) : Font.medium(17.0) @@ -235,7 +236,13 @@ private final class NavigationButtonItemNode: ImmediateTextNode { if let item = self.item { self.setEnabledListener = item.addSetEnabledListener { [weak self] value in - self?.isEnabled = value + guard let self else { + return + } + if self.isEnabled != value { + self.isEnabled = value + self.requestUpdate?() + } } self.accessibilityHint = item.accessibilityHint self.accessibilityLabel = item.accessibilityLabel @@ -261,7 +268,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 +278,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 +316,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 +431,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,11 +533,20 @@ private final class NavigationButtonItemNode: ImmediateTextNode { public final class NavigationButtonNodeImpl: ContextControllerSourceNode, NavigationButtonNode { + enum ContentType { + case accent + case accentDisabled + case generic + } + private let isGlass: Bool private var isBack: Bool = false + private var items: [UIBarButtonItem] = [] private var nodes: [NavigationButtonItemNode] = [] + var requestUpdate: (() -> Void)? + private var disappearingNodes: [(frame: CGRect, size: CGSize, node: NavigationButtonItemNode)] = [] public var singleCustomNode: ASDisplayNode? { @@ -625,6 +643,9 @@ public final class NavigationButtonNodeImpl: ContextControllerSourceNode, Naviga } } } + node.requestUpdate = { [weak self] in + self?.requestUpdate?() + } self.nodes.append(node) self.addSubnode(node) } @@ -648,6 +669,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 { @@ -670,14 +693,20 @@ public final class NavigationButtonNodeImpl: ContextControllerSourceNode, Naviga } } } + node.requestUpdate = { [weak self] in + self?.requestUpdate?() + } self.nodes.append(node) self.addSubnode(node) } 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 +778,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 +820,30 @@ 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" { + if item.isEnabled { + nodeContentType = .accent + } else { + nodeContentType = .accentDisabled + } + } + + if commonType == nil { + commonType = nodeContentType + } else { + if commonType != nodeContentType { + return .generic + } + } + } + + return commonType ?? .generic + } } diff --git a/submodules/TelegramUI/Components/NotificationPeerExceptionController/BUILD b/submodules/TelegramUI/Components/NotificationPeerExceptionController/BUILD index a7990a3978..c4c42c4916 100644 --- a/submodules/TelegramUI/Components/NotificationPeerExceptionController/BUILD +++ b/submodules/TelegramUI/Components/NotificationPeerExceptionController/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift b/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift index c9fcea3e0a..0a4c7ffdcc 100644 --- a/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift +++ b/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift @@ -1116,7 +1116,7 @@ public func notificationPeerExceptionController( break } }), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }) ], parseMarkdown: true), in: .window(.root)) } diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index a432b7cef8..b15f8aa782 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -59,6 +59,7 @@ final class PeerAllowedReactionsScreenComponent: Component { var id: AnyHashable var version: Int var isPreset: Bool + var canLoadMore: Bool } private struct EmojiSearchState { @@ -111,6 +112,7 @@ final class PeerAllowedReactionsScreenComponent: Component { self.emojiSearchState.set(.single(self.emojiSearchStateValue)) } } + private var emojiSearchContext: EmojiSearchContext? private var emptyResultEmojis: [TelegramMediaFile] = [] private var stableEmptyResultEmoji: TelegramMediaFile? @@ -471,7 +473,7 @@ final class PeerAllowedReactionsScreenComponent: Component { } else { self.stableEmptyResultEmoji = nil } - emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : .active) + emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : .active, canLoadMore: emojiSearchResult.canLoadMore) } else { self.stableEmptyResultEmoji = nil @@ -598,17 +600,20 @@ final class PeerAllowedReactionsScreenComponent: Component { switch query { case .none: + self.emojiSearchContext = nil self.emojiSearchDisposable.set(nil) - self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) case let .text(rawQuery, languageCode): let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) if query.isEmpty { + self.emojiSearchContext = nil self.emojiSearchDisposable.set(nil) - self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) } else { let context = component.context let isEmojiOnly = !"".isEmpty + self.emojiSearchContext = nil var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) if !languageCode.lowercased().hasPrefix("en") { @@ -633,7 +638,7 @@ final class PeerAllowedReactionsScreenComponent: Component { } |> distinctUntilChanged - let resultSignal: Signal<[EmojiPagerContentComponent.ItemGroup], NoError> + let resultSignal: Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?), NoError> do { let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> = .single((FoundStickerSets(), false)) |> then( @@ -644,7 +649,7 @@ final class PeerAllowedReactionsScreenComponent: Component { let localPacksSignal: Signal = context.engine.stickers.searchEmojiSets(query: query) resultSignal = signal - |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + |> mapToSignal { keywords -> Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?), NoError> in var allEmoticons: [String: String] = [:] for keyword in keywords { for emoticon in keyword.emoticons { @@ -687,9 +692,10 @@ final class PeerAllowedReactionsScreenComponent: Component { fillWithLoadingPlaceholders: false, items: items )) - return .single(resultGroups) + return .single((resultGroups, false, false, nil)) } else { - let remoteSignal = context.engine.stickers.searchEmoji(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode) + let emojiSearchContext = context.engine.stickers.emojiSearchContext(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode) + let remoteSignal = emojiSearchContext.state return combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1), @@ -699,7 +705,7 @@ final class PeerAllowedReactionsScreenComponent: Component { remoteSignal, localPacksSignal ) - |> map { view, availableReactions, hasPremium, foundPacks, foundEmoji, foundLocalPacks -> [EmojiPagerContentComponent.ItemGroup] in + |> map { view, availableReactions, hasPremium, foundPacks, foundEmoji, foundLocalPacks -> (groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?) in var result: [(String, TelegramMediaFile.Accessor?, String)] = [] var allEmoticons: [String: String] = [:] @@ -842,7 +848,7 @@ final class PeerAllowedReactionsScreenComponent: Component { )) } } - return resultGroups + return (resultGroups, foundEmoji.canLoadMore, foundEmoji.items.isEmpty && foundEmoji.isLoadingMore, emojiSearchContext) } } } @@ -857,11 +863,13 @@ final class PeerAllowedReactionsScreenComponent: Component { return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + self.emojiSearchContext = result.searchContext + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.groups, id: AnyHashable(query), version: version, isPreset: false, canLoadMore: result.canLoadMore), isSearching: result.isSearching) version += 1 })) } case let .category(value): + self.emojiSearchContext = nil let resultSignal: Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> do { resultSignal = component.context.engine.stickers.searchEmoji(category: value) @@ -938,11 +946,11 @@ final class PeerAllowedReactionsScreenComponent: Component { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) version += 1 })) } @@ -950,6 +958,9 @@ final class PeerAllowedReactionsScreenComponent: Component { updateScrollingToItemGroup: { }, onScroll: {}, + loadMore: { [weak self] in + self?.emojiSearchContext?.loadMore() + }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, diff --git a/submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem/BUILD b/submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem/BUILD index 074697e8d6..b7264d4587 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/PresentationDataUtils", "//submodules/TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift index 67084d3172..ef43f94545 100644 --- a/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen/Sources/JoinAffiliateProgramScreen.swift @@ -875,7 +875,7 @@ private final class JoinAffiliateProgramScreenComponent: Component { guard let infoController = component.context.sharedContext.makePeerInfoController( context: component.context, updatedPresentationData: nil, - peer: component.sourcePeer._asPeer(), + peer: component.sourcePeer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, diff --git a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/BUILD b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/BUILD index 78ff393298..c6a470262e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/PresentationDataUtils", "//submodules/TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerCopyProtectionInfoScreen/Sources/PeerCopyProtectionInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerCopyProtectionInfoScreen/Sources/PeerCopyProtectionInfoScreen.swift index ed1a58fd27..69c09e989e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerCopyProtectionInfoScreen/Sources/PeerCopyProtectionInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerCopyProtectionInfoScreen/Sources/PeerCopyProtectionInfoScreen.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import AccountContext diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/BUILD index 903ae1e85b..c4f5d3e07d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", "//submodules/TelegramCore", - "//submodules/Postbox", "//submodules/TelegramPresentationData", "//submodules/AccountContext", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift index 8b389de408..141796f01a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -71,7 +71,7 @@ public final class PeerInfoCoverComponent: Component { func colors(context: AccountContext, isDark: Bool) -> (UIColor, UIColor)? { switch self { case let .peer(peer): - if let colors = peer._asPeer().profileColor.flatMap({ context.peerNameColors.getProfile($0, dark: isDark) }) { + if let colors = peer.profileColor.flatMap({ context.peerNameColors.getProfile($0, dark: isDark) }) { let backgroundColor = colors.main let secondaryBackgroundColor = colors.secondary ?? colors.main return (backgroundColor, secondaryBackgroundColor) @@ -79,7 +79,7 @@ public final class PeerInfoCoverComponent: Component { return nil } case let .managedBot(peer): - if let color = peer._asPeer().nameColor { + if let color = peer.nameColor { let colors = calculateAvatarColors(context: context, explicitColorIndex: nil, peerId: peer.id, nameColor: color, icon: .none, theme: nil) if colors.count == 2 { return (colors[0], colors[1]) @@ -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) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift index ba7530fe28..01da82422c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoRatingComponent/Sources/PeerInfoRatingComponent.swift @@ -343,7 +343,7 @@ public final class PeerInfoRatingComponent: Component { } if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_outer", withExtension: "svg"), let data = try? Data(contentsOf: url) { - if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.borderColor) { + if let image = generateTintedImage(image: drawSvgImage(data: data, size: size, backgroundColor: nil, foregroundColor: nil, scale: 0.0, opaque: false), color: component.borderColor) { image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0) } } @@ -372,7 +372,7 @@ public final class PeerInfoRatingComponent: Component { } if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_inner", withExtension: "svg"), let data = try? Data(contentsOf: url) { - if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.backgroundColor) { + if let image = generateTintedImage(image: drawSvgImage(data: data, size: size, backgroundColor: nil, foregroundColor: nil, scale: 0.0, opaque: false), color: component.backgroundColor) { image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommentItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommentItem.swift index d81eca2ccd..17a891e9f2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommentItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommentItem.swift @@ -167,7 +167,11 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode { let height = textSize.height + verticalInset * 2.0 - transition.updateFrame(node: self.textNode, frame: textFrame) + if self.textNode.frame.size != textFrame.size { + self.textNode.frame = textFrame + } else { + transition.updateFrame(node: self.textNode, frame: textFrame) + } self.activateArea.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift index 9dab9b2ccc..341aa2690a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift @@ -6,7 +6,6 @@ import TelegramPresentationData import ItemListPeerItem import SwiftSignalKit import AccountContext -import Postbox import TelegramCore import ItemListUI diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift index bfbc701147..3df2239c71 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift @@ -20,7 +20,7 @@ enum PeerInfoScreenMemberItemAction { final class PeerInfoScreenMemberItem: PeerInfoScreenItem { let id: AnyHashable let context: ItemListPeerItem.Context - let enclosingPeer: Peer? + let enclosingPeer: EnginePeer? let member: PeerInfoMember let badge: String? let isAccount: Bool @@ -31,7 +31,7 @@ final class PeerInfoScreenMemberItem: PeerInfoScreenItem { init( id: AnyHashable, context: ItemListPeerItem.Context, - enclosingPeer: Peer?, + enclosingPeer: EnginePeer?, member: PeerInfoMember, badge: String? = nil, isAccount: Bool, @@ -149,9 +149,9 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { case .member: var canEditRank = false if item.member.id == item.context.accountPeerId { - if let channel = item.enclosingPeer as? TelegramChannel, channel.hasPermission(.editRank) { + if case let .channel(channel) = item.enclosingPeer, channel.hasPermission(.editRank) { canEditRank = true - } else if let group = item.enclosingPeer as? TelegramGroup, !group.hasBannedPermission(.banEditRank) { + } else if case let .legacyGroup(group) = item.enclosingPeer, !group.hasBannedPermission(.banEditRank) { canEditRank = true } } @@ -178,13 +178,13 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: item.enclosingPeer, member: item.member) var options: [ItemListPeerItemRevealOption] = [] - if actions.contains(.promote) && item.enclosingPeer is TelegramChannel { + if actions.contains(.promote), case .channel = item.enclosingPeer { options.append(ItemListPeerItemRevealOption(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: { item.action?(.promote) })) } if actions.contains(.restrict) { - if item.enclosingPeer is TelegramChannel { + if case .channel = item.enclosingPeer { options.append(ItemListPeerItemRevealOption(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: { item.action?(.restrict) })) @@ -220,7 +220,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { itemText = .presence } - let peerItem = ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: item.context, peer: EnginePeer(item.member.peer), height: itemHeight, presence: item.member.presence.flatMap(EnginePeer.Presence.init), text: itemText, label: itemLabel, editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: nil), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: false, animateFirstAvatarTransition: !item.isAccount, sectionId: 0, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in + let peerItem = ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: item.context, peer: item.member.peer, height: itemHeight, presence: item.member.presence.flatMap(EnginePeer.Presence.init), text: itemText, label: itemLabel, editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: nil), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: false, animateFirstAvatarTransition: !item.isAccount, sectionId: 0, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in }, removePeer: { _ in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift index b1600c7312..483ae0b683 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoGroupsInCommonPaneNode.swift @@ -25,23 +25,23 @@ private struct GroupsInCommonListTransaction { private struct GroupsInCommonListEntry: Comparable, Identifiable { var index: Int - var peer: Peer - + var peer: EnginePeer + var stableId: PeerId { return self.peer.id } - + static func ==(lhs: GroupsInCommonListEntry, rhs: GroupsInCommonListEntry) -> Bool { - return lhs.peer.isEqual(rhs.peer) + return lhs.peer == rhs.peer } static func <(lhs: GroupsInCommonListEntry, rhs: GroupsInCommonListEntry) -> Bool { return lhs.index < rhs.index } - func item(context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, openPeer: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem { let peer = self.peer - return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: EnginePeer(self.peer), presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: { + return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: self.peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: { openPeer(peer) }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in @@ -51,7 +51,7 @@ private struct GroupsInCommonListEntry: Comparable, Identifiable { } } -private func preparedTransition(from fromEntries: [GroupsInCommonListEntry], to toEntries: [GroupsInCommonListEntry], context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> GroupsInCommonListTransaction { +private func preparedTransition(from fromEntries: [GroupsInCommonListEntry], to toEntries: [GroupsInCommonListEntry], context: AccountContext, presentationData: PresentationData, openPeer: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?) -> Void) -> GroupsInCommonListTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } @@ -65,7 +65,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { private let context: AccountContext private let peerId: PeerId private let chatControllerInteraction: ChatControllerInteraction - private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void + private let openPeerContextAction: (Bool, EnginePeer, ASDisplayNode, ContextGesture?) -> Void private let groupsInCommonContext: GroupsInCommonContext weak var parentController: ViewController? @@ -106,7 +106,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { private var disposable: Disposable? - init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) { + init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, EnginePeer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) { self.context = context self.peerId = peerId self.chatControllerInteraction = chatControllerInteraction @@ -229,11 +229,11 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode { var entries: [GroupsInCommonListEntry] = [] for peer in state.peers { if let peer = peer.peer { - entries.append(GroupsInCommonListEntry(index: entries.count, peer: peer)) + entries.append(GroupsInCommonListEntry(index: entries.count, peer: EnginePeer(peer))) } } let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, openPeer: { [weak self] peer in - self?.chatControllerInteraction.openPeer(EnginePeer(peer), .default, nil, .default) + self?.chatControllerInteraction.openPeer(peer, .default, nil, .default) }, openPeerContextAction: { [weak self] peer, node, gesture in self?.openPeerContextAction(false, peer, node, gesture) }) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift index 7ba0773cb5..240e6fb2f4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift @@ -89,7 +89,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable { } } - func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: EnginePeer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem { switch self { case let .addMember(_, text): return ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: PresentationResourcesItemList.addPersonIcon(presentationData.theme), title: text, alwaysPlain: true, sectionId: 0, height: .compactPeerList, color: .accent, editing: false, action: { @@ -110,9 +110,9 @@ private enum PeerMembersListEntry: Comparable, Identifiable { case .member: var canEditRank = false if member.id == context.account.peerId { - if let channel = enclosingPeer as? TelegramChannel, channel.hasPermission(.editRank) { + if case let .channel(channel) = enclosingPeer, channel.hasPermission(.editRank) { canEditRank = true - } else if let group = enclosingPeer as? TelegramGroup, !group.hasBannedPermission(.banEditRank) { + } else if case let .legacyGroup(group) = enclosingPeer, !group.hasBannedPermission(.banEditRank) { canEditRank = true } } @@ -139,13 +139,13 @@ private enum PeerMembersListEntry: Comparable, Identifiable { let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: enclosingPeer, member: member) var options: [ItemListPeerItemRevealOption] = [] - if actions.contains(.promote) && enclosingPeer is TelegramChannel { + if actions.contains(.promote), case .channel = enclosingPeer { options.append(ItemListPeerItemRevealOption(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: { action(member, .promote) })) } if actions.contains(.restrict) { - if enclosingPeer is TelegramChannel { + if case .channel = enclosingPeer { options.append(ItemListPeerItemRevealOption(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: { action(member, .restrict) })) @@ -165,7 +165,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable { } var status: ContactsPeerItemStatus = .presence(presence, presentationData.dateTimeFormat) - if let user = member.peer as? TelegramUser, let botInfo = user.botInfo { + if case let .user(user) = member.peer, let botInfo = user.botInfo { let botStatus: String if botInfo.flags.contains(.hasAccessToChatHistory) { botStatus = presentationData.strings.Bot_GroupStatusReadsHistory @@ -188,7 +188,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable { displayOrder: presentationData.nameDisplayOrder, context: context, peerMode: .memberList, - peer: .peer(peer: EnginePeer(member.peer), chatPeer: EnginePeer(member.peer)), + peer: .peer(peer: member.peer, chatPeer: member.peer), status: status, rightLabelText: label.flatMap { .init(text: $0, color: labelColor, hasBackground: labelBackground) }, enabled: true, @@ -268,7 +268,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable { } } -private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> PeerMembersListTransaction { +private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: EnginePeer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> PeerMembersListTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } @@ -290,7 +290,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { private let listMaskView: UIImageView private let listNode: ListView private var currentEntries: [PeerMembersListEntry] = [] - private var enclosingPeer: Peer? + private var enclosingPeer: EnginePeer? private var currentState: PeerInfoMembersState? private var canLoadMore: Bool = false private var enqueuedTransactions: [PeerMembersListTransaction] = [] @@ -358,9 +358,9 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { return } - strongSelf.enclosingPeer = enclosingPeer._asPeer() + strongSelf.enclosingPeer = enclosingPeer strongSelf.currentState = state - strongSelf.updateState(enclosingPeer: enclosingPeer._asPeer(), state: state, presentationData: presentationData) + strongSelf.updateState(enclosingPeer: enclosingPeer, state: state, presentationData: presentationData) }) self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in @@ -439,7 +439,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode { } } - private func updateState(enclosingPeer: Peer, state: PeerInfoMembersState, presentationData: PresentationData) { + private func updateState(enclosingPeer: EnginePeer, state: PeerInfoMembersState, presentationData: PresentationData) { var entries: [PeerMembersListEntry] = [] if state.canAddMembers { entries.append(.addMember(presentationData.theme, presentationData.strings.GroupInfo_AddParticipant)) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift index 31e13a2fd3..4bb5725e15 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoRecommendedPeersPane.swift @@ -65,7 +65,7 @@ private enum RecommendedPeersListEntry: Comparable, Identifiable { } } - func item(context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem { + func item(context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem { switch self { case let .peer(_, _, peer, subscribers): let text: ItemListPeerItemText @@ -85,13 +85,13 @@ private enum RecommendedPeersListEntry: Comparable, Identifiable { }, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, contextAction: { node, gesture in - openPeerContextAction(peer._asPeer(), node, gesture) + openPeerContextAction(peer, node, gesture) }, hasTopStripe: false, noInsets: true, noCorners: true, style: .plain, disableInteractiveTransitionIfNecessary: true) } } } -private func preparedTransition(from fromEntries: [RecommendedPeersListEntry], to toEntries: [RecommendedPeersListEntry], context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> RecommendedPeersListTransaction { +private func preparedTransition(from fromEntries: [RecommendedPeersListEntry], to toEntries: [RecommendedPeersListEntry], context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?) -> Void) -> RecommendedPeersListTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } @@ -116,7 +116,7 @@ extension RecommendedBots: RecommendedPeers { final class PeerInfoRecommendedPeersPaneNode: ASDisplayNode, PeerInfoPaneNode { private let context: AccountContext private let chatControllerInteraction: ChatControllerInteraction - private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void + private let openPeerContextAction: (Bool, EnginePeer, ASDisplayNode, ContextGesture?) -> Void weak var parentController: ViewController? @@ -152,7 +152,7 @@ final class PeerInfoRecommendedPeersPaneNode: ASDisplayNode, PeerInfoPaneNode { private var disposable: Disposable? - init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void) { + init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, EnginePeer, ASDisplayNode, ContextGesture?) -> Void) { self.context = context self.chatControllerInteraction = chatControllerInteraction self.openPeerContextAction = openPeerContextAction diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift index 00af60cf77..aa6d9242e3 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift @@ -23,7 +23,7 @@ final class PeerInfoAvatarListNode: ASDisplayNode { let isReady = Promise() - var arguments: (Peer?, Int64?, EngineMessageHistoryThread.Info?, PresentationTheme, CGFloat, Bool)? + var arguments: (EnginePeer?, Int64?, EngineMessageHistoryThread.Info?, PresentationTheme, CGFloat, Bool)? var item: PeerInfoAvatarListItem? var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)? @@ -128,7 +128,7 @@ final class PeerInfoAvatarListNode: ASDisplayNode { } } - func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, isForum: Bool, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { + func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: EnginePeer?, isForum: Bool, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) { self.arguments = (peer, threadId, threadInfo, theme, avatarSize, isExpanded) self.maskNode.isForum = isForum self.pinchSourceNode.update(size: size, transition: transition) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift index aaf95233c6..7a99f8ac79 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarTransformContainerNode.swift @@ -101,7 +101,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { self.playbackStartDisposable.dispose() } - func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme, peer: Peer?) { + func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme, peer: EnginePeer?) { var colors = AvatarNode.Colors(theme: theme) let regularNavigationContentsSecondaryColor: UIColor @@ -160,7 +160,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { } var isForum = false - if let peer, let channel = peer as? TelegramChannel, channel.isForumOrMonoForum { + if let peer, case let .channel(channel) = peer, channel.isForumOrMonoForum { isForum = true } @@ -210,7 +210,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { } private struct Params { - let peer: Peer? + let peer: EnginePeer? let threadId: Int64? let threadInfo: EngineMessageHistoryThread.Info? let item: PeerInfoAvatarListItem? @@ -219,7 +219,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { let isExpanded: Bool let isSettings: Bool - init(peer: Peer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) { + init(peer: EnginePeer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) { self.peer = peer self.threadId = threadId self.threadInfo = threadInfo @@ -251,7 +251,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { ) } - func update(peer: Peer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) { + func update(peer: EnginePeer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) { self.params = Params(peer: peer, threadId: threadId, threadInfo: threadInfo, item: item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: isSettings) if let peer = peer { @@ -282,7 +282,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { } self.avatarNode.imageNode.animateFirstTransition = !isSettings - self.avatarNode.setPeer(context: self.context, theme: theme, peer: EnginePeer(peer), overrideImage: overrideImage, clipStyle: .none, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: avatarSize, height: avatarSize), storeUnrounded: true) + self.avatarNode.setPeer(context: self.context, theme: theme, peer: peer, overrideImage: overrideImage, clipStyle: .none, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: avatarSize, height: avatarSize), storeUnrounded: true) if let threadInfo = threadInfo { self.avatarNode.isHidden = true @@ -327,7 +327,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode { var isForum = false let avatarCornerRadius: CGFloat - if let channel = peer as? TelegramChannel, channel.isForumOrMonoForum { + if case let .channel(channel) = peer, channel.isForumOrMonoForum { avatarCornerRadius = floor(avatarSize * 0.25) isForum = true } else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoBirthdayOverlay.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoBirthdayOverlay.swift index d6e4eec46c..9047755ab8 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoBirthdayOverlay.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoBirthdayOverlay.swift @@ -98,7 +98,7 @@ final class PeerInfoBirthdayOverlay: ASDisplayNode { let animationNode = DefaultAnimatedStickerNodeImpl() let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.media.resource, fitzModifier: nil) - let pathPrefix = self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.media.resource.id) + let pathPrefix = self.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.media.resource.id)) animationNode.setup(source: source, width: Int(pixelSize.width), height: Int(pixelSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix)) self.addSubnode(animationNode) @@ -132,7 +132,7 @@ final class PeerInfoBirthdayOverlay: ASDisplayNode { let animationNode = DefaultAnimatedStickerNodeImpl() let source = AnimatedStickerResourceSource(account: self.context.account, resource: file.media.resource, fitzModifier: nil) - let pathPrefix: String? = self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.media.resource.id) + let pathPrefix: String? = self.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.media.resource.id)) animationNode.setup(source: source, width: Int(pixelSize.width), height: Int(pixelSize.height), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) self.addSubnode(animationNode) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 9a6e073c4e..dea0dfbc1c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -383,9 +383,9 @@ final class PeerInfoPersonalChannelData: Equatable { } final class PeerInfoScreenData { - let peer: Peer? - let chatPeer: Peer? - let savedMessagesPeer: Peer? + let peer: EnginePeer? + let chatPeer: EnginePeer? + let savedMessagesPeer: EnginePeer? let cachedData: CachedPeerData? let status: PeerInfoStatusData? let peerNotificationSettings: TelegramPeerNotificationSettings? @@ -393,8 +393,8 @@ final class PeerInfoScreenData { let globalNotificationSettings: EngineGlobalNotificationSettings? let availablePanes: [PeerInfoPaneKey] let groupsInCommon: GroupsInCommonContext? - let linkedDiscussionPeer: Peer? - let linkedMonoforumPeer: Peer? + let linkedDiscussionPeer: EnginePeer? + let linkedMonoforumPeer: EnginePeer? let members: PeerInfoMembersData? let storyListContext: StoryListContext? let storyArchiveListContext: StoryListContext? @@ -425,6 +425,7 @@ final class PeerInfoScreenData { let savedMusicContext: ProfileSavedMusicContext? let savedMusicState: ProfileSavedMusicContext.State? let managedByBot: EnginePeer? + let businessConnectedBot: EnginePeer? let _isContact: Bool var forceIsContact: Bool = false @@ -438,9 +439,9 @@ final class PeerInfoScreenData { } init( - peer: Peer?, - chatPeer: Peer?, - savedMessagesPeer: Peer?, + peer: EnginePeer?, + chatPeer: EnginePeer?, + savedMessagesPeer: EnginePeer?, cachedData: CachedPeerData?, status: PeerInfoStatusData?, peerNotificationSettings: TelegramPeerNotificationSettings?, @@ -449,8 +450,8 @@ final class PeerInfoScreenData { isContact: Bool, availablePanes: [PeerInfoPaneKey], groupsInCommon: GroupsInCommonContext?, - linkedDiscussionPeer: Peer?, - linkedMonoforumPeer: Peer?, + linkedDiscussionPeer: EnginePeer?, + linkedMonoforumPeer: EnginePeer?, members: PeerInfoMembersData?, storyListContext: StoryListContext?, storyArchiveListContext: StoryListContext?, @@ -480,7 +481,8 @@ final class PeerInfoScreenData { webAppPermissions: WebAppPermissionsState?, savedMusicContext: ProfileSavedMusicContext?, savedMusicState: ProfileSavedMusicContext.State?, - managedByBot: EnginePeer? + managedByBot: EnginePeer?, + businessConnectedBot: EnginePeer? ) { self.peer = peer self.chatPeer = chatPeer @@ -525,6 +527,7 @@ final class PeerInfoScreenData { self.savedMusicContext = savedMusicContext self.savedMusicState = savedMusicState self.managedByBot = managedByBot + self.businessConnectedBot = businessConnectedBot } } @@ -931,6 +934,20 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, let profileGiftsContext = ProfileGiftsContext(account: context.account, peerId: peerId) + let businessConnectedBot = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.BusinessConnectedBot(id: context.account.peerId) + ) + |> map { bot -> EnginePeer.Id? in + return bot?.id + } + |> distinctUntilChanged + |> mapToSignal { botPeerId -> Signal in + guard let botPeerId else { + return .single(nil) + } + return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: botPeerId)) + } + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), accountsAndPeers, @@ -940,7 +957,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, combineLatest(notificationExceptions, notificationsAuthorizationStatus.get(), notificationsWarningSuppressed.get()), combineLatest(context.account.viewTracker.featuredStickerPacks(), archivedStickerPacks), hasPassport, - context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)), context.engine.notices.getServerProvidedSuggestions(), context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), @@ -956,9 +973,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, bots, peerInfoPersonalOrLinkedChannel(context: context, peerId: peerId, isSettings: true), starsState, - tonState + tonState, + businessConnectedBot ) - |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots, personalChannel, starsState, tonState -> PeerInfoScreenData in + |> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots, personalChannel, starsState, tonState, businessConnectedBot -> PeerInfoScreenData in let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications let (featuredStickerPacks, archivedStickerPacks) = stickerPacks @@ -970,7 +988,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, }) var enableQRLogin = false - let appConfiguration = accountPreferences.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) + let appConfiguration = accountPreferences?.get(AppConfiguration.self) if let appConfiguration, let data = appConfiguration.data, let enableQR = data["qr_login_camera"] as? Bool, enableQR { enableQRLogin = true } @@ -1006,8 +1024,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, ) return PeerInfoScreenData( - peer: peer, - chatPeer: peer, + peer: peer.flatMap(EnginePeer.init), + chatPeer: peer.flatMap(EnginePeer.init), savedMessagesPeer: nil, cachedData: peerView.cachedData, status: nil, @@ -1048,7 +1066,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, webAppPermissions: nil, savedMusicContext: nil, savedMusicState: nil, - managedByBot: nil + managedByBot: nil, + businessConnectedBot: businessConnectedBot ) } } @@ -1120,7 +1139,8 @@ func peerInfoScreenData( webAppPermissions: nil, savedMusicContext: nil, savedMusicState: nil, - managedByBot: nil + managedByBot: nil, + businessConnectedBot: nil )) case let .user(userPeerId, secretChatId, kind): let groupsInCommon: GroupsInCommonContext? @@ -1131,14 +1151,7 @@ func peerInfoScreenData( } else { groupsInCommon = nil } - - let recommendedBots: Signal - if case .bot = kind { - recommendedBots = context.engine.peers.recommendedBots(peerId: userPeerId) - } else { - recommendedBots = .single(nil) - } - + let premiumGiftOptions: Signal<[PremiumGiftCodeOption], NoError> let profileGiftsContext: ProfileGiftsContext? let profileGiftsCollectionsContext: ProfileGiftsCollectionsContext? @@ -1260,7 +1273,7 @@ func peerInfoScreenData( |> distinctUntilChanged var secretChatKeyFingerprint: Signal = .single(nil) - if let secretChatId = secretChatId { + if let secretChatId { secretChatKeyFingerprint = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.SecretChatKeyFingerprint(id: secretChatId)) } @@ -1290,21 +1303,7 @@ func peerInfoScreenData( } else { hasStoryArchive = .single(false) } - - var botPreviewStoryListContext: StoryListContext? - let hasBotPreviewItems: Signal - if case .bot = kind { - let botPreviewStoryListContextValue = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: peerId, language: nil, assumeEmpty: false) - botPreviewStoryListContext = botPreviewStoryListContextValue - hasBotPreviewItems = botPreviewStoryListContextValue.state - |> map { state in - return !state.items.isEmpty - } - |> distinctUntilChanged - } else { - hasBotPreviewItems = .single(false) - } - + let accountIsPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> map { peer -> Bool in return peer?.isPremium ?? false @@ -1387,66 +1386,109 @@ func peerInfoScreenData( hasSavedMessageTags = .single(false) } - let starsRevenueContextAndState = combineLatest( - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> distinctUntilChanged, - context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId)) - |> distinctUntilChanged - ) - |> mapToSignal { peer, canViewRevenue -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in - var canViewStarsRevenue = canViewRevenue - if let peer, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) || context.sharedContext.applicationBindings.appBuildType == .internal || context.sharedContext.immediateExperimentalUISettings.devRequests { - canViewStarsRevenue = true - } - #if DEBUG - canViewStarsRevenue = peerId != context.account.peerId - #endif + let recommendedBots: Signal + var botPreviewStoryListContext: StoryListContext? + let hasBotPreviewItems: Signal + let starsRevenueContextAndState: Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> + let revenueContextAndState: Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> + let webAppPermissions: Signal + if case .bot = kind { + recommendedBots = context.engine.peers.recommendedBots(peerId: userPeerId) - guard canViewStarsRevenue else { - return .single((nil, nil)) + let botPreviewStoryListContextValue = BotPreviewStoryListContext(account: context.account, engine: context.engine, peerId: peerId, language: nil, assumeEmpty: false) + botPreviewStoryListContext = botPreviewStoryListContextValue + hasBotPreviewItems = botPreviewStoryListContextValue.state + |> map { state in + return !state.items.isEmpty } - let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId, ton: false) - return starsRevenueStatsContext.state - |> map { state -> (StarsRevenueStatsContext?, StarsRevenueStats?) in - return (starsRevenueStatsContext, state.stats) - } - } - - let revenueContextAndState = combineLatest( - context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> distinctUntilChanged, - context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId)) |> distinctUntilChanged - ) - |> mapToSignal { peer, canViewRevenue -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in - var canViewRevenue = canViewRevenue - if let peer, case let .user(user) = peer, let _ = user.botInfo, context.sharedContext.applicationBindings.appBuildType == .internal || context.sharedContext.immediateExperimentalUISettings.devRequests { - canViewRevenue = true + + starsRevenueContextAndState = combineLatest( + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> distinctUntilChanged, + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId)) + |> distinctUntilChanged + ) + |> mapToSignal { peer, canViewRevenue -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in + var canViewStarsRevenue = canViewRevenue + if let peer, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) || context.sharedContext.applicationBindings.appBuildType == .internal || context.sharedContext.immediateExperimentalUISettings.devRequests { + canViewStarsRevenue = true + } + #if DEBUG + canViewStarsRevenue = peerId != context.account.peerId + #endif + + guard canViewStarsRevenue else { + return .single((nil, nil)) + } + let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId, ton: false) + return starsRevenueStatsContext.state + |> map { state -> (StarsRevenueStatsContext?, StarsRevenueStats?) in + return (starsRevenueStatsContext, state.stats) + } } - #if DEBUG - canViewRevenue = peerId != context.account.peerId - #endif - guard canViewRevenue else { - return .single((nil, nil)) + + revenueContextAndState = combineLatest( + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> distinctUntilChanged, + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId)) + |> distinctUntilChanged + ) + |> mapToSignal { peer, canViewRevenue -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in + var canViewRevenue = canViewRevenue + if let peer, case let .user(user) = peer, let _ = user.botInfo, context.sharedContext.applicationBindings.appBuildType == .internal || context.sharedContext.immediateExperimentalUISettings.devRequests { + canViewRevenue = true + } + #if DEBUG + canViewRevenue = peerId != context.account.peerId + #endif + guard canViewRevenue else { + return .single((nil, nil)) + } + let revenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId, ton: true) + return revenueStatsContext.state + |> map { state -> (StarsRevenueStatsContext?, StarsRevenueStats?) in + return (revenueStatsContext, state.stats) + } } - let revenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId, ton: true) - return revenueStatsContext.state - |> map { state -> (StarsRevenueStatsContext?, StarsRevenueStats?) in - return (revenueStatsContext, state.stats) + + webAppPermissions = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer, case let .user(user) = peer, let _ = user.botInfo { + return webAppPermissionsState(context: context, peerId: peerId) + } else { + return .single(nil) + } } + } else { + recommendedBots = .single(nil) + hasBotPreviewItems = .single(false) + starsRevenueContextAndState = .single((nil, nil)) + revenueContextAndState = .single((nil, nil)) + webAppPermissions = .single(nil) } - - let webAppPermissions: Signal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> mapToSignal { peer -> Signal in - if let peer, case let .user(user) = peer, let _ = user.botInfo { - return webAppPermissionsState(context: context, peerId: peerId) - } else { - return .single(nil) - } - } - + let savedMusicContext = ProfileSavedMusicContext(account: context.account, peerId: peerId) - + + let businessConnectedBot: Signal + if isMyProfile { + businessConnectedBot = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.BusinessConnectedBot(id: context.account.peerId) + ) + |> map { bot -> EnginePeer.Id? in + return bot?.id + } + |> distinctUntilChanged + |> mapToSignal { botPeerId -> Signal in + guard let botPeerId else { + return .single(nil) + } + return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: botPeerId)) + } + } else { + businessConnectedBot = .single(nil) + } + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: isMyProfile, chatLocationContextHolder: chatLocationContextHolder, sharedMediaFromForumTopic: sharedMediaFromForumTopic), @@ -1468,9 +1510,10 @@ func peerInfoScreenData( revenueContextAndState, premiumGiftOptions, webAppPermissions, - savedMusicContext.state + savedMusicContext.state, + businessConnectedBot ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, recommendedBots, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions, savedMusicState -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, recommendedBots, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, hasBotPreviewItems, personalChannel, privacySettings, starsRevenueContextAndState, revenueContextAndState, premiumGiftOptions, webAppPermissions, savedMusicState, businessConnectedBot -> PeerInfoScreenData in var availablePanes = availablePanes if isMyProfile { availablePanes?.insert(.stories, at: 0) @@ -1574,9 +1617,9 @@ func peerInfoScreenData( } return PeerInfoScreenData( - peer: peer, - chatPeer: peerView.peers[peerId], - savedMessagesPeer: savedMessagesPeer?._asPeer(), + peer: peer.flatMap(EnginePeer.init), + chatPeer: peerView.peers[peerId].flatMap(EnginePeer.init), + savedMessagesPeer: savedMessagesPeer, cachedData: peerView.cachedData, status: status, peerNotificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings, @@ -1616,7 +1659,8 @@ func peerInfoScreenData( webAppPermissions: webAppPermissions, savedMusicContext: savedMusicContext, savedMusicState: savedMusicState, - managedByBot: managedByBot + managedByBot: managedByBot, + businessConnectedBot: businessConnectedBot ) } case .channel: @@ -1789,14 +1833,14 @@ func peerInfoScreenData( } } - var discussionPeer: Peer? + var discussionPeer: EnginePeer? if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] { - discussionPeer = peer + discussionPeer = EnginePeer(peer) } - - var monoforumPeer: Peer? + + var monoforumPeer: EnginePeer? if let channel = peerViewMainPeer(peerView) as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.hasMonoforum), let linkedMonoforumId = channel.linkedMonoforumId { - monoforumPeer = peerView.peers[linkedMonoforumId] + monoforumPeer = peerView.peers[linkedMonoforumId].flatMap(EnginePeer.init) } var canManageInvitations = false @@ -1820,8 +1864,8 @@ func peerInfoScreenData( } return PeerInfoScreenData( - peer: peerView.peers[peerId], - chatPeer: peerView.peers[peerId], + peer: peerView.peers[peerId].flatMap(EnginePeer.init), + chatPeer: peerView.peers[peerId].flatMap(EnginePeer.init), savedMessagesPeer: nil, cachedData: peerView.cachedData, status: status, @@ -1862,7 +1906,8 @@ func peerInfoScreenData( webAppPermissions: nil, savedMusicContext: nil, savedMusicState: nil, - managedByBot: nil + managedByBot: nil, + businessConnectedBot: nil ) } case let .group(groupId): @@ -2074,7 +2119,7 @@ func peerInfoScreenData( requestsStatePromise.get(), hasStories, threadData, - context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)), accountIsPremium, hasSavedMessages, hasSavedMessagesChats, @@ -2083,14 +2128,14 @@ func peerInfoScreenData( starsRevenueContextAndState ) |> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, threadData, preferencesView, accountIsPremium, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState -> Signal in - var discussionPeer: Peer? + var discussionPeer: EnginePeer? if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] { - discussionPeer = peer + discussionPeer = EnginePeer(peer) } - - var monoforumPeer: Peer? + + var monoforumPeer: EnginePeer? if let channel = peerViewMainPeer(peerView) as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.hasMonoforum), let linkedMonoforumId = channel.linkedMonoforumId { - monoforumPeer = peerView.peers[linkedMonoforumId] + monoforumPeer = peerView.peers[linkedMonoforumId].flatMap(EnginePeer.init) } var availablePanes = availablePanes @@ -2154,11 +2199,11 @@ func peerInfoScreenData( let peerNotificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings let threadNotificationSettings = threadData?.notificationSettings - let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue + let appConfiguration: AppConfiguration = preferencesView?.get(AppConfiguration.self) ?? .defaultValue return .single(PeerInfoScreenData( - peer: peerView.peers[groupId], - chatPeer: peerView.peers[groupId], + peer: peerView.peers[groupId].flatMap(EnginePeer.init), + chatPeer: peerView.peers[groupId].flatMap(EnginePeer.init), savedMessagesPeer: nil, cachedData: peerView.cachedData, status: status, @@ -2199,7 +2244,8 @@ func peerInfoScreenData( webAppPermissions: nil, savedMusicContext: nil, savedMusicState: nil, - managedByBot: nil + managedByBot: nil, + businessConnectedBot: nil )) } } @@ -2216,13 +2262,13 @@ func peerInfoIsCopyProtected(data: PeerInfoScreenData) -> Bool { return isCopyProtected } -func canEditPeerInfo(context: AccountContext, peer: Peer?, chatLocation: ChatLocation, threadData: MessageHistoryThreadData?) -> Bool { +func canEditPeerInfo(context: AccountContext, peer: EnginePeer?, chatLocation: ChatLocation, threadData: MessageHistoryThreadData?) -> Bool { if context.account.peerId == peer?.id { return true } - if let user = peer as? TelegramUser, let botInfo = user.botInfo { + if case let .user(user) = peer, let botInfo = user.botInfo { return botInfo.flags.contains(.canEdit) - } else if let channel = peer as? TelegramChannel { + } else if case let .channel(channel) = peer { if let threadData = threadData { if chatLocation.threadId == 1 { return false @@ -2238,7 +2284,7 @@ func canEditPeerInfo(context: AccountContext, peer: Peer?, chatLocation: ChatLoc return true } } - } else if let group = peer as? TelegramGroup { + } else if case let .legacyGroup(group) = peer { switch group.role { case .admin, .creator: return true @@ -2265,23 +2311,23 @@ struct PeerInfoMemberActions: OptionSet { static let editRank = PeerInfoMemberActions(rawValue: 1 << 3) } -func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: PeerInfoMember) -> PeerInfoMemberActions { +func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: EnginePeer?, member: PeerInfoMember) -> PeerInfoMemberActions { var result: PeerInfoMemberActions = [] - + if peer == nil { result.insert(.logout) } else if member.id == accountPeerId { - if let channel = peer as? TelegramChannel { + if case let .channel(channel) = peer { if channel.hasPermission(.editRank) { result.insert(.editRank) } - } else if let group = peer as? TelegramGroup { + } else if case let .legacyGroup(group) = peer { if !group.hasBannedPermission(.banEditRank) { result.insert(.editRank) } } } else { - if let channel = peer as? TelegramChannel { + if case let .channel(channel) = peer { if channel.flags.contains(.isCreator) { if !channel.flags.contains(.isGigagroup) { result.insert(.restrict) @@ -2325,7 +2371,7 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: break } } - } else if let group = peer as? TelegramGroup { + } else if case let .legacyGroup(group) = peer { switch group.role { case .creator: result.insert(.restrict) @@ -2333,11 +2379,13 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: result.insert(.editRank) case .admin: switch member { - case let .legacyGroupMember(_, _, invitedBy, _, _, _): - result.insert(.restrict) - if invitedBy == accountPeerId { - result.insert(.promote) - result.insert(.editRank) + case let .legacyGroupMember(_, role, invitedBy, _, _, _): + if case .member = role { + result.insert(.restrict) + if invitedBy == accountPeerId { + result.insert(.promote) + result.insert(.editRank) + } } case .channelMember: break @@ -2346,8 +2394,8 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: } case .member: switch member { - case let .legacyGroupMember(_, _, invitedBy, _, _, _): - if invitedBy == accountPeerId { + case let .legacyGroupMember(_, role, invitedBy, _, _, _): + if case .member = role, invitedBy == accountPeerId { result.insert(.restrict) result.insert(.editRank) } @@ -2383,9 +2431,9 @@ func peerInfoHeaderButtonIsHiddenWhileExpanded(buttonKey: PeerInfoHeaderButtonKe return hiddenWhileExpanded } -func peerInfoHeaderActionButtons(peer: Peer?, isSecretChat: Bool, isContact: Bool) -> [PeerInfoHeaderButtonKey] { +func peerInfoHeaderActionButtons(peer: EnginePeer?, isSecretChat: Bool, isContact: Bool) -> [PeerInfoHeaderButtonKey] { var result: [PeerInfoHeaderButtonKey] = [] - if !isContact && !isSecretChat, let user = peer as? TelegramUser, user.botInfo == nil { + if !isContact && !isSecretChat, case let .user(user) = peer, user.botInfo == nil { result = [.message, .addContact] } @@ -2396,9 +2444,9 @@ func peerInfoHeaderActionButtons(peer: Peer?, isSecretChat: Bool, isContact: Boo return result } -func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFromChat: Bool, isExpanded: Bool, videoCallsEnabled: Bool, isSecretChat: Bool, isContact: Bool, threadInfo: EngineMessageHistoryThread.Info?) -> [PeerInfoHeaderButtonKey] { +func peerInfoHeaderButtons(peer: EnginePeer?, cachedData: CachedPeerData?, isOpenedFromChat: Bool, isExpanded: Bool, videoCallsEnabled: Bool, isSecretChat: Bool, isContact: Bool, threadInfo: EngineMessageHistoryThread.Info?) -> [PeerInfoHeaderButtonKey] { var result: [PeerInfoHeaderButtonKey] = [] - if let user = peer as? TelegramUser { + if case let .user(user) = peer { if !isOpenedFromChat { result.append(.message) } @@ -2432,7 +2480,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro } else { result.append(.more) } - } else if let channel = peer as? TelegramChannel { + } else if case let .channel(channel) = peer { if let _ = threadInfo { result.append(.mute) result.append(.search) @@ -2507,7 +2555,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro } } } - } else if let group = peer as? TelegramGroup { + } else if case let .legacyGroup(group) = peer { let hasVoiceChat = group.flags.contains(.hasVoiceChat) let canStartVoiceChat: Bool @@ -2534,8 +2582,8 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro return result } -func peerInfoCanEdit(peer: Peer?, chatLocation: ChatLocation, threadData: MessageHistoryThreadData?, cachedData: CachedPeerData?, isContact: Bool?) -> Bool { - if let user = peer as? TelegramUser { +func peerInfoCanEdit(peer: EnginePeer?, chatLocation: ChatLocation, threadData: MessageHistoryThreadData?, cachedData: CachedPeerData?, isContact: Bool?) -> Bool { + if case let .user(user) = peer { if user.isDeleted { return false } @@ -2546,7 +2594,7 @@ func peerInfoCanEdit(peer: Peer?, chatLocation: ChatLocation, threadData: Messag return false } return true - } else if let peer = peer as? TelegramChannel { + } else if case let .channel(peer) = peer { if peer.isForumOrMonoForum, let threadData = threadData { if peer.flags.contains(.isCreator) { return true @@ -2567,7 +2615,7 @@ func peerInfoCanEdit(peer: Peer?, chatLocation: ChatLocation, threadData: Messag } return false } - } else if let peer = peer as? TelegramGroup { + } else if case let .legacyGroup(peer) = peer { if case .creator = peer.role { return true } else if case let .admin(rights, _) = peer.role { @@ -2582,19 +2630,19 @@ func peerInfoCanEdit(peer: Peer?, chatLocation: ChatLocation, threadData: Messag return false } -func peerInfoIsChatMuted(peer: Peer?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?) -> Bool { - func isPeerMuted(peer: Peer?, peerNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?) -> Bool { +func peerInfoIsChatMuted(peer: EnginePeer?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?) -> Bool { + func isPeerMuted(peer: EnginePeer?, peerNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?) -> Bool { var peerIsMuted = false if let peerNotificationSettings { if case .muted = peerNotificationSettings.muteState { peerIsMuted = true } else if case .default = peerNotificationSettings.muteState, let globalNotificationSettings { if let peer { - if peer is TelegramUser { + if case .user = peer { peerIsMuted = !globalNotificationSettings.privateChats.enabled - } else if peer is TelegramGroup { + } else if case .legacyGroup = peer { peerIsMuted = !globalNotificationSettings.groupChats.enabled - } else if let channel = peer as? TelegramChannel { + } else if case let .channel(channel) = peer { switch channel.info { case .group: peerIsMuted = !globalNotificationSettings.groupChats.enabled diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift index 245df71b38..076c092cce 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift @@ -58,7 +58,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { } var removedPhotoResourceIds = Set() - func update(peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) { + func update(peer: EnginePeer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) { guard let peer = peer else { return } @@ -85,12 +85,12 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode { overrideImage = item == nil && canEdit ? .editAvatarIcon(forceNone: true) : nil } self.avatarNode.font = avatarPlaceholderFont(size: floor(avatarSize * 16.0 / 37.0)) - self.avatarNode.setPeer(context: self.context, theme: theme, peer: EnginePeer(peer), overrideImage: overrideImage, clipStyle: .none, synchronousLoad: false, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) + self.avatarNode.setPeer(context: self.context, theme: theme, peer: peer, overrideImage: overrideImage, clipStyle: .none, synchronousLoad: false, displayDimensions: CGSize(width: avatarSize, height: avatarSize)) self.avatarNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize)) var isForum = false let avatarCornerRadius: CGFloat - if let channel = peer as? TelegramChannel, channel.isForumOrMonoForum { + if case let .channel(channel) = peer, channel.isForumOrMonoForum { isForum = true avatarCornerRadius = floor(avatarSize * 0.25) } else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift index 91d9455b87..0f12a4b280 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift @@ -60,7 +60,7 @@ final class PeerInfoEditingAvatarOverlayNode: ASDisplayNode { transition.updateAlpha(node: self, alpha: 1.0 - fraction) } - func update(peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) { + func update(peer: EnginePeer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) { guard let peer = peer else { return } @@ -71,7 +71,7 @@ final class PeerInfoEditingAvatarOverlayNode: ASDisplayNode { let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear) let clipStyle: AvatarNodeClipStyle - if let channel = peer as? TelegramChannel, channel.isForumOrMonoForum { + if case let .channel(channel) = peer, channel.isForumOrMonoForum { clipStyle = .roundedRect } else { clipStyle = .round diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift index bc3b966244..dafef8e000 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift @@ -49,7 +49,7 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode { self.itemNodes[key]?.layer.addShakeAnimation() } - func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, peer: Peer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, cachedData: CachedPeerData?, isContact: Bool, isSettings: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { + func update(width: CGFloat, safeInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, peer: EnginePeer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, cachedData: CachedPeerData?, isContact: Bool, isSettings: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat { let avatarSize: CGFloat = isModalOverlay ? 200.0 : 100.0 let avatarFrame = CGRect(origin: CGPoint(x: floor((width - avatarSize) / 2.0), y: statusBarHeight + 22.0), size: CGSize(width: avatarSize, height: avatarSize)) transition.updateFrameAdditiveToCenter(node: self.avatarNode, frame: CGRect(origin: avatarFrame.center, size: CGSize())) @@ -70,11 +70,11 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode { } var isEditableBot = false - if let user = peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { isEditableBot = true } var fieldKeys: [PeerInfoHeaderTextFieldNodeKey] = [] - if let user = peer as? TelegramUser { + if case let .user(user) = peer { if !user.isDeleted { fieldKeys.append(.firstName) if isEditableBot { @@ -83,12 +83,12 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode { fieldKeys.append(.lastName) } } - } else if let _ = peer as? TelegramGroup { + } else if case .legacyGroup = peer { fieldKeys.append(.title) if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) { fieldKeys.append(.description) } - } else if let _ = peer as? TelegramChannel { + } else if case .channel = peer { fieldKeys.append(.title) if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) { fieldKeys.append(.description) @@ -104,15 +104,19 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode { var isMultiline = false switch key { case .firstName: - if let peer = peer as? TelegramUser { + if case let .user(user) = peer { if let editableBotInfo = (cachedData as? CachedUserData)?.editableBotInfo { updateText = editableBotInfo.name } else { - updateText = peer.firstName ?? "" + updateText = user.firstName ?? "" } } case .lastName: - updateText = (peer as? TelegramUser)?.lastName ?? "" + if case let .user(user) = peer { + updateText = user.lastName ?? "" + } else { + updateText = "" + } case .title: updateText = peer?.debugDisplayTitle ?? "" case .description: @@ -151,7 +155,7 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode { placeholder = presentationData.strings.UserInfo_LastNamePlaceholder isEnabled = isContact || isSettings case .title: - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { placeholder = presentationData.strings.GroupInfo_ChannelListNamePlaceholder } else { placeholder = presentationData.strings.GroupInfo_GroupNamePlaceholder diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift index c392a2381d..e767664904 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift @@ -89,7 +89,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { private weak var controller: PeerInfoScreenImpl? private var presentationData: PresentationData? private var state: PeerInfoState? - private var peer: Peer? + private var peer: EnginePeer? private var threadData: MessageHistoryThreadData? private var isSearching: Bool = false private var avatarSize: CGFloat? @@ -493,7 +493,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { private var currentStatusIcon: CredibilityIcon? private var currentPanelStatusData: PeerInfoStatusData? - func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, threadData: MessageHistoryThreadData?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, profileGiftsContext: ProfileGiftsContext?, screenData: PeerInfoScreenData?, isSearching: Bool, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition, additive: Bool, animateHeader: Bool) -> CGFloat { + func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: EnginePeer?, cachedData: CachedPeerData?, threadData: MessageHistoryThreadData?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, profileGiftsContext: ProfileGiftsContext?, screenData: PeerInfoScreenData?, isSearching: Bool, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition, additive: Bool, animateHeader: Bool) -> CGFloat { if self.appliedCustomNavigationContentNode !== self.customNavigationContentNode { if let previous = self.appliedCustomNavigationContentNode { ComponentTransition(transition).setAlpha(view: previous.view, alpha: 0.0, completion: { [weak previous] _ in @@ -521,7 +521,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.peer = peer self.threadData = threadData self.isSearching = isSearching - self.avatarListNode.listContainerNode.peer = peer.flatMap(EnginePeer.init) + self.avatarListNode.listContainerNode.peer = peer let isFirstTime = self.validLayout == nil self.validLayout = (width, statusBarHeight, deviceMetrics) @@ -568,7 +568,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } hasBackground = true } else if let peer { - backgroundCoverSubject = .peer(EnginePeer(peer)) + backgroundCoverSubject = .peer(peer) if peer.effectiveProfileColor != nil { hasBackground = true } @@ -619,7 +619,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } var isForum = false - if let channel = peer as? TelegramChannel, channel.isForumOrMonoForum { + if case let .channel(channel) = peer, channel.isForumOrMonoForum { isForum = true } @@ -1215,14 +1215,14 @@ final class PeerInfoHeaderNode: ASDisplayNode { } else if let threadData = threadData { title = threadData.info.title } else { - title = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + title = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) } title = title.replacingOccurrences(of: "\u{1160}", with: "").replacingOccurrences(of: "\u{3164}", with: "") if title.replacingOccurrences(of: "\u{fe0e}", with: "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { title = "" //"\u{00A0}" } if title.isEmpty { - if let peer = peer as? TelegramUser, let phone = peer.phone { + if case let .user(user) = peer, let phone = user.phone { title = formatPhoneNumber(context: self.context, number: phone) } else if let addressName = peer.addressName { title = "@\(addressName)" @@ -1235,7 +1235,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { titleAttributes = MultiScaleTextState.Attributes(font: Font.medium(28.0), color: .white) smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.medium(28.0), color: .white, shadowColor: titleShadowColor) - if self.isSettings, let user = peer as? TelegramUser { + if self.isSettings, case let .user(user) = peer { var subtitle = formatPhoneNumber(context: self.context, number: user.phone ?? "") if let mainUsername = user.addressName, !mainUsername.isEmpty { @@ -1267,11 +1267,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { panelSubtitleString = (panelStatusData.text, MultiScaleTextState.Attributes(font: Font.regular(17.0), color: subtitleColor)) } } else if let _ = threadData { - let subtitleColor: UIColor - subtitleColor = UIColor.white + let subtitleColor: UIColor = .white let statusText: String - if let user = peer as? TelegramUser, user.isForum { + if case let .user(user) = peer, user.isForum { statusText = " " } else { statusText = peer.debugDisplayTitle @@ -1351,7 +1350,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { let textSideInset: CGFloat = 36.0 let expandedAvatarHeight: CGFloat = expandedAvatarListSize.height - let titleConstrainedSize = CGSize(width: width - textSideInset * 2.0 - (isPremium || isVerified || isFake ? 20.0 : 0.0), height: .greatestFiniteMagnitude) + var titleConstrainedSize = CGSize(width: width - textSideInset * 2.0 - (isPremium || isVerified || isFake ? 20.0 : 0.0), height: .greatestFiniteMagnitude) + if self.navigationButtonContainer.rightButtonNodes.count > 1 { + titleConstrainedSize.width -= 60.0 + } let titleNodeLayout = self.titleNode.updateLayout(text: titleStringText, states: [ TitleNodeStateRegular: MultiScaleTextState(attributes: titleAttributes, constrainedSize: titleConstrainedSize), @@ -1364,6 +1366,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { ], mainState: TitleNodeStateRegular) self.subtitleNode.accessibilityLabel = subtitleStringText + var subtitleButtonHorizontalOffset: CGFloat = 0.0 if subtitleIsButton { let subtitleBackgroundNode: ASDisplayNode if let current = self.subtitleBackgroundNode { @@ -1400,20 +1403,21 @@ final class PeerInfoHeaderNode: ASDisplayNode { let subtitleArrowNode: ASImageNode if let current = self.subtitleArrowNode { subtitleArrowNode = current - if themeUpdated { - subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: .white)?.withRenderingMode(.alwaysTemplate) - } } else { subtitleArrowNode = ASImageNode() self.subtitleArrowNode = subtitleArrowNode self.subtitleNode.insertSubnode(subtitleArrowNode, at: 1) - - subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: .white)?.withRenderingMode(.alwaysTemplate) } - subtitleBackgroundNode.backgroundColor = .white.withMultipliedAlpha(0.1) + if subtitleArrowNode.image == nil || themeUpdated { + subtitleArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Item List/DisclosureArrow"), color: presentationData.theme.list.itemSecondaryTextColor) + } + self.subtitleNode.updateTintColor(color: presentationData.theme.list.itemSecondaryTextColor, transition: navigationTransition) + + transition.updateBackgroundColor(node: subtitleBackgroundNode, color: contentButtonBackgroundColor) let subtitleSize = subtitleNodeLayout[TitleNodeStateRegular]!.size - var subtitleBackgroundFrame = CGRect(origin: CGPoint(), size: subtitleSize).offsetBy(dx: -subtitleSize.width * 0.5, dy: -subtitleSize.height * 0.5).insetBy(dx: -6.0, dy: -4.0) + var subtitleBackgroundFrame = CGRect(origin: CGPoint(), size: subtitleSize).offsetBy(dx: -subtitleSize.width * 0.5, dy: -subtitleSize.height * 0.5).insetBy(dx: -8.0, dy: -4.0) subtitleBackgroundFrame.size.width += 12.0 + subtitleButtonHorizontalOffset = subtitleBackgroundFrame.midX transition.updateFrame(node: subtitleBackgroundNode, frame: subtitleBackgroundFrame) transition.updateCornerRadius(node: subtitleBackgroundNode, cornerRadius: subtitleBackgroundFrame.height * 0.5) @@ -1620,7 +1624,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { titleOffset = -min(titleCollapseOffset, contentOffset) titleCollapseFraction = max(0.0, min(1.0, contentOffset / titleCollapseOffset)) - subtitleFrame = CGRect(origin: CGPoint(x: 16.0, y: minTitleFrame.maxY + 2.0), size: subtitleSize) + subtitleFrame = CGRect(origin: CGPoint(x: 16.0 - subtitleButtonHorizontalOffset * (1.0 - titleCollapseFraction), y: minTitleFrame.maxY + 2.0), size: subtitleSize) if self.subtitleRating != nil { subtitleFrame.origin.x += 22.0 } @@ -1642,10 +1646,10 @@ final class PeerInfoHeaderNode: ASDisplayNode { let totalSubtitleWidth = effectiveSubtitleWidth + usernameSpacing + usernameSize.width if usernameSize.width == 0.0 { - subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - effectiveSubtitleWidth) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize) + subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - effectiveSubtitleWidth) / 2.0) - subtitleButtonHorizontalOffset * (1.0 - titleCollapseFraction), y: titleFrame.maxY + 1.0), size: subtitleSize) usernameFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - usernameSize.width) / 2.0), y: subtitleFrame.maxY + 1.0), size: usernameSize) } else { - subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - totalSubtitleWidth) / 2.0), y: titleFrame.maxY + 1.0), size: subtitleSize) + subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - totalSubtitleWidth) / 2.0) - subtitleButtonHorizontalOffset * (1.0 - titleCollapseFraction), y: titleFrame.maxY + 1.0), size: subtitleSize) usernameFrame = CGRect(origin: CGPoint(x: subtitleFrame.maxX + usernameSpacing, y: titleFrame.maxY + 1.0), size: usernameSize) } } @@ -1960,7 +1964,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.avatarListNode.containerNode.view.mask = nil } - self.avatarListNode.listContainerNode.update(size: expandedAvatarListSize, peer: peer.flatMap(EnginePeer.init), isExpanded: self.isAvatarExpanded, transition: transition) + self.avatarListNode.listContainerNode.update(size: expandedAvatarListSize, peer: peer, isExpanded: self.isAvatarExpanded, transition: transition) if self.avatarListNode.listContainerNode.isCollapsing && !self.ignoreCollapse { self.avatarListNode.avatarContainerNode.canAttachVideo = false } @@ -2050,7 +2054,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { } self.controller?.push(ProfileLevelInfoScreen( context: self.context, - peer: EnginePeer(peer), + peer: peer, starRating: currentStarRating, pendingStarRating: self.currentPendingStarRating, customTheme: self.presentationData?.theme @@ -2350,7 +2354,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { buttonText = presentationData.strings.PeerInfo_ButtonVideoCall buttonIcon = .videoCall case .voiceChat: - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { buttonText = presentationData.strings.PeerInfo_ButtonLiveStream } else { buttonText = presentationData.strings.PeerInfo_ButtonVoiceChat @@ -2859,4 +2863,3 @@ final class PeerInfoHeaderNode: ASDisplayNode { transition.updateAnchorPoint(layer: self.avatarListNode.maskNode.layer, anchorPoint: maskAnchorPoint) } } - diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoInteraction.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoInteraction.swift index 0d0bdf5c72..1798db6f7b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoInteraction.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoInteraction.swift @@ -23,6 +23,7 @@ final class PeerInfoInteraction { let openAddContact: () -> Void let updateBlocked: (Bool) -> Void let openReport: (PeerInfoReportType) -> Void + let openDeleteReaction: (MessageId) -> Void let openShareBot: () -> Void let openAddBotToGroup: () -> Void let performBotCommand: (PeerInfoBotCommand) -> Void @@ -42,7 +43,7 @@ final class PeerInfoInteraction { let openPermissions: () -> Void let openLocation: () -> Void let editingOpenSetupLocation: () -> Void - let openPeerInfo: (Peer, Bool) -> Void + let openPeerInfo: (EnginePeer, Bool) -> Void let performMemberAction: (PeerInfoMember, PeerInfoMemberAction) -> Void let openPeerInfoContextMenu: (PeerInfoContextSubject, ASDisplayNode, CGRect?) -> Void let performBioLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void @@ -83,6 +84,7 @@ final class PeerInfoInteraction { let editingOpenVerifyAccounts: () -> Void let editingToggleAutoTranslate: (Bool) -> Void let displayAutoTranslateLocked: () -> Void + let editingOpenBusinessChatBots: () -> Void let getController: () -> ViewController? init( @@ -100,6 +102,7 @@ final class PeerInfoInteraction { openAddContact: @escaping () -> Void, updateBlocked: @escaping (Bool) -> Void, openReport: @escaping (PeerInfoReportType) -> Void, + openDeleteReaction: @escaping (MessageId) -> Void, openShareBot: @escaping () -> Void, openAddBotToGroup: @escaping () -> Void, performBotCommand: @escaping (PeerInfoBotCommand) -> Void, @@ -119,7 +122,7 @@ final class PeerInfoInteraction { openPermissions: @escaping () -> Void, openLocation: @escaping () -> Void, editingOpenSetupLocation: @escaping () -> Void, - openPeerInfo: @escaping (Peer, Bool) -> Void, + openPeerInfo: @escaping (EnginePeer, Bool) -> Void, performMemberAction: @escaping (PeerInfoMember, PeerInfoMemberAction) -> Void, openPeerInfoContextMenu: @escaping (PeerInfoContextSubject, ASDisplayNode, CGRect?) -> Void, performBioLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, @@ -160,6 +163,7 @@ final class PeerInfoInteraction { editingOpenVerifyAccounts: @escaping () -> Void, editingToggleAutoTranslate: @escaping (Bool) -> Void, displayAutoTranslateLocked: @escaping () -> Void, + editingOpenBusinessChatBots: @escaping () -> Void, getController: @escaping () -> ViewController? ) { self.openUsername = openUsername @@ -176,6 +180,7 @@ final class PeerInfoInteraction { self.openAddContact = openAddContact self.updateBlocked = updateBlocked self.openReport = openReport + self.openDeleteReaction = openDeleteReaction self.openShareBot = openShareBot self.openAddBotToGroup = openAddBotToGroup self.performBotCommand = performBotCommand @@ -236,6 +241,7 @@ final class PeerInfoInteraction { self.editingOpenVerifyAccounts = editingOpenVerifyAccounts self.editingToggleAutoTranslate = editingToggleAutoTranslate self.displayAutoTranslateLocked = displayAutoTranslateLocked + self.editingOpenBusinessChatBots = editingOpenBusinessChatBots self.getController = getController } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift index 5146ee4244..1e504b10fb 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift @@ -27,14 +27,14 @@ enum PeerInfoMember: Equatable { } } - var peer: Peer { + var peer: EnginePeer { switch self { case let .channelMember(participant, _): return participant.peer case let .legacyGroupMember(peer, _, _, _, _, _): - return peer.peers[peer.peerId]! + return EnginePeer(peer.peers[peer.peerId]!) case let .account(peer): - return peer.peers[peer.peerId]! + return EnginePeer(peer.peers[peer.peerId]!) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift index 385fd4dbc7..6496e23c80 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoPaneContainerNode.swift @@ -408,7 +408,7 @@ private final class PeerInfoPendingPane { updatedPresentationData: (initial: PresentationData, signal: Signal)?, chatControllerInteraction: ChatControllerInteraction, data: PeerInfoScreenData, - openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void, + openPeerContextAction: @escaping (Bool, EnginePeer, ASDisplayNode, ContextGesture?) -> Void, openAddMemberAction: @escaping () -> Void, requestPerformPeerMemberAction: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, peerId: PeerId, @@ -462,7 +462,7 @@ private final class PeerInfoPendingPane { if let cachedUserData = data.cachedData as? CachedUserData, cachedUserData.disallowedGifts == .All { canGift = false } - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { if channel.hasPermission(.sendSomething) { canManage = true } @@ -476,7 +476,7 @@ private final class PeerInfoPendingPane { if let peer = data.peer { if peer.id == context.account.peerId { canManage = true - } else if let channel = peer as? TelegramChannel { + } else if case let .channel(channel) = peer { if channel.hasPermission(.editStories) { canManage = true } @@ -493,7 +493,7 @@ private final class PeerInfoPendingPane { scope = .botPreview(id: peerId) if let peer = data.peer { - if let user = peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { canManage = true } } @@ -637,7 +637,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat var selectionPanelNode: PeerInfoSelectionPanelNode? var chatControllerInteraction: ChatControllerInteraction? - var openPeerContextAction: ((Bool, Peer, ASDisplayNode, ContextGesture?) -> Void)? + var openPeerContextAction: ((Bool, EnginePeer, ASDisplayNode, ContextGesture?) -> Void)? var openAddMemberAction: (() -> Void)? var requestPerformPeerMemberAction: ((PeerInfoMember, PeerMembersListAction) -> Void)? @@ -1249,7 +1249,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat if let peer = data?.peer { if peer.id == self.context.account.peerId { canManageTabs = true - } else if let channel = data?.peer as? TelegramChannel, case .broadcast = channel.info { + } else if case let .channel(channel) = data?.peer, case .broadcast = channel.info { if channel.hasPermission(.changeInfo) { canManageTabs = true } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift index d24b90a11c..be77e08247 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift @@ -36,7 +36,18 @@ enum InfoSection: Int, CaseIterable { case botAffiliateProgram } -func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], chatLocation: ChatLocation, isOpenedFromChat: Bool, isMyProfile: Bool) -> [(AnyHashable, [PeerInfoScreenItem])] { +func infoItems( + data: PeerInfoScreenData?, + context: AccountContext, + presentationData: PresentationData, + interaction: PeerInfoInteraction, + reactionSourceMessageId: MessageId?, + canDeleteReaction: Bool, + callMessages: [Message], + chatLocation: ChatLocation, + isOpenedFromChat: Bool, + isMyProfile: Bool +) -> [(AnyHashable, [PeerInfoScreenItem])] { guard let data = data else { return [] } @@ -67,7 +78,7 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD interaction.openBirthdayContextMenu(node, gesture) } - if let user = data.peer as? TelegramUser { + if case let .user(user) = data.peer { let ItemCallList = 1000 let ItemPersonalChannelHeader = 2000 let ItemPersonalChannel = 2001 @@ -81,9 +92,9 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD let ItemAffiliateInfo = 4001 let ItemBusinessHours = 5000 let ItemLocation = 5001 - let ItemSendMessage = 6000 - let ItemReport = 6001 - let ItemAddToContacts = 6002 + let ItemAddToContacts = 6000 + let ItemDeleteReaction = 6001 + let ItemReport = 6002 let ItemBlock = 6003 let ItemEncryptionKey = 6004 let ItemBalanceHeader = 7000 @@ -359,31 +370,22 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD } if !isMyProfile { - if let reactionSourceMessageId = reactionSourceMessageId, !data.isContact { - items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemSendMessage, text: presentationData.strings.UserInfo_SendMessage, action: { - interaction.openChat(nil) + if !data.isContact, user.botInfo == nil { + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemAddToContacts, text: presentationData.strings.PeerInfo_AddToContacts, action: { + interaction.openAddContact() })) - + } + + if let reactionSourceMessageId = reactionSourceMessageId { + if canDeleteReaction { + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemDeleteReaction, text: presentationData.strings.PeerInfo_DeleteReaction, color: .destructive, action: { + interaction.openDeleteReaction(reactionSourceMessageId) + })) + } items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemReport, text: presentationData.strings.ReportPeer_BanAndReport, color: .destructive, action: { interaction.openReport(.reaction(reactionSourceMessageId)) })) - } else if let _ = nearbyPeerDistance { - items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemSendMessage, text: presentationData.strings.UserInfo_SendMessage, action: { - interaction.openChat(nil) - })) - - items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemReport, text: presentationData.strings.ReportPeer_Report, color: .destructive, action: { - interaction.openReport(.user) - })) } else { - if !data.isContact { - if user.botInfo == nil { - items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemAddToContacts, text: presentationData.strings.PeerInfo_AddToContacts, action: { - interaction.openAddContact() - })) - } - } - var isBlocked = false if let cachedData = data.cachedData as? CachedUserData, cachedData.isBlocked { isBlocked = true @@ -519,7 +521,7 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD if let managedByBot = data.managedByBot { items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: ItemBotAddToChatInfo, icon: .managedBot, text: presentationData.strings.PeerInfo_ManagedBotFooter(managedByBot.compactDisplayTitle).string, linkAction: { _ in - interaction.openPeerInfo(managedByBot._asPeer(), false) + interaction.openPeerInfo(managedByBot, false) })) } else { items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: ItemBotAddToChatInfo, text: presentationData.strings.Bot_AddToChatInfo)) @@ -527,7 +529,7 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD } } } - } else if let channel = data.peer as? TelegramChannel { + } else if case let .channel(channel) = data.peer { let ItemUsername = 1 let ItemUsernameInfo = 2 let ItemAbout = 3 @@ -793,7 +795,7 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD } } } - } else if let group = data.peer as? TelegramGroup { + } else if case let .legacyGroup(group) = data.peer { if let cachedData = data.cachedData as? CachedGroupData { let aboutText: String? if group.isFake { @@ -818,7 +820,7 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD if let peer = data.peer, let members = data.members, case let .shortList(_, memberList) = members { var canAddMembers = false - if let group = data.peer as? TelegramGroup { + if case let .legacyGroup(group) = data.peer { switch group.role { case .admin, .creator: canAddMembers = true @@ -828,7 +830,7 @@ func infoItems(data: PeerInfoScreenData?, context: AccountContext, presentationD if !group.hasBannedPermission(.banAddMembers) { canAddMembers = true } - } else if let channel = data.peer as? TelegramChannel { + } else if case let .channel(channel) = data.peer { switch channel.info { case .broadcast: break @@ -901,7 +903,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s } if let data = data { - if let user = data.peer as? TelegramUser { + if case let .user(user) = data.peer { let ItemNote: AnyHashable = AnyHashable("note_edit") let ItemNoteInfo = 1 @@ -1045,7 +1047,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s interaction.requestDeleteContact() })) } - } else if let channel = data.peer as? TelegramChannel { + } else if case let .channel(channel) = data.peer { switch channel.info { case .broadcast: let ItemUsername = 1 @@ -1097,7 +1099,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s if let addressName = peer.addressName, !addressName.isEmpty { discussionGroupTitle = "@\(addressName)" } else { - discussionGroupTitle = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + discussionGroupTitle = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) } } else { discussionGroupTitle = presentationData.strings.Channel_DiscussionGroupAdd @@ -1192,7 +1194,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) { let labelString: NSAttributedString if channel.linkedMonoforumId != nil { - if let monoforumPeer = data.linkedMonoforumPeer as? TelegramChannel { + if case let .channel(monoforumPeer) = data.linkedMonoforumPeer { if let sendPaidMessageStars = monoforumPeer.sendPaidMessageStars { let formattedLabel = formatStarsAmountText(sendPaidMessageStars, dateTimeFormat: presentationData.dateTimeFormat) let smallLabelFont = Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 13.0)) @@ -1404,7 +1406,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s if let addressName = linkedDiscussionPeer.addressName, !addressName.isEmpty { peerTitle = "@\(addressName)" } else { - peerTitle = EnginePeer(linkedDiscussionPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + peerTitle = linkedDiscussionPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) } items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemLinkedChannel, label: .text(peerTitle), text: presentationData.strings.Group_LinkedChannel, icon: PresentationResourcesSettings.channels, action: { @@ -1589,7 +1591,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s } } } - } else if let group = data.peer as? TelegramGroup { + } else if case let .legacyGroup(group) = data.peer { let ItemUsername = 101 let ItemInviteLinks = 102 let ItemPreHistory = 103 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 256f17800c..6c61401182 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 @@ -282,8 +281,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro personalChannels: nil ) var forceIsContactPromise = ValuePromise(false) - let nearbyPeerDistance: Int32? let reactionSourceMessageId: MessageId? + let sourceMessageId: MessageId? + var canDeleteReaction = false var dataDisposable: Disposable? let activeActionDisposable = MetaDisposable() @@ -348,15 +348,38 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private var didSetReady = false - init(controller: PeerInfoScreenImpl, context: AccountContext, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool, isMyProfile: Bool, hintGroupInCommon: PeerId?, requestsContext: PeerInvitationImportersContext?, profileGiftsContext: ProfileGiftsContext?, starsContext: StarsContext?, tonContext: StarsContext?, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, switchToGiftsTarget: PeerInfoSwitchToGiftsTarget?, switchToStoryFolder: Int64?, switchToMediaTarget: PeerInfoSwitchToMediaTarget?, initialPaneKey: PeerInfoPaneKey?, sharedMediaFromForumTopic: (EnginePeer.Id, Int64)?) { + init( + controller: PeerInfoScreenImpl, + context: AccountContext, + peerId: PeerId, + avatarInitiallyExpanded: Bool, + isOpenedFromChat: Bool, + reactionSourceMessageId: MessageId?, + sourceMessageId: MessageId?, + callMessages: [Message], + isSettings: Bool, + isMyProfile: Bool, + hintGroupInCommon: PeerId?, + requestsContext: PeerInvitationImportersContext?, + profileGiftsContext: ProfileGiftsContext?, + starsContext: StarsContext?, + tonContext: StarsContext?, + chatLocation: ChatLocation, + chatLocationContextHolder: Atomic, + switchToGiftsTarget: PeerInfoSwitchToGiftsTarget?, + switchToStoryFolder: Int64?, + switchToMediaTarget: PeerInfoSwitchToMediaTarget?, + initialPaneKey: PeerInfoPaneKey?, + sharedMediaFromForumTopic: (EnginePeer.Id, Int64)? + ) { self.controller = controller self.context = context self.peerId = peerId self.isOpenedFromChat = isOpenedFromChat self.videoCallsEnabled = true self.presentationData = controller.presentationData - self.nearbyPeerDistance = nearbyPeerDistance self.reactionSourceMessageId = reactionSourceMessageId + self.sourceMessageId = sourceMessageId self.callMessages = callMessages self.isSettings = isSettings self.isMyProfile = isMyProfile @@ -438,6 +461,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro openReport: { [weak self] type in self?.openReport(type: type, contextController: nil, backAction: nil) }, + openDeleteReaction: { [weak self] messageId in + self?.openDeleteReaction(messageId: messageId) + }, openShareBot: { [weak self] in self?.openShareBot() }, @@ -683,6 +709,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } self.displayAutoTranslateLocked() + }, editingOpenBusinessChatBots: { [weak self] in + guard let self else { + return + } + self.editingOpenBusinessChatBots() }, getController: { [weak self] in return self?.controller @@ -733,14 +764,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { c, _ in c?.dismiss(completion: { if let strongSelf = self, let currentPeer = strongSelf.data?.peer, let navigationController = strongSelf.controller?.navigationController as? NavigationController { - if let channel = currentPeer as? TelegramChannel, channel.isForumOrMonoForum, let threadId = message.threadId { + if case let .channel(channel) = currentPeer, channel.isForumOrMonoForum, let threadId = message.threadId { let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: currentPeer.id, threadId: threadId, messageId: message.id, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .default, animated: true).startStandalone() } else { let targetLocation: NavigateToChatControllerParams.Location if case let .replyThread(message) = strongSelf.chatLocation { targetLocation = .replyThread(message) } else { - targetLocation = .peer(EnginePeer(currentPeer)) + targetLocation = .peer(currentPeer) } let currentPeerId = strongSelf.peerId @@ -900,14 +931,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { c, f in c?.dismiss(completion: { if let strongSelf = self, let currentPeer = strongSelf.data?.peer, let navigationController = strongSelf.controller?.navigationController as? NavigationController { - if let channel = currentPeer as? TelegramChannel, channel.isForumOrMonoForum, let threadId = message.threadId { + if case let .channel(channel) = currentPeer, channel.isForumOrMonoForum, let threadId = message.threadId { let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: currentPeer.id, threadId: threadId, messageId: message.id, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .default, animated: true).startStandalone() } else { let targetLocation: NavigateToChatControllerParams.Location if case let .replyThread(message) = strongSelf.chatLocation { targetLocation = .replyThread(message) } else { - targetLocation = .peer(EnginePeer(currentPeer)) + targetLocation = .peer(currentPeer) } let currentPeerId = strongSelf.peerId @@ -1240,7 +1271,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.openPremiumGift() }, openUniqueGift: { _ in }, openMessageFeeException: { - }, requestMessageUpdate: { _, _ in + }, requestMessageUpdate: { _, _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { }, scrollToMessageId: { _ in @@ -1253,7 +1284,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, requestToggleTodoMessageItem: { _, _, _ in }, displayTodoToggleUnavailable: { _ in }, openStarsPurchase: { _ in - }, openRankInfo: { _, _, _ in }, openSetPeerAvatar: {}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, + }, openRankInfo: { _, _, _ in + }, openSetPeerAvatar: { + }, displayPollRestrictedToast: { _ in + }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.hiddenMediaDisposable = context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in guard let strongSelf = self else { @@ -1301,7 +1335,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro items = [ .action(ContextMenuActionItem(text: presentationData.strings.Conversation_LinkDialogOpen, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ImageEnlarge"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) - self?.chatInterfaceInteraction.openPeer(EnginePeer(peer), .default, nil, .default) + self?.chatInterfaceInteraction.openPeer(peer, .default, nil, .default) })), .action(ContextMenuActionItem(text: presentationData.strings.Chat_SimilarChannels_Join, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] _, f in f(.dismissWithoutContent) @@ -1309,14 +1343,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self else { return } - self.joinChannel(peer: EnginePeer(peer)) + self.joinChannel(peer: peer) })) ] } else { items = [ .action(ContextMenuActionItem(text: presentationData.strings.Conversation_LinkDialogOpen, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ImageEnlarge"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.dismissWithoutContent) - self?.chatInterfaceInteraction.openPeer(EnginePeer(peer), .default, nil, .default) + self?.chatInterfaceInteraction.openPeer(peer, .default, nil, .default) })) ] } @@ -1531,7 +1565,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } - strongSelf.openAvatarGallery(peer: EnginePeer(peer), entries: entries, centralEntry: centralEntry, animateTransition: true) + strongSelf.openAvatarGallery(peer: peer, entries: entries, centralEntry: centralEntry, animateTransition: true) Queue.mainQueue().after(0.4) { strongSelf.resetHeaderExpansion() @@ -1629,7 +1663,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel, nil, nil) return } - if let peer = data.peer as? TelegramUser { + if case let .user(peer) = data.peer { if strongSelf.isSettings || strongSelf.isMyProfile, let cachedData = data.cachedData as? CachedUserData { let firstName = strongSelf.headerNode.editingContentNode.editingTextForKey(.firstName) ?? "" let lastName = strongSelf.headerNode.editingContentNode.editingTextForKey(.lastName) ?? "" @@ -1875,7 +1909,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } else { strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel, nil, nil) } - } else if let group = data.peer as? TelegramGroup, canEditPeerInfo(context: strongSelf.context, peer: group, chatLocation: chatLocation, threadData: data.threadData) { + } else if case let .legacyGroup(group) = data.peer, canEditPeerInfo(context: strongSelf.context, peer: data.peer, chatLocation: chatLocation, threadData: data.threadData) { let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? "" let description = strongSelf.headerNode.editingContentNode.editingTextForKey(.description) ?? "" @@ -1931,7 +1965,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel, nil, nil) })) } - } else if let channel = data.peer as? TelegramChannel, canEditPeerInfo(context: strongSelf.context, peer: channel, chatLocation: strongSelf.chatLocation, threadData: data.threadData) { + } else if case let .channel(channel) = data.peer, canEditPeerInfo(context: strongSelf.context, peer: data.peer, chatLocation: strongSelf.chatLocation, threadData: data.threadData) { let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? "" let description = strongSelf.headerNode.editingContentNode.editingTextForKey(.description) ?? "" @@ -2102,7 +2136,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.headerNode.displayCopyContextMenu = { [weak self] node, copyPhone, copyUsername in - guard let strongSelf = self, let data = strongSelf.data, let user = data.peer as? TelegramUser else { + guard let strongSelf = self, let data = strongSelf.data, case let .user(user) = data.peer else { return } var actions: [ContextMenuAction] = [] @@ -2318,12 +2352,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.actionSheet.primaryTextColor) }, action: { [weak self] c, f in if let strongSelf = self, let parent = strongSelf.controller { - presentPeerReportOptions(context: context, parent: parent, contextController: c, subject: .profilePhoto(peer.id, 0), completion: { _, _ in }) + presentPeerReportOptions(context: context, parent: parent, contextController: c, subject: .profilePhoto(peer.id, 0, sourceMessageId: strongSelf.sourceMessageId), completion: { _, _ in }) } })) ] - let galleryController = AvatarGalleryController(context: strongSelf.context, peer: EnginePeer(peer), remoteEntries: nil, replaceRootController: { controller, ready in + let galleryController = AvatarGalleryController(context: strongSelf.context, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in }, synchronousLoad: true) galleryController.setHintWillBePresentedInPreviewingContext(true) @@ -2375,9 +2409,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self, let navigationController = self.controller?.navigationController as? NavigationController, let peer = self.data?.peer else { return } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)))) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer))) } - + if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(peerId.namespace) { self.displayAsPeersPromise.set(context.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: peerId)) } @@ -2458,14 +2492,37 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self?.updateNavigation(transition: .immediate, additive: true, animateHeader: true) } + let reactionSourceMessage: Signal + if let reactionSourceMessageId = self.reactionSourceMessageId { + reactionSourceMessage = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.Message(id: reactionSourceMessageId)) + } else { + reactionSourceMessage = .single(nil) + } + self.dataDisposable = combineLatest( queue: Queue.mainQueue(), screenData, - self.forceIsContactPromise.get() - ).startStrict(next: { [weak self] data, forceIsContact in + self.forceIsContactPromise.get(), + reactionSourceMessage + ).startStrict(next: { [weak self] data, forceIsContact, reactionSourceMessage in guard let strongSelf = self else { return } + + var canDeleteReaction = false + if let sourceMessage = reactionSourceMessage?._asMessage(), let reactionPeerId = data.peer?.id, let channel = sourceMessage.peers[sourceMessage.id.peerId] as? TelegramChannel, channel.hasPermission(.deleteAllMessages) { + for attribute in sourceMessage.attributes { + guard let attribute = attribute as? ReactionsMessageAttribute else { + continue + } + if attribute.recentPeers.contains(where: { $0.peerId == reactionPeerId }) || attribute.topPeers.contains(where: { $0.peerId == reactionPeerId }) { + canDeleteReaction = true + break + } + } + } + strongSelf.canDeleteReaction = canDeleteReaction + if data.isContact && forceIsContact { strongSelf.forceIsContactPromise.set(false) } else { @@ -2475,12 +2532,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.cachedDataPromise.set(.single(data.cachedData)) }) - if let _ = nearbyPeerDistance { - self.preloadHistoryDisposable.set(self.context.account.addAdditionalPreloadHistoryPeerId(peerId: peerId)) - - self.context.prefetchManager?.prepareNextGreetingSticker() - } - self.customStatusDisposable = (self.customStatusPromise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in guard let strongSelf = self else { @@ -2550,7 +2601,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if self.headerNode.avatarListNode.avatarContainerNode.storyProgress != mappedValue { self.headerNode.avatarListNode.avatarContainerNode.storyProgress = mappedValue - self.headerNode.avatarListNode.avatarContainerNode.updateStoryView(transition: .immediate, theme: self.presentationData.theme, peer: peer?._asPeer()) + self.headerNode.avatarListNode.avatarContainerNode.updateStoryView(transition: .immediate, theme: self.presentationData.theme, peer: peer) } }) self.expiringStoryListDisposable = (combineLatest(queue: .mainQueue(), @@ -2696,7 +2747,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } - if let channel = data.peer as? TelegramChannel, channel.isForumOrMonoForum, self.chatLocation.threadId == nil { + if case let .channel(channel) = data.peer, channel.isForumOrMonoForum, self.chatLocation.threadId == nil { if self.forumTopicNotificationExceptionsDisposable == nil { self.forumTopicNotificationExceptionsDisposable = (self.context.engine.peers.forumChannelTopicNotificationExceptions(id: channel.id) |> deliverOnMainQueue).startStrict(next: { [weak self] list in @@ -2745,10 +2796,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var previousPhotoIsPersonal: Bool? var currentPhotoIsPersonal: Bool? - if let previousUser = previousData?.peer as? TelegramUser { + if case let .user(previousUser) = previousData?.peer { previousPhotoIsPersonal = previousUser.profileImageRepresentations.first?.isPersonal == true } - if let user = data.peer as? TelegramUser { + if case let .user(user) = data.peer { currentPhotoIsPersonal = user.profileImageRepresentations.first?.isPersonal == true } @@ -3035,7 +3086,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } func openAutoremove(currentValue: Int32?) { - let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, style: .default, mode: .autoremove, currentTime: currentValue, dismissByTapOutside: true, completion: { [weak self] value in + let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, style: .default, mode: .autoremove, currentTime: currentValue, completion: { [weak self] value in guard let strongSelf = self else { return } @@ -3064,7 +3115,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } func openCustomMute() { - let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, style: .default, mode: .mute, currentTime: nil, dismissByTapOutside: true, completion: { [weak self] value in + let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, style: .default, mode: .mute, currentTime: nil, completion: { [weak self] value in guard let strongSelf = self, let peer = strongSelf.data?.peer else { return } @@ -3189,7 +3240,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }) } - func openClearHistory(contextController: ContextControllerProtocol, clearPeerHistory: ClearPeerHistory, peer: Peer, chatPeer: Peer) { + func openClearHistory(contextController: ContextControllerProtocol, clearPeerHistory: ClearPeerHistory, peer: Peer, chatPeer: EnginePeer) { var subItems: [ContextMenuItem] = [] subItems.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Common_Back, icon: { theme in @@ -3206,9 +3257,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let title: String switch anyType { case .user, .secretChat, .savedMessages: - title = self.presentationData.strings.PeerInfo_ClearConfirmationUser(EnginePeer(chatPeer).compactDisplayTitle).string + title = self.presentationData.strings.PeerInfo_ClearConfirmationUser(chatPeer.compactDisplayTitle).string case .group, .channel: - title = self.presentationData.strings.PeerInfo_ClearConfirmationGroup(EnginePeer(chatPeer).compactDisplayTitle).string + title = self.presentationData.strings.PeerInfo_ClearConfirmationGroup(chatPeer.compactDisplayTitle).string } subItems.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in @@ -3227,7 +3278,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let text: String switch canClearForEveryone { case .user, .secretChat, .savedMessages: - text = self.presentationData.strings.Conversation_DeleteMessagesFor(EnginePeer(chatPeer).compactDisplayTitle).string + text = self.presentationData.strings.Conversation_DeleteMessagesFor(chatPeer.compactDisplayTitle).string case .channel, .group: text = self.presentationData.strings.Conversation_DeleteMessagesForEveryone } @@ -3245,7 +3296,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let text: String switch canClearForMyself { case .secretChat: - text = self.presentationData.strings.Conversation_DeleteMessagesFor(EnginePeer(chatPeer).compactDisplayTitle).string + text = self.presentationData.strings.Conversation_DeleteMessagesFor(chatPeer.compactDisplayTitle).string default: text = self.presentationData.strings.Conversation_DeleteMessagesForMe } @@ -3385,7 +3436,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro |> deliverOnMainQueue).startStrict(completed: { [weak self] in if let strongSelf = self, let peer = strongSelf.data?.peer { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_DeletedFromContacts(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }) + let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_DeletedFromContacts(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }) controller.keepOnParentDismissal = true strongSelf.controller?.present(controller, in: .window(.root)) @@ -3431,7 +3482,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let presentationData = strongSelf.presentationData if case let .user(peer) = peer, let _ = peer.botInfo { - strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: block).startStrict()) + strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: block, sourceMessageId: strongSelf.sourceMessageId).startStrict()) if !block { let _ = enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: "/start", attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).startStandalone() if let navigationController = strongSelf.controller?.navigationController as? NavigationController { @@ -3456,12 +3507,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } - strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: true).startStrict()) + strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: true, sourceMessageId: strongSelf.sourceMessageId).startStrict()) if deleteChat { let _ = strongSelf.context.engine.peers.removePeerChat(peerId: strongSelf.peerId, reportChatSpam: reportSpam).startStandalone() (strongSelf.controller?.navigationController as? NavigationController)?.popToRoot(animated: true) } else if reportSpam { - let _ = strongSelf.context.engine.peers.reportPeer(peerId: strongSelf.peerId, reason: .spam, message: "").startStandalone() + let _ = strongSelf.context.engine.peers.reportPeer(peerId: strongSelf.peerId, reason: .spam, message: "", sourceMessageId: strongSelf.sourceMessageId).startStandalone() } deleteSendMessageIntents(peerId: strongSelf.peerId) @@ -3482,7 +3533,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let strongSelf = self else { return } - strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: block).startStrict()) + strongSelf.activeActionDisposable.set(strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.id, isBlocked: block, sourceMessageId: strongSelf.sourceMessageId).startStrict()) })]), in: .window(.root)) } } @@ -3500,7 +3551,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.view.endEditing(true) let statsController: ViewController - if let channel = peer as? TelegramChannel, case .group = channel.info { + if case let .channel(channel) = peer, case .group = channel.info { if case .monetization = section { statsController = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, section: section, existingStarsRevenueContext: data.starsRevenueStatsContext, boostStatus: boostStatus) } else { @@ -3513,7 +3564,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } func openBoost() { - guard let peer = self.data?.peer, let channel = peer as? TelegramChannel, let controller = self.controller else { + guard case let .channel(channel) = self.data?.peer, let controller = self.controller else { return } @@ -3563,7 +3614,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let self else { return } - self.activeActionDisposable.set(self.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: self.peerId, isBlocked: true).startStrict()) + self.activeActionDisposable.set(self.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: self.peerId, isBlocked: true, sourceMessageId: sourceMessageId).startStrict()) self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .emoji(name: "PoliceCar", text: self.presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) }), ActionSheetButtonItem(title: presentationData.strings.ReportPeer_ReportReaction_Report, action: { [weak self] in @@ -3586,7 +3637,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro default: contextController?.dismiss() - self.context.sharedContext.makeContentReportScreen(context: self.context, subject: .peer(self.peerId), forceDark: false, present: { [weak self] controller in + let reportSubject = ReportContentSubject.peer(self.peerId, sourceMessageId: self.sourceMessageId) + self.context.sharedContext.makeContentReportScreen(context: self.context, subject: reportSubject, forceDark: false, present: { [weak self] controller in self?.controller?.push(controller) }, completion: { }, requestSelectMessages: { [weak self] title, option, message in @@ -3599,12 +3651,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let data = self.data, let peer = data.peer, let encryptionKeyFingerprint = data.encryptionKeyFingerprint else { return } - self.controller?.push(SecretChatKeyController(context: self.context, fingerprint: encryptionKeyFingerprint, peer: EnginePeer(peer))) + self.controller?.push(SecretChatKeyController(context: self.context, fingerprint: encryptionKeyFingerprint, peer: peer)) } 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 +3673,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 +3697,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 +3714,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)) } @@ -3700,7 +3750,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } func performBotCommand(command: PeerInfoBotCommand) { - let _ = (self.context.account.postbox.loadedPeerWithId(peerId) + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in guard let strongSelf = self else { return @@ -3717,7 +3774,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let _ = enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).startStandalone() if let peer = strongSelf.data?.peer, let navigationController = strongSelf.controller?.navigationController as? NavigationController { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)))) + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer))) } }) } @@ -3741,7 +3798,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private func editingOpenPublicLinkSetup() { - if let peer = self.data?.peer as? TelegramUser, peer.botInfo != nil { + if case let .user(peer) = self.data?.peer, peer.botInfo != nil { let controller = usernameSetupController(context: self.context, mode: .bot(self.peerId)) self.controller?.push(controller) } else { @@ -3761,7 +3818,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private func editingOpenAffiliateProgram() { - if let peer = self.data?.peer as? TelegramUser, let botInfo = peer.botInfo { + if case let .user(peer) = self.data?.peer, let botInfo = peer.botInfo { if botInfo.flags.contains(.canEdit) { let _ = (self.context.sharedContext.makeAffiliateProgramSetupScreenInitialData(context: self.context, peerId: peer.id, mode: .editProgram) |> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in @@ -3951,7 +4008,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } if self.peerId == self.context.account.peerId { controller.push(UserAppearanceScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData)) - } else if let peer = self.data?.peer, peer is TelegramChannel { + } else if case .channel = self.data?.peer { controller.push(ChannelAppearanceScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, boostStatus: self.boostStatus)) } } @@ -3989,6 +4046,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro })) }) } + + private func editingOpenBusinessChatBots() { + let _ = (self.context.sharedContext.makeChatbotSetupScreenInitialData(context: self.context) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] initialData in + guard let self else { + return + } + self.controller?.push(self.context.sharedContext.makeChatbotSetupScreen(context: self.context, initialData: initialData)) + }) + } private func editingOpenInviteLinksSetup() { self.controller?.push(inviteLinkListController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, admin: nil)) @@ -4038,7 +4106,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let data = self.data, let peer = data.peer else { return } - if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + if case let .channel(channel) = peer, case .broadcast = channel.info { let subscription = Promise() subscription.set(PeerAllowedReactionsScreen.content(context: self.context, peerId: peer.id)) let _ = (subscription.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] content in @@ -4092,7 +4160,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let data = self.data, let peer = data.peer else { return } - if peer is TelegramGroup { + if case .legacyGroup = peer { if isEnabled { let context = self.context let signal: Signal = self.context.engine.peers.convertGroupToSupergroup(peerId: self.peerId, additionalProcessing: { upgradedPeerId -> Signal in @@ -4151,9 +4219,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .members: self.controller?.push(channelMembersController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId)) case .admins: - if peer is TelegramGroup { + if case .legacyGroup = peer { self.controller?.push(channelAdminsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId)) - } else if peer is TelegramChannel { + } else if case .channel = peer { self.controller?.push(channelAdminsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId)) } case .banned: @@ -4215,7 +4283,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let context = self.context let presentationData = self.presentationData - let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, venue: MapVenue(title: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), address: location.address, provider: nil, id: nil, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, venue: MapVenue(title: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), address: location.address, provider: nil, id: nil, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) let controllerParams = LocationViewParams(sendLiveLocation: { _ in }, stopLiveLocation: { _ in @@ -4224,7 +4292,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, openPeer: { _ in }, showAll: false) - let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peer, text: "", attributes: [], media: [map], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peer._asPeer(), text: "", attributes: [], media: [map], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let controller = LocationViewController(context: context, updatedPresentationData: self.controller?.updatedPresentationData, subject: EngineMessage(message), params: controllerParams) self.controller?.push(controller) @@ -4263,11 +4331,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.controller?.push(controller) } - private func openPeerInfo(peer: Peer, isMember: Bool) { - var mode: PeerInfoControllerMode = .generic - if isMember { - mode = .group(self.peerId) - } + private func openPeerInfo(peer: EnginePeer, isMember: Bool) { + let mode: PeerInfoControllerMode = .generic if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: mode, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { (self.controller?.navigationController as? NavigationController)?.pushViewController(infoController) } @@ -4464,7 +4529,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro ActionSheetTextItem(title: text), ActionSheetButtonItem(title: title, color: .destructive, action: { dismissAction() - self?.deletePeerChat(peer: peer._asPeer(), globally: true) + self?.deletePeerChat(peer: peer, globally: true) }), ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) @@ -4518,32 +4583,32 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.controller?.present(textAlertController(context: strongSelf.context, title: title, text: text, actions: [ TextAlertAction(type: .destructiveAction, title: actionText, action: { - self?.deletePeerChat(peer: peer._asPeer(), globally: delete) + self?.deletePeerChat(peer: peer, globally: delete) }), - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }) ], parseMarkdown: true), in: .window(.root)) }) } - private func deletePeerChat(peer: Peer, globally: Bool) { + private func deletePeerChat(peer: EnginePeer, globally: Bool) { guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { return } - guard let tabController = navigationController.viewControllers.first as? TabBarController else { + guard let navigationTarget = resolveChatListNavigationTarget(navigationController: navigationController, excluding: controller) else { return } - for childController in tabController.controllers { - if let chatListController = childController as? ChatListController { - chatListController.maybeAskForPeerChatRemoval(peer: EngineRenderedPeer(peer: EnginePeer(peer)), joined: false, deleteGloballyIfPossible: globally, completion: { [weak navigationController] deleted in - if deleted { - navigationController?.popToRoot(animated: true) - } - }, removed: { - }) - break + navigationTarget.chatListController.maybeAskForPeerChatRemoval(peer: EngineRenderedPeer(peer: peer), joined: false, deleteGloballyIfPossible: globally, completion: { [weak navigationController] deleted in + guard deleted, let navigationController = navigationController else { + return } - } + if let popToController = navigationTarget.popToController { + let _ = navigationController.popToViewController(popToController, animated: true) + } else { + navigationController.popToRoot(animated: true) + } + }, removed: { + }) } func deleteProfilePhoto(_ item: PeerInfoAvatarListItem) { @@ -4672,7 +4737,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } self.postingAvailabilityDisposable?.dispose() - if let data = self.data, let user = data.peer as? TelegramUser, let botInfo = user.botInfo { + if let data = self.data, case let .user(user) = data.peer, let botInfo = user.botInfo { if !botInfo.flags.contains(.canEdit) { return } @@ -4791,7 +4856,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return nil } - if let data = self.data, let user = data.peer as? TelegramUser, let _ = user.botInfo { + if let data = self.data, case let .user(user) = data.peer, let _ = user.botInfo { if let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode, let transitionView = pane.extractPendingStoryTransitionView() { return StoryCameraTransitionOut( destinationView: transitionView, @@ -4949,7 +5014,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 } @@ -5164,7 +5229,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro navigationController: navigationController, chatController: nil, context: strongSelf.context, - chatLocation: .peer(EnginePeer(peer)), + chatLocation: .peer(peer), subject: .message(id: .id(index.id), highlight: nil, timecode: nil, setupReply: false), botStart: nil, updateTextInputState: nil, @@ -5174,7 +5239,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro scrollToEndIfExists: false, activateMessageSearch: nil, peekData: nil, - peerNearbyData: nil, reportReason: nil, animated: true, options: [], @@ -5261,7 +5325,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let peer = self.data?.peer else { return } - let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.UserInfo_ResetToOriginalAlertText(EnginePeer(peer).compactDisplayTitle).string, actions: [ + let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.UserInfo_ResetToOriginalAlertText(peer.compactDisplayTitle).string, actions: [ TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { }), @@ -5270,7 +5334,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } strongSelf.updateAvatarDisposable.set((strongSelf.context.engine.contacts.updateContactPhoto(peerId: strongSelf.peerId, resource: nil, videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in - mapResourceToAvatarSizes(postbox: strongSelf.context.account.postbox, resource: resource, representations: representations) + mapResourceToAvatarSizes(engine: strongSelf.context.engine, resource: resource, representations: representations) }) |> deliverOnMainQueue).startStrict(next: { [weak self] _ in guard let strongSelf = self else { @@ -5380,7 +5444,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro insets.left += sectionInset insets.right += sectionInset - let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, chatLocation: self.chatLocation, isOpenedFromChat: self.isOpenedFromChat, isMyProfile: self.isMyProfile) + let items = self.isSettings ? settingsItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isExpanded: self.headerNode.isAvatarExpanded) : infoItems(data: self.data, context: self.context, presentationData: self.presentationData, interaction: self.interaction, reactionSourceMessageId: self.reactionSourceMessageId, canDeleteReaction: self.canDeleteReaction, callMessages: self.callMessages, chatLocation: self.chatLocation, isOpenedFromChat: self.isOpenedFromChat, isMyProfile: self.isMyProfile) contentHeight += headerHeight if !((self.isSettings || self.isMyProfile) && self.state.isEditing) { @@ -5552,9 +5616,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)) } @@ -5601,13 +5665,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let type: PeerType if isBot { type = .bot - } else if let user = peer as? TelegramUser { + } else if case let .user(user) = peer { if user.botInfo != nil && !user.id.isVerificationCodes { type = .bot } else { type = .user } - } else if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + } else if case let .channel(channel) = peer, case .broadcast = channel.info { type = .channel } else { type = .group @@ -5823,7 +5887,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } else if peerInfoCanEdit(peer: self.data?.peer, chatLocation: self.chatLocation, threadData: self.data?.threadData, cachedData: self.data?.cachedData, isContact: self.data?.isContact) { rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false)) } - if let data = self.data, !data.isPremiumRequiredForStoryPosting || data.accountIsPremium, let channel = data.peer as? TelegramChannel, channel.hasPermission(.postStories) { + if let data = self.data, !data.isPremiumRequiredForStoryPosting || data.accountIsPremium, case let .channel(channel) = data.peer, channel.hasPermission(.postStories) { rightNavigationButtons.insert(PeerInfoHeaderNavigationButtonSpec(key: .postStory, isForExpandedView: false), at: 0) } else if self.isMyProfile { rightNavigationButtons.insert(PeerInfoHeaderNavigationButtonSpec(key: .postStory, isForExpandedView: false), at: 0) @@ -5850,7 +5914,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro case .media: rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .more, isForExpandedView: true)) case .botPreview: - if let data = self.data, data.hasBotPreviewItems, let user = data.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + if let data = self.data, data.hasBotPreviewItems, case let .user(user) = data.peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .more, isForExpandedView: true)) } case .stories: @@ -6292,8 +6356,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc public let peerId: PeerId private let avatarInitiallyExpanded: Bool private let isOpenedFromChat: Bool - private let nearbyPeerDistance: Int32? private let reactionSourceMessageId: MessageId? + private let sourceMessageId: MessageId? private let callMessages: [Message] let isSettings: Bool let isMyProfile: Bool @@ -6380,8 +6444,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, - nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, + sourceMessageId: MessageId? = nil, callMessages: [Message], isSettings: Bool = false, isMyProfile: Bool = false, @@ -6401,8 +6465,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.peerId = peerId self.avatarInitiallyExpanded = avatarInitiallyExpanded self.isOpenedFromChat = isOpenedFromChat - self.nearbyPeerDistance = nearbyPeerDistance self.reactionSourceMessageId = reactionSourceMessageId + self.sourceMessageId = sourceMessageId self.callMessages = callMessages self.isSettings = isSettings self.isMyProfile = isMyProfile @@ -6460,7 +6524,10 @@ 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, + accentDisabledButtonColor: baseNavigationBarPresentationData.theme.accentDisabledButtonColor, + accentForegroundColor: baseNavigationBarPresentationData.theme.accentForegroundColor ), strings: baseNavigationBarPresentationData.strings)) self._hasGlassStyle = true @@ -6527,7 +6594,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc if let primary = primary { let size = CGSize(width: 31.0, height: 31.0) let inset: CGFloat = 3.0 - if let signal = peerAvatarImage(account: primary.0, peerReference: PeerReference(primary.1._asPeer()), authorOfMessage: nil, representation: primary.1.profileImageRepresentations.first, displayDimensions: size, inset: 3.0, emptyColor: nil, synchronousLoad: false) { + if let signal = peerAvatarImage(account: primary.0, peerReference: PeerReference(primary.1), authorOfMessage: nil, representation: primary.1.profileImageRepresentations.first, displayDimensions: size, inset: 3.0, emptyColor: nil, synchronousLoad: false) { return signal |> map { imageVersions -> (UIImage, UIImage)? in if let image = imageVersions?.0 { @@ -6736,6 +6803,19 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc }) self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate) + + if let sourceMessageId { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard case let .user(user) = peer else { + return + } + if case .personal = user.accessHash { + } else { + let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peerId, sourceMessageId: sourceMessageId).startStandalone() + } + }) + } } required init(coder aDecoder: NSCoder) { @@ -6767,7 +6847,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc initialPaneKey = .files } } - self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, profileGiftsContext: self.profileGiftsContext, starsContext: self.starsContext, tonContext: self.tonContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, switchToGiftsTarget: self.switchToGiftsTarget, switchToStoryFolder: self.switchToStoryFolder, switchToMediaTarget: self.switchToMediaTarget, initialPaneKey: initialPaneKey, sharedMediaFromForumTopic: self.sharedMediaFromForumTopic) + self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, reactionSourceMessageId: self.reactionSourceMessageId, sourceMessageId: self.sourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, profileGiftsContext: self.profileGiftsContext, starsContext: self.starsContext, tonContext: self.tonContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, switchToGiftsTarget: self.switchToGiftsTarget, switchToStoryFolder: self.switchToStoryFolder, switchToMediaTarget: self.switchToMediaTarget, initialPaneKey: initialPaneKey, sharedMediaFromForumTopic: self.sharedMediaFromForumTopic) self.controllerNode.accountsAndPeers.set(self.accountsAndPeers.get() |> map { $0.1 }) self.controllerNode.activeSessionsContextAndCount.set(self.activeSessionsContextAndCount.get()) self.cachedDataPromise.set(self.controllerNode.cachedDataPromise.get()) @@ -6875,7 +6955,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: subject, keepStack: .always, peekData: peekData)) case .info: if peer.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) == nil { - if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { navigationController.pushViewController(infoController) } } @@ -7178,7 +7258,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc }) { let _ = navigationController.popToViewController(infoController, animated: false) } else { - if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { navigationController.replaceController(sourceController, with: infoController, animated: false) } } @@ -7376,16 +7456,16 @@ struct ClearPeerHistory { var canClearForMyself: ClearType? = nil var canClearForEveryone: ClearType? = nil - init(context: AccountContext, peer: Peer, chatPeer: Peer, cachedData: CachedPeerData?) { + init(context: AccountContext, peer: Peer, chatPeer: EnginePeer, cachedData: CachedPeerData?) { if peer.id == context.account.peerId { canClearCache = false canClearForMyself = .savedMessages canClearForEveryone = nil - } else if chatPeer is TelegramSecretChat { + } else if case .secretChat = chatPeer { canClearCache = false canClearForMyself = .secretChat canClearForEveryone = nil - } else if let group = chatPeer as? TelegramGroup { + } else if case let .legacyGroup(group) = chatPeer { canClearCache = false switch group.role { @@ -7396,7 +7476,7 @@ struct ClearPeerHistory { canClearForMyself = .group canClearForEveryone = nil } - } else if let channel = chatPeer as? TelegramChannel { + } else if case let .channel(channel) = chatPeer { var canDeleteHistory = false if let cachedData = cachedData as? CachedChannelData { canDeleteHistory = cachedData.flags.contains(.canDeleteHistory) @@ -7424,7 +7504,7 @@ struct ClearPeerHistory { canClearCache = false canClearForMyself = .user - if let user = chatPeer as? TelegramUser, user.botInfo != nil { + if case let .user(user) = chatPeer, user.botInfo != nil { canClearForEveryone = nil } else { canClearForEveryone = .user diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift index 0c83494846..5ed112284f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift @@ -25,7 +25,7 @@ private func sharedSetupProfilePhotoUpload(context: AccountContext, image: UIIma } let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: mode == .custom) let _ = representation @@ -245,11 +245,10 @@ public extension PeerInfoScreenImpl { return } - let postbox = context.account.postbox let signal: Signal - - signal = context.engine.peers.updatePeerPhoto(peerId: peer.id, photo: context.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + + signal = context.engine.peers.updatePeerPhoto(peerId: peer.id, photo: context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(resource)), mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) var dismissStatus: (() -> Void)? @@ -409,8 +408,8 @@ public extension PeerInfoScreenImpl { let updateAvatarDisposable = MetaDisposable() updateAvatarDisposable.set((videoResource |> mapToSignal { videoResource -> Signal in - return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: $0) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(photoResource)), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: EngineMediaResource($0)) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) } |> deliverOnMainQueue).startStandalone(next: { result in @@ -420,7 +419,7 @@ public extension PeerInfoScreenImpl { case let .progress(value): uploadStatus?.set(.single(.progress(value))) } - + if case .complete = result { dismissStatus?() } @@ -440,7 +439,7 @@ extension PeerInfoScreenImpl { let peerId = self.peerId var isForum = false - if let peer = peer as? TelegramChannel, peer.isForumOrMonoForum { + if case let .channel(peer) = peer, peer.isForumOrMonoForum { isForum = true } @@ -666,7 +665,11 @@ extension PeerInfoScreenImpl { } if mainController is ActionSheetController { - self.present(mainController, in: .window(.root)) + if let navigationController = self.navigationController, let topController = navigationController.topViewController as? ViewController { + topController.present(mainController, in: .window(.root)) + } else { + self.present(mainController, in: .window(.root)) + } } else { mainController.navigationPresentation = .flatModal mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) @@ -699,11 +702,10 @@ extension PeerInfoScreenImpl { } } } - let postbox = strongSelf.context.account.postbox let signal: Signal if case .custom = mode { signal = strongSelf.context.engine.contacts.updateContactPhoto(peerId: strongSelf.peerId, resource: nil, videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + return mapResourceToAvatarSizes(engine: strongSelf.context.engine, resource: resource, representations: representations) }) } else if case .fallback = mode { signal = strongSelf.context.engine.accountData.removeFallbackPhoto(reference: nil) @@ -713,7 +715,7 @@ extension PeerInfoScreenImpl { } } else { signal = strongSelf.context.engine.peers.updatePeerPhoto(peerId: strongSelf.peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + return mapResourceToAvatarSizes(engine: strongSelf.context.engine, resource: resource, representations: representations) }) } strongSelf.controllerNode.updateAvatarDisposable.set((signal @@ -766,7 +768,7 @@ extension PeerInfoScreenImpl { self.controllerNode.scrollNode.view.setContentOffset(CGPoint(), animated: false) let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) + self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: mode == .custom) if [.suggest, .fallback].contains(mode) { @@ -794,29 +796,28 @@ extension PeerInfoScreenImpl { return } - let postbox = self.context.account.postbox let signal: Signal if self.isSettings || self.isMyProfile { if case .fallback = mode { - signal = self.context.engine.accountData.updateFallbackPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + signal = self.context.engine.accountData.updateFallbackPhoto(resource: EngineMediaResource(resource), videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: self.context.engine, resource: resource, representations: representations) }) } else { - signal = self.context.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + signal = self.context.engine.accountData.updateAccountPhoto(resource: EngineMediaResource(resource), videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: self.context.engine, resource: resource, representations: representations) }) } } else if case .custom = mode { - signal = self.context.engine.contacts.updateContactPhoto(peerId: self.peerId, resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + signal = self.context.engine.contacts.updateContactPhoto(peerId: self.peerId, resource: EngineMediaResource(resource), videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .custom, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: self.context.engine, resource: resource, representations: representations) }) } else if case .suggest = mode { - signal = self.context.engine.contacts.updateContactPhoto(peerId: self.peerId, resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + signal = self.context.engine.contacts.updateContactPhoto(peerId: self.peerId, resource: EngineMediaResource(resource), videoResource: nil, videoStartTimestamp: nil, markup: nil, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: self.context.engine, resource: resource, representations: representations) }) } else { - signal = self.context.engine.peers.updatePeerPhoto(peerId: self.peerId, photo: self.context.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations) + signal = self.context.engine.peers.updatePeerPhoto(peerId: self.peerId, photo: self.context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(resource)), mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: self.context.engine, resource: resource, representations: representations) }) } @@ -1020,25 +1021,25 @@ extension PeerInfoScreenImpl { |> mapToSignal { videoResource -> Signal in if isSettings || isMyProfile { if case .fallback = mode { - return context.engine.accountData.updateFallbackPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + return context.engine.accountData.updateFallbackPhoto(resource: EngineMediaResource(photoResource), videoResource: videoResource.flatMap(EngineMediaResource.init), videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) } else { - return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + return context.engine.accountData.updateAccountPhoto(resource: EngineMediaResource(photoResource), videoResource: videoResource.flatMap(EngineMediaResource.init), videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) } } else if case .custom = mode { - return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .custom, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: EngineMediaResource(photoResource), videoResource: videoResource.flatMap(EngineMediaResource.init), videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .custom, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) } else if case .suggest = mode { - return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: EngineMediaResource(photoResource), videoResource: videoResource.flatMap(EngineMediaResource.init), videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) } else { - return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: $0) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(photoResource)), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: EngineMediaResource($0)) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) } } @@ -1234,25 +1235,25 @@ extension PeerInfoScreenImpl { |> mapToSignal { videoResource -> Signal in if isSettings || isMyProfile { if case .fallback = mode { - return context.engine.accountData.updateFallbackPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + return context.engine.accountData.updateFallbackPhoto(resource: EngineMediaResource(photoResource), videoResource: videoResource.flatMap(EngineMediaResource.init), videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) } else { - return context.engine.accountData.updateAccountPhoto(resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + return context.engine.accountData.updateAccountPhoto(resource: EngineMediaResource(photoResource), videoResource: videoResource.flatMap(EngineMediaResource.init), videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) } } else if case .custom = mode { - return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .custom, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: EngineMediaResource(photoResource), videoResource: videoResource.flatMap(EngineMediaResource.init), videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .custom, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) } else if case .suggest = mode { - return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: photoResource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + return context.engine.contacts.updateContactPhoto(peerId: peerId, resource: EngineMediaResource(photoResource), videoResource: videoResource.flatMap(EngineMediaResource.init), videoStartTimestamp: videoStartTimestamp, markup: markup, mode: .suggest, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) } else { - return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: photoResource), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: $0) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: account.postbox, resource: resource, representations: representations) + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(photoResource)), video: videoResource.flatMap { context.engine.peers.uploadedPeerVideo(resource: EngineMediaResource($0)) |> map(Optional.init) }, videoStartTimestamp: videoStartTimestamp, markup: markup, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift index f27cded7ca..0740909da9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenCallActions.swift @@ -45,7 +45,7 @@ extension PeerInfoScreenNode { return } - guard let peer = self.data?.peer as? TelegramUser, let cachedUserData = self.data?.cachedData as? CachedUserData else { + guard case let .user(peer) = self.data?.peer, let cachedUserData = self.data?.cachedData as? CachedUserData else { return } if cachedUserData.callsPrivate { @@ -128,7 +128,7 @@ extension PeerInfoScreenNode { case .generic, .scheduledTooLate: text = strongSelf.presentationData.strings.Login_UnknownError case .anonymousNotAllowed: - if let channel = strongSelf.data?.peer as? TelegramChannel, case .broadcast = channel.info { + if case let .channel(channel) = strongSelf.data?.peer, case .broadcast = channel.info { text = strongSelf.presentationData.strings.LiveStream_AnonymousDisabledAlertText } else { text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText @@ -144,7 +144,14 @@ extension PeerInfoScreenNode { func openVoiceChatDisplayAsPeerSelection(completion: @escaping (PeerId) -> Void, gesture: ContextGesture? = nil, contextController: ContextControllerProtocol? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextControllerProtocol) -> Void)? = nil) { let dismissOnSelection = contextController == nil - let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(context.account.peerId) + let currentAccountPeer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> map { peer in return [FoundPeer(peer: peer, subscribers: nil)] } @@ -165,10 +172,10 @@ extension PeerInfoScreenNode { var isGroup = false for peer in peers { - if peer.peer is TelegramGroup { + if case .legacyGroup = peer.peer { isGroup = true break - } else if let peer = peer.peer as? TelegramChannel, case .group = peer.info { + } else if case let .channel(channel) = peer.peer, case .group = channel.info { isGroup = true break } @@ -183,7 +190,7 @@ extension PeerInfoScreenNode { if peer.peer.id.namespace == Namespaces.Peer.CloudUser { subtitle = strongSelf.presentationData.strings.VoiceChat_PersonalAccount } else if let subscribers = peer.subscribers { - if let peer = peer.peer as? TelegramChannel, case .broadcast = peer.info { + if case let .channel(channel) = peer.peer, case .broadcast = channel.info { subtitle = strongSelf.presentationData.strings.Conversation_StatusSubscribers(subscribers) } else { subtitle = strongSelf.presentationData.strings.Conversation_StatusMembers(subscribers) @@ -191,8 +198,8 @@ extension PeerInfoScreenNode { } let avatarSize = CGSize(width: 28.0, height: 28.0) - let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: EnginePeer(peer.peer), size: avatarSize) - items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { _, f in + let avatarSignal = peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize) + items.append(.action(ContextMenuActionItem(text: peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), action: { _, f in if dismissOnSelection { f(.dismissWithoutContent) } @@ -246,7 +253,14 @@ extension PeerInfoScreenNode { let context = self.context let peerId = self.peerId let defaultJoinAsPeerId = defaultJoinAsPeerId ?? self.context.account.peerId - let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) + let currentAccountPeer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> map { peer in return [FoundPeer(peer: peer, subscribers: nil)] } @@ -271,7 +285,7 @@ extension PeerInfoScreenNode { } if let peer = selectedPeer { let avatarSize = CGSize(width: 28.0, height: 28.0) - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { c, f in + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(peer.peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: strongSelf.context.account, peer: peer.peer, size: avatarSize)), action: { c, f in guard let strongSelf = self else { return } @@ -290,7 +304,7 @@ extension PeerInfoScreenNode { let createVoiceChatTitle: String let scheduleVoiceChatTitle: String - if let channel = strongSelf.data?.peer as? TelegramChannel, case .broadcast = channel.info { + if case let .channel(channel) = strongSelf.data?.peer, case .broadcast = channel.info { createVoiceChatTitle = strongSelf.presentationData.strings.ChannelInfo_CreateLiveStream scheduleVoiceChatTitle = strongSelf.presentationData.strings.ChannelInfo_ScheduleLiveStream } else { @@ -313,11 +327,11 @@ extension PeerInfoScreenNode { var credentialsPromise: Promise? var canCreateStream = false switch chatPeer { - case let group as TelegramGroup: + case let .legacyGroup(group): if case .creator = group.role { canCreateStream = true } - case let channel as TelegramChannel: + case let .channel(channel): if channel.hasPermission(.manageCalls) { canCreateStream = true credentialsPromise = Promise() diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayGiftsContextMenu.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayGiftsContextMenu.swift index b43ac185cc..70a5757803 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayGiftsContextMenu.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayGiftsContextMenu.swift @@ -26,7 +26,7 @@ extension PeerInfoScreenNode { let giftsContext = pane.giftsContext var hasVisibility = false - if let channel = data.peer as? TelegramChannel, channel.hasPermission(.sendSomething) { + if case let .channel(channel) = data.peer, channel.hasPermission(.sendSomething) { hasVisibility = true } else if data.peer?.id == self.context.account.peerId { hasVisibility = true diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayMediaGalleryContextMenu.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayMediaGalleryContextMenu.swift index c4a0ff8b21..0c98038837 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayMediaGalleryContextMenu.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenDisplayMediaGalleryContextMenu.swift @@ -30,7 +30,7 @@ extension PeerInfoScreenNode { } if case .botPreview = pane.scope { - guard let data = self.data, let user = data.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) else { + guard let data = self.data, case let .user(user) = data.peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) else { return } @@ -141,7 +141,7 @@ extension PeerInfoScreenNode { self.mediaGalleryContextMenu = contextController controller.presentInGlobalOverlay(contextController) } else if case .peer = pane.scope { - guard let data = self.data, let user = data.peer as? TelegramUser else { + guard let data = self.data, case let .user(user) = data.peer else { return } let _ = user diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenFrozenAccount.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenFrozenAccount.swift index 9638abbe96..1466e1c280 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenFrozenAccount.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenFrozenAccount.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import Display diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenMessageActions.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenMessageActions.swift index 54143e7daf..f05fef3627 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenMessageActions.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenMessageActions.swift @@ -19,9 +19,9 @@ extension PeerInfoScreenNode { var items: [ActionSheetItem] = [] var personalPeerName: String? var isChannel = false - if let user = peer as? TelegramUser { - personalPeerName = EnginePeer(user).compactDisplayTitle - } else if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + if case .user = peer { + personalPeerName = peer.compactDisplayTitle + } else if case let .channel(channel) = peer, case .broadcast = channel.info { isChannel = true } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBio.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBio.swift index 3b789a30a7..c6921aea19 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBio.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBio.swift @@ -3,7 +3,6 @@ import UIKit import Display import AccountContext import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import TelegramUIPreferences @@ -54,7 +53,7 @@ extension PeerInfoScreenNode { UIPasteboard.general.string = bioText let toastText: String - if let _ = self.data?.peer as? TelegramUser { + if case .user = self.data?.peer { toastText = self.presentationData.strings.MyProfile_ToastBioCopied } else { toastText = self.presentationData.strings.ChannelProfile_ToastAboutCopied @@ -88,7 +87,7 @@ extension PeerInfoScreenNode { } let copyText: String - if let _ = self.data?.peer as? TelegramUser { + if case .user = self.data?.peer { copyText = self.presentationData.strings.MyProfile_BioActionCopy } else { copyText = self.presentationData.strings.ChannelProfile_AboutActionCopy diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBirthday.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBirthday.swift index 5aab30eef2..d728aa07f5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBirthday.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenBirthday.swift @@ -3,7 +3,6 @@ import UIKit import Display import AccountContext import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import TelegramStringFormatting diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift index 824bd0ce0b..35f61fa9ed 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift @@ -22,25 +22,14 @@ extension PeerInfoScreenNode { navigationController.setViewControllers(viewControllers, animated: true) current.activateSearch(domain: .everything, query: "") } else if let peer = self.data?.chatPeer { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: self.nearbyPeerDistance != nil ? .always : .default, activateMessageSearch: (.everything, ""), peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in - if let strongSelf = self, strongSelf.nearbyPeerDistance != nil { - var viewControllers = navigationController.viewControllers - viewControllers = viewControllers.filter { controller in - if controller is PeerInfoScreen { - return false - } - return true - } - navigationController.setViewControllers(viewControllers, animated: false) - } - })) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default, activateMessageSearch: (.everything, ""))) } } } func openChatForReporting(title: String, option: Data, message: String?) { if let peer = self.data?.peer, let navigationController = (self.controller?.navigationController as? NavigationController) { - if let channel = peer as? TelegramChannel, channel.isForumOrMonoForum { + if case let .channel(channel) = peer, channel.isForumOrMonoForum { //let _ = self.context.engine.peers.reportPeer(peerId: peer.id, reason: reason, message: "").startStandalone() //self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .emoji(name: "PoliceCar", text: self.presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) } else { @@ -48,11 +37,9 @@ extension PeerInfoScreenNode { NavigateToChatControllerParams( navigationController: navigationController, context: self.context, - chatLocation: .peer(EnginePeer(peer)), + chatLocation: .peer(peer), keepStack: .default, - reportReason: NavigateToChatControllerParams.ReportReason(title: title, option: option, message: message), - completion: { _ in - } + reportReason: NavigateToChatControllerParams.ReportReason(title: title, option: option, message: message) ) ) } @@ -61,15 +48,13 @@ extension PeerInfoScreenNode { func openChatForThemeChange() { if let peer = self.data?.peer, let navigationController = (self.controller?.navigationController as? NavigationController) { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: .default, changeColors: true, completion: { _ in - })) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default, changeColors: true)) } } - + func openChatForTranslation() { if let peer = self.data?.peer, let navigationController = (self.controller?.navigationController as? NavigationController) { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: .default, changeColors: false, completion: { _ in - })) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default, changeColors: false)) } } @@ -92,45 +77,66 @@ extension PeerInfoScreenNode { } if let peer = self.data?.peer, let navigationController = self.controller?.navigationController as? NavigationController { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: self.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in - if let strongSelf = self, strongSelf.nearbyPeerDistance != nil { - var viewControllers = navigationController.viewControllers - viewControllers = viewControllers.filter { controller in - if controller is PeerInfoScreen { - return false - } - return true - } - navigationController.setViewControllers(viewControllers, animated: false) - } - })) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default)) } } + func openDeleteReaction(messageId: MessageId) { + guard let authorPeer = self.data?.peer?._asPeer(), let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId), + TelegramEngine.EngineData.Item.Messages.Message(id: messageId) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] sourcePeer, sourceMessage in + guard let self, let sourcePeer, let sourceMessage = sourceMessage?._asMessage() else { + return + } + guard let channel = sourceMessage.peers[sourceMessage.id.peerId] as? TelegramChannel, channel.hasPermission(.deleteAllMessages) else { + return + } + + var hasReaction = false + for attribute in sourceMessage.attributes { + guard let attribute = attribute as? ReactionsMessageAttribute else { + continue + } + if attribute.recentPeers.contains(where: { $0.peerId == authorPeer.id }) || attribute.topPeers.contains(where: { $0.peerId == authorPeer.id }) { + hasReaction = true + break + } + } + guard hasReaction else { + return + } + + let chatLocation: NavigateToChatControllerParams.Location + if case let .channel(channel) = sourcePeer, channel.isForumOrMonoForum, let threadId = sourceMessage.threadId { + chatLocation = .replyThread(ChatReplyThreadMessage(peerId: sourcePeer.id, threadId: threadId, channelMessageId: nil, isChannelPost: false, isForumPost: true, isMonoforumPost: channel.isMonoForum, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)) + } else { + chatLocation = .peer(sourcePeer) + } + + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: true, completion: { chatController in + chatController.presentReactionDeletionOptions(author: authorPeer, messageId: messageId) + })) + }) + } + func openChatWithClearedHistory(type: InteractiveHistoryClearingType) { guard let peer = self.data?.chatPeer, let navigationController = self.controller?.navigationController as? NavigationController else { return } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: self.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), setupController: { controller in + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default, setupController: { controller in controller.beginClearHistory(type: type) - }, completion: { [weak self] _ in - if let strongSelf = self, strongSelf.nearbyPeerDistance != nil { - var viewControllers = navigationController.viewControllers - viewControllers = viewControllers.filter { controller in - if controller is PeerInfoScreen { - return false - } - return true - } - - navigationController.setViewControllers(viewControllers, animated: false) - } })) } func openChannelMessages() { - guard let channel = self.data?.peer as? TelegramChannel, let linkedMonoforumId = channel.linkedMonoforumId else { + guard case let .channel(channel) = self.data?.peer, let linkedMonoforumId = channel.linkedMonoforumId else { return } let _ = (self.context.engine.data.get( diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMember.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMember.swift index ef1ebc78ce..90734a0c9f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMember.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMember.swift @@ -3,7 +3,6 @@ import UIKit import Display import AccountContext import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import TelegramStringFormatting @@ -25,7 +24,7 @@ extension PeerInfoScreenNode { guard let self, let navigationController = self.controller?.navigationController as? NavigationController else { return } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(member.peer)))) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(member.peer))) } }))) } @@ -51,7 +50,7 @@ extension PeerInfoScreenNode { }))) } - if actions.contains(.promote) && enclosingPeer is TelegramChannel { + if actions.contains(.promote), case .channel = enclosingPeer { var actionTitle: String = self.presentationData.strings.GroupInfo_ActionPromote if case .admin = member.role { actionTitle = self.presentationData.strings.GroupInfo_ActionEditAdmin @@ -67,7 +66,7 @@ extension PeerInfoScreenNode { } if actions.contains(.restrict) { - if enclosingPeer is TelegramChannel { + if case .channel = enclosingPeer { items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.GroupInfo_ActionRestrict, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in c?.dismiss { guard let self else { 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/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenNote.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenNote.swift index 2db0ca69fe..fff66b0adb 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenNote.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenNote.swift @@ -3,7 +3,6 @@ import UIKit import Display import AccountContext import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import ContextUI diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenPeerInfoContextMenu.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenPeerInfoContextMenu.swift index b810ae587a..b70068452d 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenPeerInfoContextMenu.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenPeerInfoContextMenu.swift @@ -3,7 +3,6 @@ import UIKit import Display import AccountContext import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import UndoUI @@ -123,7 +122,7 @@ extension PeerInfoScreenNode { text = customLink content = .linkCopied(title: nil, text: self.presentationData.strings.Conversation_LinkCopied) } else if let addressName = peer.addressName { - if peer is TelegramChannel { + if case .channel = peer { text = "https://t.me/\(addressName)" content = .linkCopied(title: nil, text: self.presentationData.strings.Conversation_LinkCopied) } else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenPhone.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenPhone.swift index 9cfa3a0f54..c27895619e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenPhone.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenPhone.swift @@ -3,7 +3,6 @@ import UIKit import Display import AccountContext import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import ContextUI diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenURL.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenURL.swift index 476ce5a7f7..99088bd1e9 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 { @@ -26,7 +25,7 @@ extension PeerInfoScreenNode { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: subject, updateTextInputState: inputState, activateInput: inputState != nil ? .text : nil, keepStack: .always, peekData: peekData)) case .info: if let strongSelf = self, peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil { - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { strongSelf.controller?.push(infoController) } } @@ -159,7 +158,7 @@ extension PeerInfoScreenNode { if self.resolvePeerByNameDisposable == nil { self.resolvePeerByNameDisposable = MetaDisposable() } - var resolveSignal: Signal + var resolveSignal: Signal if let peerName = peerName { resolveSignal = self.context.engine.peers.resolvePeerByName(name: peerName, referrer: nil) |> mapToSignal { result -> Signal in @@ -168,12 +167,16 @@ extension PeerInfoScreenNode { } return .single(result) } - |> mapToSignal { peer -> Signal in - return .single(peer?._asPeer()) - } } else { - resolveSignal = self.context.account.postbox.loadedPeerWithId(self.peerId) - |> map(Optional.init) + resolveSignal = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + |> map { Optional($0) } } var cancelImpl: (() -> Void)? let presentationData = self.presentationData @@ -204,7 +207,7 @@ extension PeerInfoScreenNode { self.resolvePeerByNameDisposable?.set((resolveSignal |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self, !hashtag.isEmpty { - let searchController = HashtagSearchController(context: strongSelf.context, peer: peer.flatMap(EnginePeer.init), query: hashtag) + let searchController = HashtagSearchController(context: strongSelf.context, peer: peer, query: hashtag) strongSelf.controller?.push(searchController) } })) @@ -222,8 +225,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 +243,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 +267,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 +284,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/PeerInfoScreenOpenUsername.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenUsername.swift index 94896590ed..c66c500b38 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenUsername.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenUsername.swift @@ -3,7 +3,6 @@ import UIKit import Display import AccountContext import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import ContextUI diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift index 7d77f7bac8..a04be400a4 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 @@ -23,31 +22,20 @@ extension PeerInfoScreenNode { switch key { case .message: if let navigationController = controller.navigationController as? NavigationController, let peer = self.data?.peer { - if let channel = peer as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.hasMonoforum), let linkedMonoforumId = channel.linkedMonoforumId { + if case let .channel(channel) = peer, case let .broadcast(info) = channel.info, info.flags.contains(.hasMonoforum), let linkedMonoforumId = channel.linkedMonoforumId { Task { @MainActor [weak self] in guard let self else { return } - + guard let peer = await self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: linkedMonoforumId)).get() else { return } - + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default)) } } else { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: self.nearbyPeerDistance != nil ? .always : .default, peerNearbyData: self.nearbyPeerDistance.flatMap({ ChatPeerNearbyData(distance: $0) }), completion: { [weak self] _ in - if let strongSelf = self, strongSelf.nearbyPeerDistance != nil { - var viewControllers = navigationController.viewControllers - viewControllers = viewControllers.filter { controller in - if controller is PeerInfoScreen { - return false - } - return true - } - navigationController.setViewControllers(viewControllers, animated: false) - } - })) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default)) } } case .discussion: @@ -76,7 +64,7 @@ extension PeerInfoScreenNode { } else { displayCustomNotificationSettings = true } - if self.data?.threadData == nil, let channel = self.data?.peer as? TelegramChannel, channel.isForumOrMonoForum { + if self.data?.threadData == nil, case let .channel(channel) = self.data?.peer, channel.isForumOrMonoForum { displayCustomNotificationSettings = true } @@ -234,13 +222,13 @@ extension PeerInfoScreenNode { let mode: NotificationExceptionMode let defaultSound: PeerMessageSound - if let _ = peer as? TelegramUser { + if case .user = peer { mode = .users([:]) defaultSound = globalSettings.privateChats.sound._asMessageSound() - } else if let _ = peer as? TelegramSecretChat { + } else if case .secretChat = peer { mode = .users([:]) defaultSound = globalSettings.privateChats.sound._asMessageSound() - } else if let channel = peer as? TelegramChannel { + } else if case let .channel(channel) = peer { if case .broadcast = channel.info { mode = .channels([:]) defaultSound = globalSettings.channels.sound._asMessageSound() @@ -256,7 +244,7 @@ extension PeerInfoScreenNode { let canRemove = false - let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peer: EnginePeer(peer), threadId: threadId, isStories: nil, canRemove: canRemove, defaultSound: defaultSound, defaultStoriesSound: globalSettings.privateChats.storySettings.sound, edit: true, updatePeerSound: { peerId, sound in + let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peer: peer, threadId: threadId, isStories: nil, canRemove: canRemove, defaultSound: defaultSound, defaultStoriesSound: globalSettings.privateChats.storySettings.sound, edit: true, updatePeerSound: { peerId, sound in let _ = (updatePeerSound(peer.id, sound) |> deliverOnMainQueue).startStandalone(next: { _ in }) @@ -437,18 +425,18 @@ extension PeerInfoScreenNode { var canSetupAutoremoveTimeout = false - if let secretChat = chatPeer as? TelegramSecretChat { + if case let .secretChat(secretChat) = chatPeer { currentAutoremoveTimeout = secretChat.messageAutoremoveTimeout canSetupAutoremoveTimeout = false - } else if let group = chatPeer as? TelegramGroup { + } else if case let .legacyGroup(group) = chatPeer { if !group.hasBannedPermission(.banChangeInfo) { canSetupAutoremoveTimeout = true } - } else if let user = chatPeer as? TelegramUser { + } else if case let .user(user) = chatPeer { if user.id != strongSelf.context.account.peerId { canSetupAutoremoveTimeout = true } - } else if let channel = chatPeer as? TelegramChannel { + } else if case let .channel(channel) = chatPeer { if channel.hasPermission(.changeInfo) { canSetupAutoremoveTimeout = true } @@ -472,7 +460,7 @@ extension PeerInfoScreenNode { } var hasDiscussion = false - if let channel = chatPeer as? TelegramChannel { + if case let .channel(channel) = chatPeer { switch channel.info { case let .broadcast(info): hasDiscussion = info.flags.contains(.hasDiscussionGroup) @@ -489,7 +477,7 @@ extension PeerInfoScreenNode { }))) } - if let user = peer as? TelegramUser { + if case let .user(user) = peer { if user.botInfo == nil && strongSelf.data?.encryptionKeyFingerprint == nil && !user.isDeleted { items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_ChangeWallpaper, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor) @@ -576,7 +564,7 @@ extension PeerInfoScreenNode { } } - if user.botInfo == nil && data.isContact, let peer = strongSelf.data?.peer as? TelegramUser, let phone = peer.phone { + if user.botInfo == nil && data.isContact, case let .user(peer) = strongSelf.data?.peer, let phone = peer.phone { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Profile_ShareContactButton, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in @@ -584,8 +572,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 +583,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 +608,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 +626,7 @@ extension PeerInfoScreenNode { }), in: .current) }) } - } + })) strongSelf.controller?.present(shareController, in: .window(.root)) } }))) @@ -784,7 +771,7 @@ extension PeerInfoScreenNode { }))) } - if let user = data.peer as? TelegramUser, let cachedData = data.cachedData as? CachedUserData, user.botInfo == nil && !user.flags.contains(.isSupport) && user.id != strongSelf.context.account.peerId && strongSelf.peerId.namespace != Namespaces.Peer.SecretChat { + if case let .user(user) = data.peer, let cachedData = data.cachedData as? CachedUserData, user.botInfo == nil && !user.flags.contains(.isSupport) && user.id != strongSelf.context.account.peerId && strongSelf.peerId.namespace != Namespaces.Peer.SecretChat { let copyProtectionEnabled = cachedData.flags.contains(.myCopyProtectionEnabled) || cachedData.flags.contains(.copyProtectionEnabled) items.append(.action(ContextMenuActionItem(text: !copyProtectionEnabled ? strongSelf.presentationData.strings.PeerInfo_DisableSharing : strongSelf.presentationData.strings.PeerInfo_EnableSharing, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: !copyProtectionEnabled ? "Chat/Context Menu/ForwardDisable" : "Chat/Context Menu/ForwardEnable"), color: theme.contextMenu.primaryColor) @@ -814,7 +801,7 @@ extension PeerInfoScreenNode { } let _ = self.context.engine.peers.toggleMessageCopyProtection(peerId: user.id, enabled: true).start() - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: .default, peerNearbyData: nil, completion: { _ in })) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in })) } let _ = (ApplicationSpecificNotice.getCopyProtectionTips(accountManager: self.context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { [weak self] count in @@ -840,7 +827,7 @@ extension PeerInfoScreenNode { let action = { let _ = self.context.engine.peers.toggleMessageCopyProtection(peerId: user.id, enabled: false).start() - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: .default, scrollToEndIfExists: true, completion: { _ in })) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default, scrollToEndIfExists: true, completion: { _ in })) } let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) @@ -848,7 +835,7 @@ extension PeerInfoScreenNode { if let cachedUserData = self.data?.cachedData as? CachedUserData, !cachedUserData.flags.contains(.copyProtectionEnabled), let date = cachedUserData.myCopyProtectionEnableDate, currentTime < date + timeout { action() } else { - let peerName = self.data?.peer.flatMap(EnginePeer.init)?.compactDisplayTitle ?? "" + let peerName = self.data?.peer?.compactDisplayTitle ?? "" let alertController = AlertScreen(context: self.context, configuration: .init(actionAlignment: .vertical), title: self.presentationData.strings.EnableSharing_Title, text: self.presentationData.strings.EnableSharing_Text(peerName).string, actions: [ .init(title: self.presentationData.strings.EnableSharing_SendRequest, type: .default, action: { action() @@ -867,7 +854,7 @@ extension PeerInfoScreenNode { generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ClearMessages"), color: theme.contextMenu.primaryColor) }, action: { c, _ in if let c { - self?.openClearHistory(contextController: c, clearPeerHistory: clearPeerHistory, peer: user, chatPeer: user) + self?.openClearHistory(contextController: c, clearPeerHistory: clearPeerHistory, peer: user, chatPeer: EnginePeer(user)) } }))) } @@ -903,7 +890,7 @@ extension PeerInfoScreenNode { if finalItemsCount > itemsCount { items.insert(.separator, at: itemsCount) } - } else if let channel = peer as? TelegramChannel { + } else if case let .channel(channel) = peer { if let cachedData = strongSelf.data?.cachedData as? CachedChannelData { if case .broadcast = channel.info, cachedData.flags.contains(.starGiftsAvailable) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Profile_SendGift, badge: nil, icon: { theme in @@ -1077,13 +1064,13 @@ extension PeerInfoScreenNode { }))) } - let clearPeerHistory = ClearPeerHistory(context: strongSelf.context, peer: channel, chatPeer: channel, cachedData: strongSelf.data?.cachedData) + let clearPeerHistory = ClearPeerHistory(context: strongSelf.context, peer: channel, chatPeer: EnginePeer(channel), cachedData: strongSelf.data?.cachedData) if clearPeerHistory.canClearForMyself != nil || clearPeerHistory.canClearForEveryone != nil { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_ClearMessages, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ClearMessages"), color: theme.contextMenu.primaryColor) }, action: { c, _ in if let c { - self?.openClearHistory(contextController: c, clearPeerHistory: clearPeerHistory, peer: channel, chatPeer: channel) + self?.openClearHistory(contextController: c, clearPeerHistory: clearPeerHistory, peer: channel, chatPeer: EnginePeer(channel)) } }))) } @@ -1125,7 +1112,7 @@ extension PeerInfoScreenNode { } } } - } else if let group = peer as? TelegramGroup { + } else if case let .legacyGroup(group) = peer { if canSetupAutoremoveTimeout { let strings = strongSelf.presentationData.strings items.append(.action(ContextMenuActionItem(text: currentAutoremoveTimeout == nil ? strongSelf.presentationData.strings.PeerInfo_EnableAutoDelete : strongSelf.presentationData.strings.PeerInfo_AdjustAutoDelete, icon: { theme in @@ -1244,13 +1231,13 @@ extension PeerInfoScreenNode { }))) } - let clearPeerHistory = ClearPeerHistory(context: strongSelf.context, peer: group, chatPeer: group, cachedData: strongSelf.data?.cachedData) + let clearPeerHistory = ClearPeerHistory(context: strongSelf.context, peer: group, chatPeer: EnginePeer(group), cachedData: strongSelf.data?.cachedData) if clearPeerHistory.canClearForMyself != nil || clearPeerHistory.canClearForEveryone != nil { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_ClearMessages, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ClearMessages"), color: theme.contextMenu.primaryColor) }, action: { c, _ in if let c { - self?.openClearHistory(contextController: c, clearPeerHistory: clearPeerHistory, peer: group, chatPeer: group) + self?.openClearHistory(contextController: c, clearPeerHistory: clearPeerHistory, peer: group, chatPeer: EnginePeer(group)) } }))) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift index 0e64cec1d7..c0a89120bf 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift @@ -58,7 +58,6 @@ extension PeerInfoScreenNode { peerId: self.context.account.peerId, avatarInitiallyExpanded: false, isOpenedFromChat: false, - nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], isMyProfile: true, @@ -198,7 +197,7 @@ extension PeerInfoScreenNode { guard let controller = self.controller, !controller.presentAccountFrozenInfoIfNeeded() else { return } - if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone { + if case let .user(user) = self.data?.peer, let phoneNumber = user.phone { let introController = PrivacyIntroController(context: self.context, mode: .changePhoneNumber(phoneNumber), proceedAction: { [weak self] in if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { navigationController.replaceTopController(ChangePhoneNumberController(context: strongSelf.context), animated: true) @@ -251,7 +250,7 @@ extension PeerInfoScreenNode { } }) case .logout: - if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone { + if case let .user(user) = self.data?.peer, let phoneNumber = user.phone { if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController { self.controller?.push(logoutOptionsController(context: self.context, navigationController: navigationController, canAddAccounts: true, phoneNumber: phoneNumber)) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift index 78af5c091e..97065b90f1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSelectionPanelNode.swift @@ -198,8 +198,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { self.backgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor - let interfaceState = ChatPresentationInterfaceState(chatWallpaper: .color(0), theme: presentationData.theme, preferredGlassType: .default, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: .defaultValue, fontSize: .regular, bubbleCorners: PresentationChatBubbleCorners(mainRadius: 16.0, auxiliaryRadius: 8.0, mergeBubbleCorners: true), accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: self.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) - let panelHeight = self.selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height, maxOverlayHeight: layout.size.height, isSecondary: false, transition: transition, interfaceState: interfaceState, metrics: layout.metrics, isMediaInputExpanded: false) + let interfaceState = ChatPresentationInterfaceState(chatWallpaper: .color(0), theme: presentationData.theme, preferredGlassType: .default, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: .defaultValue, fontSize: .regular, bubbleCorners: PresentationChatBubbleCorners(mainRadius: 16.0, auxiliaryRadius: 8.0, mergeBubbleCorners: true), accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: self.peerId), subject: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) + let panelHeight = self.selectionPanel.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height, maxOverlayHeight: layout.size.height, isSecondary: false, transition: transition, interfaceState: interfaceState, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, isMediaInputExpanded: false) transition.updateFrame(node: self.selectionPanel, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: panelHeight))) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift index d4554dfec9..32aa2f6619 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift @@ -48,7 +48,7 @@ func settingsItems(data: PeerInfoScreenData?, context: AccountContext, presentat var setStatusTitle: String = "" let displaySetStatus: Bool var hasEmojiStatus = false - if let peer = data.peer as? TelegramUser, peer.isPremium { + if case let .user(peer) = data.peer, peer.isPremium { if peer.emojiStatus != nil { hasEmojiStatus = true setStatusTitle = presentationData.strings.PeerInfo_ChangeEmojiStatus @@ -86,7 +86,7 @@ func settingsItems(data: PeerInfoScreenData?, context: AccountContext, presentat items[.phone]!.append(PeerInfoScreenActionItem(id: 1, text: "Restore Subscription", action: { interaction.openSettings(.premiumManagement) })) - } else if settings.suggestPhoneNumberConfirmation, let peer = data.peer as? TelegramUser { + } else if settings.suggestPhoneNumberConfirmation, case let .user(peer) = data.peer { let phoneNumber = formatPhoneNumber(context: context, number: peer.phone ?? "") items[.phone]!.append(PeerInfoScreenInfoItem(id: 0, title: presentationData.strings.Settings_CheckPhoneNumberTitle(phoneNumber).string, text: .markdown(presentationData.strings.Settings_CheckPhoneNumberText), linkAction: { link in if case .tap = link { @@ -120,8 +120,7 @@ func settingsItems(data: PeerInfoScreenData?, context: AccountContext, presentat for (peerAccountContext, peer, badgeCount) in settings.accountsAndPeers { let mappedContext = ItemListPeerItem.Context.custom(ItemListPeerItem.Context.Custom( accountPeerId: peerAccountContext.account.peerId, - postbox: peerAccountContext.account.postbox, - network: peerAccountContext.account.network, + engine: peerAccountContext.engine, animationCache: context.animationCache, animationRenderer: context.animationRenderer, isPremiumDisabled: false, @@ -129,7 +128,7 @@ func settingsItems(data: PeerInfoScreenData?, context: AccountContext, presentat return context.engine.stickers.resolveInlineStickers(fileIds: fileIds) } )) - let member: PeerInfoMember = .account(peer: RenderedPeer(peer: peer._asPeer())) + let member: PeerInfoMember = .account(peer: RenderedPeer(peer: peer)) items[.accounts]!.append(PeerInfoScreenMemberItem(id: member.id, context: mappedContext, enclosingPeer: nil, member: member, badge: badgeCount > 0 ? "\(compactNumericCountString(Int(badgeCount), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))" : nil, isAccount: true, action: { action in switch action { case .open: @@ -175,7 +174,7 @@ func settingsItems(data: PeerInfoScreenData?, context: AccountContext, presentat if let settings = data.globalSettings { for bot in settings.bots { let iconSignal: Signal - if let peer = PeerReference(bot.peer._asPeer()), let icon = bot.icons[.iOSSettingsStatic] { + if let peer = PeerReference(bot.peer), let icon = bot.icons[.iOSSettingsStatic] { let fileReference: FileMediaReference = .attachBot(peer: peer, media: icon) iconSignal = instantPageImageFile(account: context.account, userLocation: .other, fileReference: fileReference, fetched: true) |> map { generator -> UIImage? in @@ -370,21 +369,36 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte let ItemBirthdayRemove = 11 let ItemBirthdayHelp = 12 let ItemPeerPersonalChannel = 13 + let ItemPeerChatAutomation = 14 + let ItemPeerChatAutomationHelp = 15 items[.help]!.append(PeerInfoScreenCommentItem(id: ItemNameHelp, text: presentationData.strings.EditProfile_NameAndPhotoOrVideoHelp)) if let cachedData = data.cachedData as? CachedUserData { - items[.bio]!.append(PeerInfoScreenMultilineInputItem(id: ItemBio, text: state.updatingBio ?? (cachedData.about ?? ""), placeholder: presentationData.strings.UserInfo_About_Placeholder, textUpdated: { updatedText in + let currentBio = state.updatingBio ?? (cachedData.about ?? "") + items[.bio]!.append(PeerInfoScreenMultilineInputItem(id: ItemBio, text: currentBio, placeholder: presentationData.strings.UserInfo_About_Placeholder, textUpdated: { updatedText in interaction.updateBio(updatedText) }, action: { interaction.dismissInput() }, maxLength: Int(data.globalSettings?.userLimits.maxAboutLength ?? 70))) - items[.bio]!.append(PeerInfoScreenCommentItem(id: ItemBioHelp, text: presentationData.strings.Settings_About_PrivacyHelp, linkAction: { _ in + + + var bioPrivacyInfo = presentationData.strings.Settings_About_PrivacyHelpEmpty + if let bioPrivacy = data.globalSettings?.privacySettings?.bio, !currentBio.isEmpty { + switch bioPrivacy { + case .enableEveryone: + bioPrivacyInfo = presentationData.strings.Settings_About_PrivacyHelpEveryone + case .enableContacts: + bioPrivacyInfo = presentationData.strings.Settings_About_PrivacyHelpContacts + case .disableEveryone: + bioPrivacyInfo = presentationData.strings.Settings_About_PrivacyHelpNobody + } + } + items[.bio]!.append(PeerInfoScreenCommentItem(id: ItemBioHelp, text: bioPrivacyInfo, linkAction: { _ in interaction.openBioPrivacy() })) } - var birthday: TelegramBirthday? if let updatingBirthDate = state.updatingBirthDate { birthday = updatingBirthDate @@ -400,7 +414,7 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte } let isEditingBirthDate = state.isEditingBirthDate - items[.birthday]!.append(PeerInfoScreenDisclosureItem(id: ItemBirthday, label: .coloredText(birthDateString, isEditingBirthDate ? .accent : .generic), text: presentationData.strings.Settings_Birthday, icon: nil, hasArrow: false, action: { + items[.birthday]!.append(PeerInfoScreenDisclosureItem(id: ItemBirthday, label: .coloredText(birthDateString, isEditingBirthDate ? .accent : .generic), text: presentationData.strings.Settings_Birthday, icon: PresentationResourcesSettings.birthday, hasArrow: false, action: { interaction.updateIsEditingBirthdate(!isEditingBirthDate) })) if isEditingBirthDate, let birthday { @@ -413,17 +427,25 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte })) } - - var birthdayIsForContactsOnly = false - if let birthdayPrivacy = data.globalSettings?.privacySettings?.birthday, case .enableContacts = birthdayPrivacy { - birthdayIsForContactsOnly = true + var birthdayPrivacyInfo = "" + if let birthdayPrivacy = data.globalSettings?.privacySettings?.birthday { + switch birthdayPrivacy { + case .enableEveryone: + birthdayPrivacyInfo = presentationData.strings.Settings_Birthday_PrivacyHelpEveryone + case .enableContacts: + birthdayPrivacyInfo = presentationData.strings.Settings_Birthday_PrivacyHelpContacts + case .disableEveryone: + birthdayPrivacyInfo = presentationData.strings.Settings_Birthday_PrivacyHelpNobody + } + } + if !birthdayPrivacyInfo.isEmpty { + items[.birthday]!.append(PeerInfoScreenCommentItem(id: ItemBirthdayHelp, text: birthdayPrivacyInfo, linkAction: { _ in + interaction.openBirthdatePrivacy() + })) } - items[.birthday]!.append(PeerInfoScreenCommentItem(id: ItemBirthdayHelp, text: birthdayIsForContactsOnly ? presentationData.strings.Settings_Birthday_ContactsHelp : presentationData.strings.Settings_Birthday_Help, linkAction: { _ in - interaction.openBirthdatePrivacy() - })) - if let user = data.peer as? TelegramUser { - items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPhoneNumber, label: .text(user.phone.flatMap({ formatPhoneNumber(context: context, number: $0) }) ?? ""), text: presentationData.strings.Settings_PhoneNumber, action: { + if case let .user(user) = data.peer { + items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPhoneNumber, label: .text(user.phone.flatMap({ formatPhoneNumber(context: context, number: $0) }) ?? ""), text: presentationData.strings.Settings_PhoneNumber, icon: PresentationResourcesSettings.recentCalls, action: { interaction.openSettings(.phoneNumber) })) } @@ -431,11 +453,11 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte if let addressName = data.peer?.addressName, !addressName.isEmpty { username = "@\(addressName)" } - items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text(username), text: presentationData.strings.Settings_Username, action: { + items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text(username), text: presentationData.strings.Settings_Username, icon: PresentationResourcesSettings.email, action: { interaction.openSettings(.username) })) - if let peer = data.peer as? TelegramUser { + if case let .user(peer) = data.peer { var colors: [PeerNameColors.Colors] = [] if let nameColor = peer.nameColor { let nameColors: PeerNameColors.Colors @@ -452,7 +474,7 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte } let colorImage = generateSettingsMenuPeerColorsLabelIcon(colors: colors) - items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), text: presentationData.strings.Settings_YourColor, icon: nil, action: { + items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), text: presentationData.strings.Settings_YourColor, icon: PresentationResourcesSettings.yourColor, action: { interaction.editingOpenNameColorSetup() })) @@ -468,12 +490,24 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte personalChannelTitle = peer.compactDisplayTitle } - items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerPersonalChannel, label: .text(personalChannelTitle ?? presentationData.strings.Settings_PersonalChannelEmptyValue), text: presentationData.strings.Settings_PersonalChannelItem, icon: nil, action: { + items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerPersonalChannel, label: .text(personalChannelTitle ?? presentationData.strings.Settings_PersonalChannelEmptyValue), text: presentationData.strings.Settings_PersonalChannelItem, icon: PresentationResourcesSettings.channels, action: { interaction.editingOpenPersonalChannel() })) } } + let automationBotTitle: String + if let botPeer = data.businessConnectedBot { + let _ = botPeer + automationBotTitle = "@\(botPeer.compactDisplayTitle)" + } else { + automationBotTitle = presentationData.strings.Settings_ChatAutomationOff + } + items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerChatAutomation, label: .text(automationBotTitle), additionalBadgeLabel: nil, text: presentationData.strings.Settings_ChatAutomation, icon: PresentationResourcesSettings.aiTools, action: { + interaction.editingOpenBusinessChatBots() + })) + items[.info]!.append(PeerInfoScreenCommentItem(id: ItemPeerChatAutomationHelp, text: presentationData.strings.Settings_ChatAutomationInfo)) + items[.account]!.append(PeerInfoScreenActionItem(id: ItemAddAccount, text: presentationData.strings.Settings_AddAnotherAccount, alignment: .center, action: { interaction.openSettings(.addAccount) })) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PhotoUpdateConfirmationController.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PhotoUpdateConfirmationController.swift index ce30f6f28a..3408599eae 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PhotoUpdateConfirmationController.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PhotoUpdateConfirmationController.swift @@ -3,7 +3,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PresentAddMembers.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PresentAddMembers.swift index af50c523e1..2ff46cd6a4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PresentAddMembers.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PresentAddMembers.swift @@ -11,7 +11,7 @@ import SendInviteLinkScreen import UndoUI import PresentationDataUtils -public func presentAddMembersImpl(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, parentController: ViewController, groupPeer: Peer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable) { +public func presentAddMembersImpl(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, parentController: ViewController, groupPeer: EnginePeer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable) { let members: Promise<[PeerId]> = Promise() if groupPeer.id.namespace == Namespaces.Peer.CloudChannel { /*var membersDisposable: Disposable? @@ -35,7 +35,7 @@ public func presentAddMembersImpl(context: AccountContext, updatedPresentationDa let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } var canCreateInviteLink = false - if let group = groupPeer as? TelegramGroup { + if case let .legacyGroup(group) = groupPeer { switch group.role { case .creator: canCreateInviteLink = true @@ -44,7 +44,7 @@ public func presentAddMembersImpl(context: AccountContext, updatedPresentationDa default: break } - } else if let channel = groupPeer as? TelegramChannel, (channel.addressName?.isEmpty ?? true) { + } else if case let .channel(channel) = groupPeer, (channel.addressName?.isEmpty ?? true) { if channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) { canCreateInviteLink = true } @@ -60,13 +60,20 @@ public func presentAddMembersImpl(context: AccountContext, updatedPresentationDa contactsController.navigationPresentation = .modal confirmationImpl = { [weak contactsController] peerId in - return context.account.postbox.loadedPeerWithId(peerId) + return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue |> mapToSignal { peer in let result = ValuePromise() let presentationData = context.sharedContext.currentPresentationData.with { $0 } if let contactsController = contactsController { - let alertController = textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.GroupInfo_AddParticipantConfirmation(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, actions: [ + let alertController = textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.GroupInfo_AddParticipantConfirmation(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string, actions: [ TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: { result.set(false) }), @@ -200,7 +207,7 @@ public func presentAddMembersImpl(context: AccountContext, updatedPresentationDa if !failedPeers.isEmpty, let contactsController, let navigationController = contactsController.navigationController as? NavigationController { var viewControllers = navigationController.viewControllers if let index = viewControllers.firstIndex(where: { $0 === contactsController }) { - let inviteScreen = SendInviteLinkScreen(context: context, subject: .chat(peer: EnginePeer(groupPeer), link: exportedInvitation?.link), peers: failedPeers) + let inviteScreen = SendInviteLinkScreen(context: context, subject: .chat(peer: groupPeer, link: exportedInvitation?.link), peers: failedPeers) viewControllers.remove(at: index) viewControllers.append(inviteScreen) navigationController.setViewControllers(viewControllers, animated: true) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD index b26f1644a1..220a3538e5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AsyncDisplayKit", "//submodules/TelegramCore", - "//submodules/Postbox", "//submodules/TelegramPresentationData", "//submodules/AccountContext", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index 3b3a3c0082..e6443db7b5 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -242,7 +242,7 @@ final class PeerInfoStoryGridScreenComponent: Component { guard let self, let component = self.component, let peer else { return } - guard let peerReference = PeerReference(peer._asPeer()) else { + guard let peerReference = PeerReference(peer) else { return } @@ -265,7 +265,7 @@ final class PeerInfoStoryGridScreenComponent: Component { for (_, item) in sortedItems { let itemOffset = progressStart progressStart += valueNorm - signals.append(saveToCameraRoll(context: component.context, postbox: component.context.account.postbox, userLocation: .other, mediaReference: .story(peer: peerReference, id: item.id, media: item.media._asMedia())) + signals.append(saveToCameraRoll(context: component.context, userLocation: .other, mediaReference: .story(peer: peerReference, id: item.id, media: item.media._asMedia())) |> map { progress -> Float in return itemOffset + progress * valueNorm }) 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..b5ada88e29 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 @@ -490,7 +489,7 @@ private final class DurationLayer: SimpleLayer { }) self.contents = image?.cgImage - if let smallProfileImage = author.smallProfileImage, let peerReference = PeerReference(author._asPeer()) { + if let smallProfileImage = author.smallProfileImage, let peerReference = PeerReference(author) { if let result = directMediaImageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: smallProfileImage.immediateThumbnailData, size: 24, includeBlurred: true, synchronous: synchronous == .full) { if let image = result.image { avatarLayer.contents = image.cgImage @@ -2767,28 +2766,14 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr guard let self else { return } - guard let peer, let peerReference = PeerReference(peer._asPeer()) else { + guard let peer, let peerReference = PeerReference(peer) else { 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)) }) }) @@ -3075,7 +3060,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr if let value = state.peerReference { peerReference = value } else if let peer = item.peer { - peerReference = PeerReference(peer._asPeer()) + peerReference = PeerReference(peer) } guard let peerReference else { continue @@ -5192,7 +5177,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public func presentDeleteBotPreviewLanguage() { self.parentController?.present(textAlertController(context: self.context, title: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Title, text: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Text, actions: [ - TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: { + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in guard let self else { @@ -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/PeerInfo/ProfileLevelRatingBarComponent/BUILD b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelRatingBarComponent/BUILD index b61bba252c..1bc81a7320 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelRatingBarComponent/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelRatingBarComponent/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", "//submodules/TelegramCore", - "//submodules/Postbox", "//submodules/TelegramPresentationData", "//submodules/AccountContext", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/PeerInfo/VerifyAlertController/BUILD b/submodules/TelegramUI/Components/PeerInfo/VerifyAlertController/BUILD index 788a843cf0..3944bcd19e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/VerifyAlertController/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/VerifyAlertController/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/PeerInfo/VerifyAlertController/Sources/VerifyAlertController.swift b/submodules/TelegramUI/Components/PeerInfo/VerifyAlertController/Sources/VerifyAlertController.swift index d807a4efb0..9ea1e37f24 100644 --- a/submodules/TelegramUI/Components/PeerInfo/VerifyAlertController/Sources/VerifyAlertController.swift +++ b/submodules/TelegramUI/Components/PeerInfo/VerifyAlertController/Sources/VerifyAlertController.swift @@ -3,7 +3,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import AccountContext diff --git a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD index b4afc963ea..620a7dd779 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", "//submodules/TelegramCore", - "//submodules/Postbox", "//submodules/TelegramPresentationData", "//submodules/PresentationDataUtils", "//submodules/AccountContext", diff --git a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift index 743d9178cb..db24d5d6ca 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift @@ -45,7 +45,7 @@ func localizedOldChannelDate(peer: InactiveChannel, strings: PresentationStrings string = strings.OldChannels_InactiveYear(Int32(dif)) } - if let channel = peer.peer as? TelegramChannel, case .group = channel.info { + if case let .channel(channel) = peer.peer, case .group = channel.info { if let participantsCount = peer.participantsCount, participantsCount != 0 { string = strings.OldChannels_GroupFormat(participantsCount) + ", " + string } else { @@ -172,7 +172,7 @@ private enum OldChannelsEntry: ItemListNodeEntry { case let .peersHeader(title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .peer(_, peer, selected): - return ContactsPeerItem(presentationData: presentationData, style: .blocks, systemStyle: .glass, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: NSAttributedString(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings)), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in + return ContactsPeerItem(presentationData: presentationData, style: .blocks, systemStyle: .glass, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .custom(string: NSAttributedString(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings)), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in arguments.togglePeer(peer.peer.id, true) }, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil) } @@ -386,7 +386,7 @@ public func oldChannelsController(context: AccountContext, updatedPresentationDa let peers = peers ?? [] let ensureStoredPeers = peers.map { $0.peer }.filter { state.selectedPeers.contains($0.id) } - let ensureStoredPeersSignal: Signal = context.engine.peers.ensurePeersAreLocallyAvailable(peers: ensureStoredPeers.map(EnginePeer.init)) + let ensureStoredPeersSignal: Signal = context.engine.peers.ensurePeersAreLocallyAvailable(peers: ensureStoredPeers) return ensureStoredPeersSignal |> then(context.engine.peers.removePeerChats(peerIds: Array(ensureStoredPeers.map(\.id)))) diff --git a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift index 8259a5ab74..2b85b10410 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift @@ -154,7 +154,7 @@ private enum OldChannelsSearchEntry: Comparable, Identifiable { func item(context: AccountContext, presentationData: ItemListPresentationData, interaction: OldChannelsSearchInteraction) -> ListViewItem { switch self { case let .peer(_, peer, selected): - return ContactsPeerItem(presentationData: presentationData, style: .plain, sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: NSAttributedString(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings)), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in + return ContactsPeerItem(presentationData: presentationData, style: .plain, sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .custom(string: NSAttributedString(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings)), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in interaction.togglePeer(peer.peer.id) }, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil) } diff --git a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD index 0afb500308..499b18092a 100644 --- a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", "//submodules/TelegramCore", - "//submodules/Postbox", "//submodules/TelegramPresentationData", "//submodules/PresentationDataUtils", "//submodules/AccountContext", diff --git a/submodules/TelegramUI/Components/PeerReportScreen/BUILD b/submodules/TelegramUI/Components/PeerReportScreen/BUILD index 95a0b40747..4b5c1ff8bc 100644 --- a/submodules/TelegramUI/Components/PeerReportScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerReportScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/PeerReportScreen/Sources/PeerReportScreen.swift b/submodules/TelegramUI/Components/PeerReportScreen/Sources/PeerReportScreen.swift index cdae86c7b0..d4cef5ddbe 100644 --- a/submodules/TelegramUI/Components/PeerReportScreen/Sources/PeerReportScreen.swift +++ b/submodules/TelegramUI/Components/PeerReportScreen/Sources/PeerReportScreen.swift @@ -18,7 +18,7 @@ import Markdown public enum PeerReportSubject { case peer(EnginePeer.Id) case messages([EngineMessage.Id]) - case profilePhoto(EnginePeer.Id, Int64) + case profilePhoto(EnginePeer.Id, Int64, sourceMessageId: EngineMessage.Id?) case story(EnginePeer.Id, Int32) } @@ -149,8 +149,8 @@ public func presentPeerReportOptions( displaySuccess() completion(nil, false) }) - case let .profilePhoto(peerId, _): - let _ = (context.engine.peers.reportPeerPhoto(peerId: peerId, reason: reportReason, message: "") + case let .profilePhoto(peerId, _, sourceMessageId): + let _ = (context.engine.peers.reportPeerPhoto(peerId: peerId, reason: reportReason, message: "", sourceMessageId: sourceMessageId) |> deliverOnMainQueue).start(completed: { displaySuccess() completion(nil, false) @@ -285,8 +285,8 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe displaySuccess() completion(nil, true) }) - case let .profilePhoto(peerId, _): - let _ = (context.engine.peers.reportPeerPhoto(peerId: peerId, reason: reportReason, message: message) + case let .profilePhoto(peerId, _, sourceMessageId): + let _ = (context.engine.peers.reportPeerPhoto(peerId: peerId, reason: reportReason, message: message, sourceMessageId: sourceMessageId) |> deliverOnMainQueue).start(completed: { displaySuccess() completion(nil, true) 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/PeerSelectionController/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift index 6d5c9bb67a..fb257f9244 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) @@ -354,7 +356,7 @@ public final class PeerSelectionControllerImpl: ViewController, PeerSelectionCon self.peerSelectionNode.requestOpenPeerFromSearch = { [weak self] peer, threadId in if let strongSelf = self { - strongSelf.openMessageFromSearchDisposable.set((_internal_storedMessageFromSearchPeer(postbox: strongSelf.context.account.postbox, peer: peer._asPeer()) + strongSelf.openMessageFromSearchDisposable.set((_internal_storedMessageFromSearchPeer(postbox: strongSelf.context.account.postbox, peer: peer) |> deliverOnMainQueue).start(completed: { [weak strongSelf] in if let strongSelf = strongSelf, let peerSelected = strongSelf.peerSelected { if case let .channel(peer) = peer, peer.isForumOrMonoForum, threadId == nil, strongSelf.selectForumThreads { diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift index aedd7a31fa..51907ac87e 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionControllerNode.swift @@ -41,16 +41,17 @@ 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() - + private var interfaceInteraction: ChatPanelInterfaceInteraction? - + var inProgress: Bool = false - + var navigationBar: NavigationBar? - + private let requirementsBackgroundNode: NavigationBackgroundNode? private let requirementsSeparatorNode: ASDisplayNode? private let requirementsTextNode: ImmediateTextNode? @@ -60,28 +61,28 @@ final class PeerSelectionControllerNode: ASDisplayNode { private let emptyTitleNode: ImmediateTextNode private let emptyTextNode: ImmediateTextNode private let emptyButtonNode: SolidRoundedButtonNode - + private let bottomEdgeEffectView: EdgeEffectView? private let segmentedControl: ComponentView? private var segmentedControlItems: [String]? private var segmentedControlSelectedIndex: Int = 0 - + private var textInputPanelNode: AttachmentTextInputPanelNode? private var forwardAccessoryPanelNode: ForwardAccessoryPanelNode? - + var contactListNode: ContactListNode? let chatListNode: ChatListNode? let mainContainerNode: ChatListContainerNode? - + private var contactListActive = false - + private var searchDisplayController: SearchDisplayController? - + private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)? - + var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? var contentScrollingEnded: ((ListView) -> Bool)? - + var requestActivateSearch: (() -> Void)? var requestDeactivateSearch: (() -> Void)? var requestOpenPeer: ((EnginePeer, Int64?) -> Void)? @@ -89,33 +90,33 @@ final class PeerSelectionControllerNode: ASDisplayNode { var requestOpenPeerFromSearch: ((EnginePeer, Int64?) -> Void)? var requestOpenMessageFromSearch: ((EnginePeer, Int64?, EngineMessage.Id) -> Void)? var requestSend: (([EnginePeer], [EnginePeer.Id: EnginePeer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?, ChatSendMessageActionSheetController.SendParameters?) -> Void)? - + private var presentationData: PresentationData { didSet { self.presentationDataPromise.set(.single(self.presentationData)) } } private var presentationDataPromise = Promise() - + private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer - + private var countPanelNode: PeersCountPanelNode? - + weak var pushedController: ViewController? - + private var readyValue = Promise() var ready: Signal { return self.readyValue.get() } - + private var isEmpty = false - + private var updatedPresentationData: (initial: PresentationData, signal: Signal) { 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,17 +128,18 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.forwardedMessageIds = forwardedMessageIds self.hasTypeHeaders = hasTypeHeaders self.requestPeerType = requestPeerType - + self.suggestedPeers = suggestedPeers + self.presentationData = presentationData - + self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer - - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, preferredGlassType: .default, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) - + + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, preferredGlassType: .default, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: PeerId(0)), subject: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) + self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardMessageIds(forwardedMessageIds) } self.presentationInterfaceStatePromise.set(self.presentationInterfaceState) - + if let _ = self.requestPeerType { self.requirementsBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) self.requirementsSeparatorNode = ASDisplayNode() @@ -150,34 +152,34 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.requirementsSeparatorNode = nil self.requirementsTextNode = nil } - + self.emptyTitleNode = ImmediateTextNode() self.emptyTitleNode.displaysAsynchronously = false self.emptyTitleNode.maximumNumberOfLines = 0 self.emptyTitleNode.isHidden = true self.emptyTitleNode.textAlignment = .center self.emptyTitleNode.lineSpacing = 0.25 - + self.emptyTextNode = ImmediateTextNode() self.emptyTextNode.displaysAsynchronously = false self.emptyTextNode.maximumNumberOfLines = 0 self.emptyTextNode.isHidden = true self.emptyTextNode.lineSpacing = 0.25 - + self.emptyAnimationNode = DefaultAnimatedStickerNodeImpl() self.emptyAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ChatListNoResults"), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) self.emptyAnimationNode.isHidden = true self.emptyAnimationSize = CGSize(width: 120.0, height: 120.0) - + self.emptyButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), cornerRadius: 11.0, isShimmering: true) self.emptyButtonNode.isHidden = true self.emptyButtonNode.pressed = { createNewGroup?() } - + if hasChatListSelector && hasContactSelector { self.bottomEdgeEffectView = EdgeEffectView() - + self.segmentedControl = ComponentView() self.segmentedControlItems = [ self.presentationData.strings.DialogList_TabTitle, @@ -187,13 +189,13 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.bottomEdgeEffectView = nil self.segmentedControl = nil } - + var chatListCategories: [ChatListNodeAdditionalCategory] = [] - + if let _ = createNewGroup { chatListCategories.append(ChatListNodeAdditionalCategory(id: 0, icon: PresentationResourcesItemList.createGroupIcon(self.presentationData.theme), smallIcon: nil, title: self.presentationData.strings.PeerSelection_ImportIntoNewGroup, appearance: .action)) } - + let chatListLocation: ChatListControllerLocation if let (forumPeerId, isMonoforum) = self.forumPeerId { if isMonoforum { @@ -204,14 +206,14 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else { chatListLocation = .chatList(groupId: .root) } - + let chatListMode: ChatListNodeMode 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 { self.mainContainerNode = ChatListContainerNode(context: context, controller: nil, location: chatListLocation, chatListMode: chatListMode, previewing: false, controlsHistoryPreload: false, isInlineMode: false, presentationData: presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filterBecameEmpty: { _ in }, filterEmptyAction: { _ in @@ -226,38 +228,38 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.chatListNode?.selectionLimit = multipleSelectionLimit } } - + super.init() - + self.setViewBlock({ return UITracingLayerView() }) - + self.chatListNode?.additionalCategorySelected = { _ in createNewGroup?() } - + self.backgroundColor = self.presentationData.theme.chatList.backgroundColor - + self.chatListNode?.selectionCountChanged = { [weak self] count in self?.textInputPanelNode?.updateSendButtonEnabled(count > 0, animated: true) } self.chatListNode?.accessibilityPageScrolledString = { row, count in return presentationData.strings.VoiceOver_ScrollStatus(row, count).string } - + self.chatListNode?.activateSearch = { [weak self] in self?.requestActivateSearch?() } self.mainContainerNode?.activateSearch = { [weak self] in self?.requestActivateSearch?() } - + self.chatListNode?.peerSelected = { [weak self] peer, threadId, _, _, _ in guard let self else { return } - + if let (peerId, isMonoforum) = self.forumPeerId, isMonoforum { let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) @@ -278,7 +280,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { guard let self else { return } - + if let (peerId, isMonoforum) = self.forumPeerId, isMonoforum { let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) @@ -297,14 +299,14 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.requestOpenPeer?(peer, threadId) } } - + self.chatListNode?.disabledPeerSelected = { [weak self] peer, threadId, reason in self?.requestOpenDisabledPeer?(peer, threadId, reason) } self.mainContainerNode?.disabledPeerSelected = { [weak self] peer, threadId, reason in self?.requestOpenDisabledPeer?(peer, threadId, reason) } - + self.chatListNode?.contentOffsetChanged = { [weak self] offset in guard let strongSelf = self else { return @@ -313,7 +315,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { strongSelf.contentOffsetChanged?(offset) } } - + self.mainContainerNode?.contentOffsetChanged = { [weak self] offset, _ in guard let strongSelf = self else { return @@ -322,11 +324,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { strongSelf.contentOffsetChanged?(offset) } } - + self.chatListNode?.contentScrollingEnded = { [weak self] listView in return self?.contentScrollingEnded?(listView) ?? false } - + self.chatListNode?.isEmptyUpdated = { [weak self] state, _, _ in guard let strongSelf = self else { return @@ -337,7 +339,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate) } } - + if let mainContainerNode = self.mainContainerNode { mainContainerNode.displayFilterLimit = { [weak self] in guard let strongSelf = self else { @@ -360,28 +362,28 @@ final class PeerSelectionControllerNode: ASDisplayNode { if let chatListNode = self.chatListNode { self.addSubnode(chatListNode) } - + if hasChatListSelector && hasContactSelector { if let bottomEdgeEffectView = self.bottomEdgeEffectView { self.view.addSubview(bottomEdgeEffectView) } } - + if let requirementsBackgroundNode = self.requirementsBackgroundNode, let requirementsSeparatorNode = self.requirementsSeparatorNode, let requirementsTextNode = self.requirementsTextNode { self.chatListNode?.addSubnode(requirementsBackgroundNode) self.chatListNode?.addSubnode(requirementsSeparatorNode) self.chatListNode?.addSubnode(requirementsTextNode) - + self.addSubnode(self.emptyAnimationNode) self.addSubnode(self.emptyTitleNode) self.addSubnode(self.emptyTextNode) self.addSubnode(self.emptyButtonNode) } - + if !hasChatListSelector && hasContactSelector { self.indexChanged(1) } - + self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _, _, _ in }, setupEditMessage: { _, _ in }, beginMessageSelection: { _, _ in @@ -405,16 +407,16 @@ final class PeerSelectionControllerNode: ASDisplayNode { } let presentationData = strongSelf.presentationData - + let peerIds = strongSelf.selectedPeers.0.map { $0.id } - + let forwardOptions: Signal forwardOptions = strongSelf.presentationInterfaceStatePromise.get() |> map { state -> ChatControllerSubject.ForwardOptions in return ChatControllerSubject.ForwardOptions(hideNames: state.interfaceState.forwardOptionsState?.hideNames ?? false, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false) } |> distinctUntilChanged - + let chatController = strongSelf.context.sharedContext.makeChatController( context: strongSelf.context, chatLocation: .peer(id: strongSelf.context.account.peerId), @@ -424,7 +426,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { params: nil ) chatController.canReadHistory.set(false) - + let messageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] let messagesCount: Signal if messageIds.count > 1 { @@ -441,15 +443,15 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else { messagesCount = .single(1) } - + let accountPeerId = strongSelf.context.account.peerId let items = combineLatest(forwardOptions, strongSelf.context.account.postbox.messagesAtIds(messageIds), messagesCount) |> map { forwardOptions, messages, messagesCount -> [ContextMenuItem] in var items: [ContextMenuItem] = [] - + var hasCaptions = false var uniquePeerIds = Set() - + var hasOther = false var hasNotOwnMessages = false for message in messages { @@ -462,15 +464,12 @@ final class PeerSelectionControllerNode: ASDisplayNode { hasNotOwnMessages = true } } - + var isDice = false - var isMusic = false for media in message.media { - if let media = media as? TelegramMediaFile, media.isMusic { - isMusic = true - } else if media is TelegramMediaDice { + if media is TelegramMediaDice { isDice = true - } else { + } else if (media as? TelegramMediaFile)?.isMusic != true { if !message.text.isEmpty { if media is TelegramMediaImage || media is TelegramMediaFile { hasCaptions = true @@ -478,16 +477,16 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } } - if !isDice && !isMusic { + if !isDice { hasOther = true } } - + let canHideNames = hasNotOwnMessages && hasOther - + let hideNames = forwardOptions.hideNames let hideCaptions = forwardOptions.hideCaptions - + if !"".isEmpty { // check if seecret chat } else { if canHideNames { @@ -506,7 +505,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return updated }) }))) - + items.append(.action(ContextMenuActionItem(text: uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_HideSendersName : presentationData.strings.Conversation_ForwardOptions_HideSendersNames, icon: { theme in if hideNames { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) @@ -521,10 +520,10 @@ final class PeerSelectionControllerNode: ASDisplayNode { return updated }) }))) - + items.append(.separator) } - + if hasCaptions { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_ShowCaption, icon: { theme in if hideCaptions { @@ -543,7 +542,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return updated }) }))) - + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_HideCaption, icon: { theme in if hideCaptions { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) @@ -561,11 +560,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { return updated }) }))) - + items.append(.separator) } } - + items.append(.action(ContextMenuActionItem(text: messagesCount == 1 ? presentationData.strings.Conversation_ForwardOptions_SendMessage : presentationData.strings.Conversation_ForwardOptions_SendMessages, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak chatController] c, f in guard let strongSelf = self else { return @@ -579,7 +578,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { f(.default) }))) - + return items } @@ -683,7 +682,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { var selectionRange: Range? var text: NSAttributedString? var inputMode: ChatInputMode? - + strongSelf.updateChatPresentationInterfaceState(animated: true, { state in selectionRange = state.interfaceState.effectiveInputState.selectionRange if let selectionRange = selectionRange { @@ -692,7 +691,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { inputMode = state.inputMode return state }) - + var link: String? if let text { text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in @@ -701,7 +700,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } } - + let controller = chatTextLinkEditController(context: context, updatedPresentationData: (presentationData, .never()), text: text?.string ?? "", link: link, apply: { [weak self] link in if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange { if let link = link { @@ -723,13 +722,13 @@ final class PeerSelectionControllerNode: ASDisplayNode { }) strongSelf.present(controller, nil) } - }, openDateEditing: { + }, openDateEditing: { }, displaySlowmodeTooltip: { _, _ in }, displaySendMessageOptions: { [weak self] node, gesture in guard let strongSelf = self else { return } - + let _ = (ChatSendMessageContextScreen.initialData(context: strongSelf.context, currentMessageEffectId: nil) |> deliverOnMainQueue).start(next: { initialData in guard let strongSelf = self, let textInputPanelNode = strongSelf.textInputPanelNode else { @@ -739,12 +738,12 @@ final class PeerSelectionControllerNode: ASDisplayNode { guard let textInputNode = textInputPanelNode.textInputNode else { return } - + var hasEntityKeyboard = false if case .media = strongSelf.presentationInterfaceState.inputMode { hasEntityKeyboard = true } - + let controller = makeChatSendMessageActionSheetController( initialData: initialData, context: strongSelf.context, @@ -845,15 +844,15 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, chatController: { return nil }, statuses: nil) - + if let chatListNode = self.chatListNode { self.readyValue.set(chatListNode.ready) } } - + override func didLoad() { super.didLoad() - + if let controller = self.controller, controller.immediatelySwitchToContacts { self.segmentedControlSelectedIndex = 1 if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { @@ -862,33 +861,33 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.indexChanged(1) } } - + func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData self.updateThemeAndStrings() self.mainContainerNode?.updatePresentationData(presentationData) } - + private func updateChatPresentationInterfaceState(animated: Bool = true, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, f, completion: completion) } - + private func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { let presentationInterfaceState = f(self.presentationInterfaceState) let updateInputTextState = self.presentationInterfaceState.interfaceState.effectiveInputState != presentationInterfaceState.interfaceState.effectiveInputState - + self.presentationInterfaceState = presentationInterfaceState self.presentationInterfaceStatePromise.set(presentationInterfaceState) - + if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated) } - + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: transition) } } - + private var selectedPeers: ([EnginePeer], [EnginePeer.Id: EnginePeer]) { if self.contactListActive { let selectedContactPeers = self.contactListNode?.selectedPeers ?? [] @@ -897,8 +896,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { var selectedPeerMap: [EnginePeer.Id: EnginePeer] = [:] for contactPeer in selectedContactPeers { if case let .peer(peer, _, _) = contactPeer { - selectedPeers.append(EnginePeer(peer)) - selectedPeerMap[peer.id] = EnginePeer(peer) + selectedPeers.append(peer) + selectedPeerMap[peer.id] = peer } } return (selectedPeers, selectedPeerMap) @@ -928,7 +927,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return (selectedPeers, selectedPeerMap) } } - + func beginSelection() { guard let controller = self.controller else { return @@ -945,7 +944,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { }) self.addSubnode(countPanelNode) self.countPanelNode = countPanelNode - + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate) } @@ -956,33 +955,50 @@ final class PeerSelectionControllerNode: ASDisplayNode { forwardAccessoryPanelNode.interfaceInteraction = self.interfaceInteraction self.addSubnode(forwardAccessoryPanelNode) self.forwardAccessoryPanelNode = forwardAccessoryPanelNode - - let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, presentController: { [weak self] c in self?.present(c, nil) }, makeEntityInputView: { - return nil - }) + + let textInputPanelNode = AttachmentTextInputPanelNode( + context: self.context, + presentationInterfaceState: self.presentationInterfaceState, + customEmojiAvailable: self.presentationInterfaceState.customEmojiAvailable, + presentController: { [weak self] c in + self?.present(c, nil) + }, + presentInGlobalOverlay: { [weak self] c in + self?.presentInGlobalOverlay(c, nil) + }, + getNavigationController: { [weak self] in + return self?.controller?.navigationController as? NavigationController + } + ) textInputPanelNode.interfaceInteraction = self.interfaceInteraction textInputPanelNode.sendMessage = { [weak self] mode, messageEffect in guard let strongSelf = self else { return } - + let effectiveInputText = strongSelf.presentationInterfaceState.interfaceState.composeInputState.inputText let forwardOptionsState = strongSelf.presentationInterfaceState.interfaceState.forwardOptionsState - + let (selectedPeers, selectedPeerMap) = strongSelf.selectedPeers if !selectedPeers.isEmpty { strongSelf.requestSend?(selectedPeers, selectedPeerMap, effectiveInputText, mode, forwardOptionsState, messageEffect) } } + textInputPanelNode.updateHeight = { [weak self] _ in + guard let self, let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout else { + return + } + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.3, curve: .spring)) + } self.addSubnode(textInputPanelNode) self.textInputPanelNode = textInputPanelNode - + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.3, curve: .spring)) } } } - + if self.contactListActive { self.contactListNode?.multipleSelection = true self.contactListNode?.updateSelectionState({ _ in @@ -1007,11 +1023,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.countPanelNode?.buttonTitle = self.presentationData.strings.ShareMenu_Send } self.countPanelNode?.count = count - + if let titleView = self.controller?.titleView, let maxCount = self.controller?.multipleSelectionLimit { titleView.title = CounterControllerTitle(title: titleView.title.title, counter: "\(count)/\(maxCount)") } - + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.3, curve: .spring)) } @@ -1025,49 +1041,46 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } } - + private func updateThemeAndStrings() { self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.searchDisplayController?.updatePresentationData(self.presentationData) self.chatListNode?.updateThemeAndStrings(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) - + self.updateChatPresentationInterfaceState({ $0.updatedTheme(self.presentationData.theme) }) - + self.requirementsBackgroundNode?.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - + if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate) } } - + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight) - + let cleanInsets = layout.insets(options: []) var insets = layout.insets(options: [.input]) - + var toolbarHeight: CGFloat = cleanInsets.bottom var textPanelHeight: CGFloat? var accessoryHeight: CGFloat = 0.0 - + if let forwardAccessoryPanelNode = self.forwardAccessoryPanelNode { let size = forwardAccessoryPanelNode.calculateSizeThatFits(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: layout.size.height)) accessoryHeight = size.height } - + if let textInputPanelNode = self.textInputPanelNode { var panelTransition = transition if textInputPanelNode.frame.width.isZero { panelTransition = .immediate } - var panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false) - if self.searchDisplayController == nil { - panelHeight += insets.bottom - } else { - panelHeight += cleanInsets.bottom - } + var panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.safeInsets.bottom, keyboardHeight: layout.inputHeight ?? 0.0, additionalSideInsets: UIEdgeInsets(), textFieldMaxHeight: layout.size.height / 2.0, availableHeight: layout.size.height, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false) + let effectiveBottomInset = textInputPanelNode.additionalInputHeight > 0.0 ? 0.0 : (self.searchDisplayController == nil ? insets.bottom : cleanInsets.bottom) + panelHeight += effectiveBottomInset textPanelHeight = panelHeight - + let panelFrame = CGRect(x: 0.0, y: layout.size.height - panelHeight, width: layout.size.width, height: panelHeight) if textInputPanelNode.frame.width.isZero { var initialPanelFrame = panelFrame @@ -1076,13 +1089,13 @@ final class PeerSelectionControllerNode: ASDisplayNode { } transition.updateFrame(node: textInputPanelNode, frame: panelFrame) } - + if let forwardAccessoryPanelNode = self.forwardAccessoryPanelNode { let size = forwardAccessoryPanelNode.calculateSizeThatFits(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: layout.size.height)) forwardAccessoryPanelNode.updateState(size: size, inset: layout.safeInsets.left, interfaceState: self.presentationInterfaceState) forwardAccessoryPanelNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, forwardOptionsState: self.presentationInterfaceState.interfaceState.forwardOptionsState) let panelFrame = CGRect(x: 0.0, y: layout.size.height - (textPanelHeight ?? 0.0) - size.height, width: size.width, height: size.height) - + accessoryHeight = size.height if forwardAccessoryPanelNode.frame.width.isZero { var initialPanelFrame = panelFrame @@ -1091,7 +1104,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } transition.updateFrame(node: forwardAccessoryPanelNode, frame: panelFrame) } - + if let countPanelNode = self.countPanelNode { let countPanelHeight = countPanelNode.updateLayout(width: layout.size.width, sideInset: layout.safeInsets.left, bottomInset: layout.intrinsicInsets.bottom, transition: transition) if countPanelNode.count == 0 { @@ -1106,11 +1119,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else { toolbarHeight += 44.0 } - + let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight - 10.0), size: CGSize(width: layout.size.width, height: toolbarHeight + 10.0)) transition.updateFrame(view: bottomEdgeEffectView, frame: edgeEffectFrame) bottomEdgeEffectView.update(content: self.presentationData.theme.list.plainBackgroundColor, blur: true, alpha: 0.3, rect: edgeEffectFrame, edge: .bottom, edgeSize: min(30.0, edgeEffectFrame.height), transition: ComponentTransition(transition)) - + let controlSize = segmentedControl.update( transition: ComponentTransition(transition), component: AnyComponent(SegmentControlComponent( @@ -1130,7 +1143,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) } - + self.indexChanged(index) } )), @@ -1146,28 +1159,28 @@ final class PeerSelectionControllerNode: ASDisplayNode { transition.updateFrame(view: segmentedControlView, frame: segmentedControlFrame) } } - + insets.top += navigationBarHeight insets.bottom = max(insets.bottom, toolbarHeight) insets.left += layout.safeInsets.left insets.right += layout.safeInsets.right - + var headerInsets = layout.insets(options: [.input]) headerInsets.top += actualNavigationBarHeight headerInsets.bottom = max(headerInsets.bottom, cleanInsets.bottom) headerInsets.left += layout.safeInsets.left headerInsets.right += layout.safeInsets.right - + if let chatListNode = self.chatListNode { chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) } - + if let mainContainerNode = self.mainContainerNode { transition.updateFrame(node: mainContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) mainContainerNode.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: actualNavigationBarHeight, originalNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: navigationBarHeight, insets: insets, isReorderingFilters: false, isEditing: false, inlineNavigationLocation: nil, inlineNavigationTransitionFraction: 0.0, storiesInset: 0.0, transition: transition) } - + if let requestPeerTypes = self.requestPeerType, let requestPeerType = requestPeerTypes.first { if self.isEmpty { self.chatListNode?.isHidden = true @@ -1175,7 +1188,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.requirementsTextNode?.isHidden = true self.requirementsSeparatorNode?.isHidden = true self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) - + var emptyTitle: String var emptyText: String var emptyButtonText: String @@ -1217,14 +1230,14 @@ final class PeerSelectionControllerNode: ASDisplayNode { emptyText = "" emptyButtonText = "" } - + self.emptyTitleNode.attributedText = NSAttributedString(string: emptyTitle, font: Font.semibold(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) self.emptyTextNode.attributedText = NSAttributedString(string: emptyText, font: Font.regular(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) - + let padding: CGFloat = 44.0 let emptyTitleSize = self.emptyTitleNode.updateLayout(CGSize(width: layout.size.width - insets.left * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) let emptyTextSize = self.emptyTextNode.updateLayout(CGSize(width: layout.size.width - insets.left * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) - + let emptyAnimationHeight = self.emptyAnimationSize.height let emptyAnimationSpacing: CGFloat = 12.0 let emptyTextSpacing: CGFloat = 17.0 @@ -1236,7 +1249,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + emptyButtonSpacing + emptyButtonHeight let emptyAnimationY = floorToScreenPixels((layout.size.height - emptyTotalHeight) / 2.0) - + if !emptyButtonText.isEmpty { let buttonPadding: CGFloat = 30.0 self.emptyButtonNode.title = emptyButtonText @@ -1247,13 +1260,13 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else { self.emptyButtonNode.isHidden = true } - + let textTransition = ContainedViewLayoutTransition.immediate textTransition.updateFrame(node: self.emptyAnimationNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - self.emptyAnimationSize.width) / 2.0), y: emptyAnimationY), size: self.emptyAnimationSize)) textTransition.updateFrame(node: self.emptyTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyTitleSize.width) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize)) textTransition.updateFrame(node: self.emptyTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyTextSize.width) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) self.emptyAnimationNode.updateLayout(size: self.emptyAnimationSize) - + self.emptyAnimationNode.isHidden = false self.emptyTitleNode.isHidden = false self.emptyTextNode.isHidden = false @@ -1261,54 +1274,54 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else if let requirementsBackgroundNode = self.requirementsBackgroundNode, let requirementsSeparatorNode = self.requirementsSeparatorNode, let requirementsTextNode = self.requirementsTextNode, let requirementsText = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: true) { let requirements = NSMutableAttributedString(string: self.presentationData.strings.RequestPeer_Requirements + "\n", font: Font.semibold(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) requirements.append(NSAttributedString(string: requirementsText, font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)) - + requirementsTextNode.attributedText = requirements let sideInset: CGFloat = 16.0 let verticalInset: CGFloat = 11.0 let requirementsSize = requirementsTextNode.updateLayout(CGSize(width: layout.size.width - insets.left - insets.right - sideInset * 2.0, height: .greatestFiniteMagnitude)) - + let requirementsBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: actualNavigationBarHeight), size: CGSize(width: layout.size.width, height: requirementsSize.height + verticalInset * 2.0)) insets.top += requirementsBackgroundFrame.height - + requirementsBackgroundNode.update(size: requirementsBackgroundFrame.size, transition: transition) transition.updateFrame(node: requirementsBackgroundNode, frame: requirementsBackgroundFrame) - + transition.updateFrame(node: requirementsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: requirementsBackgroundFrame.maxY - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel))) - + requirementsTextNode.frame = CGRect(origin: CGPoint(x: insets.left + sideInset, y: requirementsBackgroundFrame.minY + verticalInset), size: requirementsSize) } } - + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve) - + if let chatListNode = self.chatListNode { chatListNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: updateSizeAndInsets.insets.top, originalTopInset: updateSizeAndInsets.insets.top, storiesInset: 0.0, inlineNavigationLocation: nil, inlineNavigationTransitionFraction: 0.0) } - + if let contactListNode = self.contactListNode { contactListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) contactListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - + contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, storiesInset: 0.0, transition: transition) } - + if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } } - + func activateSearch(placeholderNode: SearchBarPlaceholderNode) { guard let (containerLayout, navigationBarHeight, _) = self.containerLayout, let navigationBar = self.navigationBar else { return } - + self.navigationBar?.setSecondaryContentNode(nil, animated: true) - + if self.chatListNode?.supernode != nil || self.mainContainerNode?.supernode != nil { self.chatListNode?.accessibilityElementsHidden = true self.mainContainerNode?.accessibilityElementsHidden = true - + let chatListLocation: ChatListControllerLocation if let (forumPeerId, isMonoforum) = self.forumPeerId { if isMonoforum { @@ -1319,7 +1332,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } else { chatListLocation = .chatList(groupId: EngineChatList.Group(.root)) } - + self.searchDisplayController = SearchDisplayController( presentationData: self.presentationData, contentNode: ChatListSearchContainerNode( @@ -1330,6 +1343,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 @@ -1338,14 +1352,14 @@ final class PeerSelectionControllerNode: ASDisplayNode { } var updated = false var count = 0 - + let chatListNode: ChatListNode? if let mainContainerNode = strongSelf.mainContainerNode { chatListNode = mainContainerNode.currentItemNode } else { chatListNode = strongSelf.chatListNode } - + chatListNode?.updateState { state in if state.editing { updated = true @@ -1416,7 +1430,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } }, fieldStyle: placeholderNode.fieldStyle ) - + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in if let strongSelf = self, let strongPlaceholderNode = placeholderNode { @@ -1427,10 +1441,10 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } }, placeholder: placeholderNode) - + } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { contactListNode.accessibilityElementsHidden = true - + var categories: ContactsSearchCategories = [.cloudContacts] if self.hasGlobalSearch { categories.insert(.global) @@ -1462,7 +1476,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { return nil } } - + if updated { strongSelf.textInputPanelNode?.updateSendButtonEnabled(count > 0, animated: true) strongSelf.requestDeactivateSearch?() @@ -1490,7 +1504,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { requestDeactivateSearch() } }, fieldStyle: placeholderNode.fieldStyle) - + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in if let strongSelf = self, let strongPlaceholderNode = placeholderNode { @@ -1503,30 +1517,30 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, placeholder: placeholderNode) } } - + func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) { if let searchDisplayController = self.searchDisplayController { if self.chatListNode?.supernode != nil || self.mainContainerNode?.supernode != nil { self.chatListNode?.accessibilityElementsHidden = false self.mainContainerNode?.accessibilityElementsHidden = false - + if let controller = self.controller, controller.chatListFiltersNonEmpty { self.navigationBar?.setSecondaryContentNode(self.controller?.tabContainerNode, animated: true) } self.controller?.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) - + searchDisplayController.deactivate(placeholder: placeholderNode) self.searchDisplayController = nil } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { contactListNode.accessibilityElementsHidden = false - + self.controller?.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) searchDisplayController.deactivate(placeholder: placeholderNode) self.searchDisplayController = nil } } } - + func scrollToTop() { if self.mainContainerNode?.supernode != nil { self.mainContainerNode?.scrollToTop(animated: true, adjustForTempInset: false) @@ -1536,7 +1550,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { //contactListNode.scrollToTop() } } - + private func indexChanged(_ index: Int) { let contactListActive = index == 1 if contactListActive != self.contactListActive { @@ -1553,7 +1567,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } self.recursivelyEnsureDisplaySynchronously(true) contactListNode.enableUpdates = true - + if let (layout, _, _) = self.containerLayout { self.controller?.containerLayoutUpdated(layout, transition: .immediate) } @@ -1572,7 +1586,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { contactListNode.openPeer = { [weak self] peer, _, _, _ in if case let .peer(peer, _, _) = peer { self?.contactListNode?.listNode.clearHighlightAnimated(true) - self?.requestOpenPeer?(EnginePeer(peer), nil) + self?.requestOpenPeer?(peer, nil) } } contactListNode.openDisabledPeer = { [weak self] peer, reason in @@ -1596,14 +1610,14 @@ final class PeerSelectionControllerNode: ASDisplayNode { strongSelf.contentOffsetChanged?(offset) } } - + contactListNode.contentScrollingEnded = { [weak self] listView in return self?.contentScrollingEnded?(listView) ?? false } - + if let (layout, navigationHeight, actualNavigationHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, actualNavigationBarHeight: actualNavigationHeight, transition: .immediate) - + let _ = (contactListNode.ready |> deliverOnMainQueue).start(next: { [weak self] _ in if let strongSelf = self { strongSelf.navigationBar?.setSecondaryContentNode(nil, animated: false) @@ -1617,7 +1631,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { } } strongSelf.recursivelyEnsureDisplaySynchronously(true) - + if let (layout, _, _) = strongSelf.containerLayout { strongSelf.controller?.containerLayoutUpdated(layout, transition: .immediate) } @@ -1633,7 +1647,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { mainContainerNode.removeFromSupernode() } self.recursivelyEnsureDisplaySynchronously(true) - + if let (layout, _, _) = self.containerLayout { self.controller?.containerLayoutUpdated(layout, transition: .immediate) } @@ -1644,7 +1658,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.navigationBar?.setSecondaryContentNode(self.controller?.tabContainerNode, animated: false) } contactListNode.enableUpdates = false - + if let mainContainerNode = self.mainContainerNode { self.insertSubnode(mainContainerNode, aboveSubnode: contactListNode) } @@ -1652,7 +1666,7 @@ final class PeerSelectionControllerNode: ASDisplayNode { self.insertSubnode(chatListNode, aboveSubnode: contactListNode) } contactListNode.removeFromSupernode() - + if let (layout, _, _) = self.containerLayout { self.controller?.containerLayoutUpdated(layout, transition: .immediate) } @@ -1666,7 +1680,7 @@ public func stringForAdminRights(strings: PresentationStrings, adminRights: Tele func append(_ string: String) { rights.append("• \(string)") } - + if isChannel { if adminRights.rights.contains(.canChangeInfo) { append(strings.RequestPeer_Requirement_Channel_Rights_Info) @@ -1742,7 +1756,7 @@ public func stringForAdminRights(strings: PresentationStrings, adminRights: Tele private func stringForRequestPeerType(strings: PresentationStrings, peerType: ReplyMarkupButtonRequestPeerType, offset: Bool) -> String? { var lines: [String] = [] - + func append(_ string: String) { if offset { lines.append(" • \(string)") @@ -1750,7 +1764,7 @@ private func stringForRequestPeerType(strings: PresentationStrings, peerType: Re lines.append("• \(string)") } } - + switch peerType { case let .user(user): if let isPremium = user.isPremium { @@ -1883,25 +1897,25 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent let controller: ViewController weak var sourceView: UIView? let sourceRect: CGRect? - + let navigationController: NavigationController? = nil - + let passthroughTouches: Bool - + init(controller: ViewController, sourceNode: ASDisplayNode?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { self.controller = controller self.sourceView = sourceNode?.view self.sourceRect = sourceRect self.passthroughTouches = passthroughTouches } - + init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { self.controller = controller self.sourceView = sourceView self.sourceRect = sourceRect self.passthroughTouches = passthroughTouches } - + func transitionInfo() -> ContextControllerTakeControllerInfo? { let sourceView = self.sourceView let sourceRect = self.sourceRect @@ -1913,7 +1927,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent } }) } - + func animatedIn() { } } @@ -1921,60 +1935,60 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent private final class PeersCountPanelNode: ASDisplayNode { private let theme: PresentationTheme private let strings: PresentationStrings - + private let separatorNode: ASDisplayNode private let button: SolidRoundedButtonNode - + private var validLayout: (CGFloat, CGFloat, CGFloat)? - + var buttonTitle: String = "" var count: Int = 0 { didSet { if self.count != oldValue && self.count > 0 { self.button.title = self.buttonTitle self.button.badge = "\(self.count)" - + if let (width, sideInset, bottomInset) = self.validLayout { let _ = self.updateLayout(width: width, sideInset: sideInset, bottomInset: bottomInset, transition: .immediate) } } } } - + init(theme: PresentationTheme, strings: PresentationStrings, action: @escaping () -> Void) { self.theme = theme self.strings = strings self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = theme.rootController.navigationBar.separatorColor - + self.button = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 48.0, cornerRadius: 10.0) - + super.init() - + self.backgroundColor = theme.rootController.navigationBar.opaqueBackgroundColor - + self.addSubnode(self.button) self.addSubnode(self.separatorNode) - + self.button.pressed = { action() } } - + func updateLayout(width: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { self.validLayout = (width, sideInset, bottomInset) let topInset: CGFloat = 9.0 var bottomInset = bottomInset bottomInset += topInset - (bottomInset.isZero ? 0.0 : 4.0) - + let buttonInset: CGFloat = 16.0 + sideInset let buttonWidth = width - buttonInset * 2.0 let buttonHeight = self.button.updateLayout(width: buttonWidth, transition: transition) transition.updateFrame(node: self.button, frame: CGRect(x: buttonInset, y: topInset, width: buttonWidth, height: buttonHeight)) - + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) - + return topInset + buttonHeight + bottomInset } } diff --git a/submodules/TelegramUI/Components/PollStatsScreen/BUILD b/submodules/TelegramUI/Components/PollStatsScreen/BUILD new file mode 100644 index 0000000000..6cd172cdc4 --- /dev/null +++ b/submodules/TelegramUI/Components/PollStatsScreen/BUILD @@ -0,0 +1,32 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PollStatsScreen", + module_name = "PollStatsScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/SheetComponent", + "//submodules/ItemListUI", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/StatisticsUI", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/PollStatsScreen/Sources/PollStatsScreen.swift b/submodules/TelegramUI/Components/PollStatsScreen/Sources/PollStatsScreen.swift new file mode 100644 index 0000000000..03be0edc4b --- /dev/null +++ b/submodules/TelegramUI/Components/PollStatsScreen/Sources/PollStatsScreen.swift @@ -0,0 +1,342 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import TelegramCore +import AccountContext +import TelegramPresentationData +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import BundleIconComponent +import GlassBarButtonComponent +import ListSectionComponent +import ListItemComponentAdaptor +import StatisticsUI +import ItemListUI + +private final class PollStatsSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let messageId: EngineMessage.Id + let animateOut: ActionSlot> + let getController: () -> ViewController? + + init( + context: AccountContext, + messageId: EngineMessage.Id, + animateOut: ActionSlot>, + getController: @escaping () -> ViewController? + ) { + self.context = context + self.messageId = messageId + self.animateOut = animateOut + self.getController = getController + } + + static func ==(lhs: PollStatsSheetContent, rhs: PollStatsSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.messageId != rhs.messageId { + return false + } + return true + } + + final class State: ComponentState { + let pollStatsContext: PollStatsContext + + private let animateOut: ActionSlot> + private let getController: () -> ViewController? + private let stateDisposable = MetaDisposable() + + var stats: PollStats? + private var loadedVotesGraphToken: String? + + init( + context: AccountContext, + messageId: EngineMessage.Id, + animateOut: ActionSlot>, + getController: @escaping () -> ViewController? + ) { + self.pollStatsContext = PollStatsContext(account: context.account, messageId: messageId) + self.animateOut = animateOut + self.getController = getController + + super.init() + + self.stateDisposable.set((self.pollStatsContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + self.stats = state.stats + if let stats = state.stats, case let .OnDemand(token) = stats.votesGraph, !token.isEmpty, self.loadedVotesGraphToken != token { + self.loadedVotesGraphToken = token + self.pollStatsContext.loadVotesGraph() + } + self.updated(transition: .immediate) + })) + } + + deinit { + self.stateDisposable.dispose() + } + + func dismiss(animated: Bool) { + guard let controller = self.getController() else { + return + } + if animated { + self.animateOut.invoke(Action { [weak controller] _ in + controller?.dismiss(completion: nil) + }) + } else { + controller.dismiss(animated: false) + } + } + } + + func makeState() -> State { + return State( + context: self.context, + messageId: self.messageId, + animateOut: self.animateOut, + getController: self.getController + ) + } + + static var body: Body { + let closeButton = Child(GlassBarButtonComponent.self) + let title = Child(MultilineTextComponent.self) + let section = Child(ListSectionComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self].value + let component = context.component + let state = context.state + + let theme = environment.theme + let strings = environment.strings + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let sideInset: CGFloat = 16.0 + let sectionWidth = context.availableSize.width - sideInset * 2.0 + let graph = state.stats?.votesGraph ?? .OnDemand(token: "") + + var contentSize = CGSize(width: context.availableSize.width, height: 16.0) + + let closeButton = closeButton.update( + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + state.dismiss(animated: true) + } + ), + availableSize: CGSize(width: 44.0, height: 44.0), + transition: .immediate + ) + context.add(closeButton.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: contentSize.height + closeButton.size.height / 2.0))) + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.PollStats_Title, + font: Font.semibold(17.0), + textColor: theme.actionSheet.primaryTextColor + )), + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - 120.0, height: 44.0), + transition: .immediate + ) + context.add(title.position(CGPoint(x: context.availableSize.width / 2.0, y: 16.0 + 22.0))) + contentSize.height += 44.0 + contentSize.height += 19.0 + + let section = section.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.PollStats_GraphHeader.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: StatsGraphItem( + presentationData: ItemListPresentationData(presentationData), + systemStyle: .glass, + graph: graph, + type: .lines5Min, + getDetailsData: { date, completion in + let _ = state.pollStatsContext.loadDetailedGraph(graph, x: Int64(date.timeIntervalSince1970) * 1000).start(next: { detailedGraph in + if let detailedGraph, case let .Loaded(_, data) = detailedGraph { + completion(data) + } else { + completion(nil) + } + }) + }, + sectionId: 0, + style: .blocks + ), + params: ListViewItemLayoutParams( + width: sectionWidth, + leftInset: 0.0, + rightInset: 0.0, + availableHeight: 10000.0, + isStandalone: true + ) + ))), + ], + displaySeparators: false + ), + availableSize: CGSize(width: sectionWidth, height: context.availableSize.height), + transition: context.transition + ) + context.add(section.position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + section.size.height / 2.0))) + contentSize.height += section.size.height + contentSize.height += 39.0 + + return contentSize + } + } +} + +private final class PollStatsSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let messageId: EngineMessage.Id + + init( + context: AccountContext, + messageId: EngineMessage.Id + ) { + self.context = context + self.messageId = messageId + } + + static func ==(lhs: PollStatsSheetComponent, rhs: PollStatsSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.messageId != rhs.messageId { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let dismiss: (Bool) -> Void = { animated in + guard let controller = controller() else { + return + } + if animated { + animateOut.invoke(Action { [weak controller] _ in + controller?.dismiss(completion: nil) + }) + } else { + controller.dismiss(completion: nil) + } + } + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(PollStatsSheetContent( + context: context.component.context, + messageId: context.component.messageId, + animateOut: animateOut, + getController: controller + )), + style: .glass, + backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + autoAnimateOut: false, + externalState: sheetExternalState, + animateOut: animateOut, + onPan: {}, + willDismiss: {} + ), + environment: { + environment + SheetComponentEnvironment( + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + dismiss(animated) + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))) + + return context.availableSize + } + } +} + +public final class PollStatsScreen: ViewControllerComponentContainer { + public init( + context: AccountContext, + messageId: EngineMessage.Id + ) { + super.init( + context: context, + component: PollStatsSheetComponent( + context: context, + messageId: messageId + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + self.automaticallyControlPresentationContextLayout = false + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } +} diff --git a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift index d0a78d1da8..a5ac5752eb 100644 --- a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift @@ -394,7 +394,7 @@ public final class GiftAvatarComponent: Component { self.mergedAvatarsNode = mergedAvatarsNode } - mergedAvatarsNode.update(context: component.context, peers: Array(component.peers.map { $0._asPeer() }.prefix(3)), synchronousLoad: false, imageSize: avatarSize.width, imageSpacing: 30.0, borderWidth: 2.0, avatarFontSize: 26.0) + mergedAvatarsNode.update(context: component.context, peers: Array(component.peers.prefix(3)), synchronousLoad: false, imageSize: avatarSize.width, imageSpacing: 30.0, borderWidth: 2.0, avatarFontSize: 26.0) let avatarsSize = CGSize(width: avatarSize.width + 30.0 * CGFloat(min(3, component.peers.count) - 1), height: avatarSize.height) mergedAvatarsNode.updateLayout(size: avatarsSize) mergedAvatarsNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarsSize.width) / 2.0), y: 113.0 - avatarSize.height / 2.0), size: avatarsSize) diff --git a/submodules/TelegramUI/Components/PremiumAlertController/BUILD b/submodules/TelegramUI/Components/PremiumAlertController/BUILD index 25d0e52f0d..6e388d0d47 100644 --- a/submodules/TelegramUI/Components/PremiumAlertController/BUILD +++ b/submodules/TelegramUI/Components/PremiumAlertController/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/PremiumAlertController/Sources/PremiumAlertController.swift b/submodules/TelegramUI/Components/PremiumAlertController/Sources/PremiumAlertController.swift index 001ca763bb..6cf81be6df 100644 --- a/submodules/TelegramUI/Components/PremiumAlertController/Sources/PremiumAlertController.swift +++ b/submodules/TelegramUI/Components/PremiumAlertController/Sources/PremiumAlertController.swift @@ -3,7 +3,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import AccountContext diff --git a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD index 69caa55031..58b2aaff5f 100644 --- a/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD +++ b/submodules/TelegramUI/Components/PremiumGiftAttachmentScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/AccountContext", diff --git a/submodules/TelegramUI/Components/ProxyServerPreviewScreen/BUILD b/submodules/TelegramUI/Components/ProxyServerPreviewScreen/BUILD index a74ad1276b..e1f2a486e7 100644 --- a/submodules/TelegramUI/Components/ProxyServerPreviewScreen/BUILD +++ b/submodules/TelegramUI/Components/ProxyServerPreviewScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/BUILD b/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/BUILD index 8606deae33..04c2c1a374 100644 --- a/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/BUILD +++ b/submodules/TelegramUI/Components/SavedMessages/SavedMessagesScreen/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit", "//submodules/Display", "//submodules/TelegramCore", - "//submodules/Postbox", "//submodules/TelegramPresentationData", "//submodules/AccountContext", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD b/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD index e95817f52a..5dd601dc55 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/BUILD b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/BUILD index 0f7c19aaa1..b8ba016275 100644 --- a/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/AccountFreezeInfoScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/BUILD b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/BUILD index 4d6f471d37..1c9cc083ac 100644 --- a/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/ArchiveInfoScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift index e1f7fd4b1f..0c8224d4f7 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift @@ -6,7 +6,6 @@ import ListSectionComponent import TelegramPresentationData import AppBundle import AccountContext -import Postbox import TelegramCore import TextNodeWithEntities import MultilineTextComponent 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/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index f9a8a6cdf4..355728c104 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -1064,7 +1064,9 @@ final class QuickReplySetupScreenComponent: Component { let timingFunction: String switch curve { case .easeInOut: - timingFunction = CAMediaTimingFunctionName.easeOut.rawValue + timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue + case .easeIn: + timingFunction = CAMediaTimingFunctionName.easeIn.rawValue case .linear: timingFunction = CAMediaTimingFunctionName.linear.rawValue case .spring: diff --git a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/BUILD b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/BUILD index d025efe999..6bedd804ae 100644 --- a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/BUILD index dfe7c400e0..737e18eef2 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift index 6266befb8c..1bba33ac1f 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift index 6eecea9155..1eafa17d0a 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessHoursSetupScreen.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift index 683f23d097..e19b2c5d51 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/BusinessIntroSetupScreen.swift @@ -56,6 +56,7 @@ final class BusinessIntroSetupScreenComponent: Component { var id: AnyHashable var version: Int var isPreset: Bool + var canLoadMore: Bool } private struct EmojiSearchState { @@ -100,6 +101,7 @@ final class BusinessIntroSetupScreenComponent: Component { private var stickerContent: EmojiPagerContentComponent? private var stickerContentDisposable: Disposable? private let stickerSearchDisposable = MetaDisposable() + private var stickerSearchContext: StickerSearchContext? private var stickerSearchState = EmojiSearchState(result: nil, isSearching: false) private var displayStickerInput: Bool = false @@ -309,6 +311,7 @@ final class BusinessIntroSetupScreenComponent: Component { self.stickerFile = itemFile._parse() self.displayStickerInput = false + self.stickerSearchContext = nil self.stickerSearchDisposable.set(nil) self.stickerSearchState = EmojiSearchState(result: nil, isSearching: false) @@ -351,6 +354,7 @@ final class BusinessIntroSetupScreenComponent: Component { switch query { case .none: + self.stickerSearchContext = nil self.stickerSearchDisposable.set(nil) self.stickerSearchState = EmojiSearchState(result: nil, isSearching: false) if !self.isUpdating { @@ -360,54 +364,49 @@ final class BusinessIntroSetupScreenComponent: Component { let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) if query.isEmpty { + self.stickerSearchContext = nil self.stickerSearchDisposable.set(nil) self.stickerSearchState = EmojiSearchState(result: nil, isSearching: false) self.state?.updated(transition: .immediate) } else { let context = component.context + self.stickerSearchContext = nil - let stickers: Signal<[(String?, FoundStickerItem)], NoError> = Signal { subscriber in - var signals: Signal<[Signal<(String?, [FoundStickerItem]), NoError>], NoError> = .single([]) - - if query.isSingleEmoji { - signals = .single([context.engine.stickers.searchStickers(query: nil, emoticon: [query.basicEmoji.0]) - |> map { (nil, $0.items) }]) - } else if query.count > 1, !languageCode.isEmpty && languageCode != "emoji" { - var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3) - if !languageCode.lowercased().hasPrefix("en") { - signal = signal - |> mapToSignal { keywords in - return .single(keywords) - |> then( - context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query.lowercased(), completeMatch: query.count < 3) - |> map { englishKeywords in - return keywords + englishKeywords - } - ) - } - } - signals = signal - |> map { keywords -> [Signal<(String?, [FoundStickerItem]), NoError>] in - let emoticon = keywords.flatMap { $0.emoticons }.map { $0.basicEmoji.0 } - return [context.engine.stickers.searchStickers(query: query, emoticon: emoticon, inputLanguageCode: languageCode) - |> map { (nil, $0.items) }] + let stickers: Signal<(items: [(String?, FoundStickerItem)], canLoadMore: Bool, isSearching: Bool, searchContext: StickerSearchContext?), NoError> + if query.isSingleEmoji { + let searchContext = context.engine.stickers.stickerSearchContext(query: nil, emoticon: [query.basicEmoji.0]) + stickers = searchContext.state + |> map { state -> (items: [(String?, FoundStickerItem)], canLoadMore: Bool, isSearching: Bool, searchContext: StickerSearchContext?) in + return (state.items.map { (nil, $0) }, state.canLoadMore, state.items.isEmpty && state.isLoadingMore, searchContext) + } + } else if query.count > 1, !languageCode.isEmpty && languageCode != "emoji" { + var keywordsSignal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3) + if !languageCode.lowercased().hasPrefix("en") { + keywordsSignal = keywordsSignal + |> mapToSignal { keywords in + return .single(keywords) + |> then( + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query.lowercased(), completeMatch: query.count < 3) + |> map { englishKeywords in + return keywords + englishKeywords + } + ) } } - - return (signals - |> mapToSignal { signals in - return combineLatest(signals) - }).start(next: { results in - var result: [(String?, FoundStickerItem)] = [] - for (emoji, stickers) in results { - for sticker in stickers { - result.append((emoji, sticker)) - } + stickers = keywordsSignal + |> mapToSignal { keywords -> Signal<(items: [(String?, FoundStickerItem)], canLoadMore: Bool, isSearching: Bool, searchContext: StickerSearchContext?), NoError> in + let emoticon = Array(Set(keywords.flatMap { $0.emoticons }.map { $0.basicEmoji.0 })) + guard !emoticon.isEmpty else { + return .single(([], false, false, nil)) } - subscriber.putNext(result) - }, completed: { - subscriber.putCompletion() - }) + let searchContext = context.engine.stickers.stickerSearchContext(query: query, emoticon: emoticon, inputLanguageCode: languageCode) + return searchContext.state + |> map { state -> (items: [(String?, FoundStickerItem)], canLoadMore: Bool, isSearching: Bool, searchContext: StickerSearchContext?) in + return (state.items.map { (nil, $0) }, state.canLoadMore, state.items.isEmpty && state.isLoadingMore, searchContext) + } + } + } else { + stickers = .single(([], false, false, nil)) } let currentRemotePacks = Atomic(value: nil) @@ -467,17 +466,10 @@ final class BusinessIntroSetupScreenComponent: Component { } let signal = combineLatest(stickers, packs) - |> map { stickers, packs -> ([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)? in - return (stickers, packs.0, packs.1, packs.2) - } - - let resultSignal: Signal<[EmojiPagerContentComponent.ItemGroup], NoError> = signal - |> mapToSignal { result in - guard let result else { - return .complete() - } - - let (foundItems, localSets, complete, remoteSets) = result + |> map { stickers, packs -> (groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: StickerSearchContext?) in + let foundItems = stickers.items + let localSets = packs.0 + let remoteSets = packs.2 var items: [EmojiPagerContentComponent.Item] = [] @@ -529,11 +521,7 @@ final class BusinessIntroSetupScreenComponent: Component { items.append(item) } - if items.isEmpty && !complete { - return .complete() - } - - return .single([EmojiPagerContentComponent.ItemGroup( + let groups = [EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, @@ -550,26 +538,29 @@ final class BusinessIntroSetupScreenComponent: Component { headerItem: nil, fillWithLoadingPlaceholders: false, items: items - )]) + )] + return (groups, stickers.canLoadMore, stickers.isSearching, stickers.searchContext) } var version = 0 self.stickerSearchState.isSearching = true self.state?.updated(transition: .immediate) - self.stickerSearchDisposable.set((resultSignal + self.stickerSearchDisposable.set((signal |> delay(0.15, queue: .mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] result in guard let self else { return } - self.stickerSearchState = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + self.stickerSearchContext = result.searchContext + self.stickerSearchState = EmojiSearchState(result: EmojiSearchResult(groups: result.groups, id: AnyHashable(query), version: version, isPreset: false, canLoadMore: result.canLoadMore), isSearching: result.isSearching) version += 1 self.state?.updated(transition: .immediate) })) } case let .category(value): + self.stickerSearchContext = nil let resultSignal = component.context.engine.stickers.searchStickers(category: value, scope: [.installed, .remote]) |> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -642,13 +633,13 @@ final class BusinessIntroSetupScreenComponent: Component { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) if !self.isUpdating { self.state?.updated(transition: .immediate) } return } - self.stickerSearchState = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + self.stickerSearchState = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) version += 1 if !self.isUpdating { self.state?.updated(transition: .immediate) @@ -659,6 +650,9 @@ final class BusinessIntroSetupScreenComponent: Component { updateScrollingToItemGroup: { }, onScroll: {}, + loadMore: { [weak self] in + self?.stickerSearchContext?.loadMore() + }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, @@ -1036,14 +1030,14 @@ final class BusinessIntroSetupScreenComponent: Component { if let stickerSearchResult = self.stickerSearchState.result { var stickerSearchResults: EmojiPagerContentComponent.EmptySearchResults? - if !stickerSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { + if !self.stickerSearchState.isSearching && !stickerSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { stickerSearchResults = EmojiPagerContentComponent.EmptySearchResults( text: environment.strings.Stickers_NoStickersFound, iconFile: nil ) } let defaultSearchState: EmojiPagerContentComponent.SearchState = stickerSearchResult.isPreset ? .active : .empty(hasResults: true) - stickerContent = stickerContent.withUpdatedItemGroups(panelItemGroups: stickerContent.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: self.stickerSearchState.isSearching ? .searching : defaultSearchState) + stickerContent = stickerContent.withUpdatedItemGroups(panelItemGroups: stickerContent.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: self.stickerSearchState.isSearching ? .searching : defaultSearchState, canLoadMore: stickerSearchResult.canLoadMore) } else if self.stickerSearchState.isSearching { stickerContent = stickerContent.withUpdatedItemGroups(panelItemGroups: stickerContent.panelItemGroups, contentItemGroups: stickerContent.contentItemGroups, itemContentUniqueId: stickerContent.itemContentUniqueId, emptySearchResults: stickerContent.emptySearchResults, searchState: .searching) } diff --git a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift index e1a568e5da..0674297168 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift @@ -122,7 +122,6 @@ final class ChatIntroItemComponent: Component { mode: .standard(.default), chatLocation: .peer(id: component.context.account.peerId), subject: nil, - peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, diff --git a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD index dd67a3b410..4772e4f748 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD +++ b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift index 8a531ac3a3..23e0f9387c 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLinkNameAlertController/Sources/BusinessLinkNameAlertController.swift @@ -3,7 +3,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import AccountContext diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/BUILD index 83cfb9c9c7..5bde7cdc39 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/BUILD @@ -11,7 +11,6 @@ swift_library( ], deps = [ "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift index 6f0654f867..82307c7d53 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/BusinessLocationSetupScreen.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD index 210ac682a5..ed399a9fc6 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/BUILD @@ -36,6 +36,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/TelegramUI/Components/AlertComponent", "//submodules/ShimmerEffect", + "//submodules/UndoUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift index bbe37bf01b..ecda61297b 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift @@ -208,7 +208,11 @@ final class ChatbotSearchResultItemComponent: Component { case let .found(peer, _): isTextVisible = true titleValue = peer.displayTitle(strings: component.strings, displayOrder: .firstLast) - subtitleValue = component.strings.Bot_GenericBotStatus + if let addressName = peer.addressName { + subtitleValue = "@\(addressName)" + } else { + subtitleValue = component.strings.Bot_GenericBotStatus + } } let titleSize = self.titleLabel.update( diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index f6807785f5..c387c29fa6 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -15,6 +15,7 @@ import ViewControllerComponent import MultilineTextComponent import BalancedTextComponent import BackButtonComponent +import EdgeEffect import ListSectionComponent import ListActionItemComponent import ListTextFieldItemComponent @@ -24,6 +25,7 @@ import Markdown import PeerListItemComponent import AvatarNode import AlertComponent +import UndoUI private let checkIcon: UIImage = { return generateImage(CGSize(width: 12.0, height: 10.0), rotatedContext: { size, context in @@ -144,6 +146,7 @@ final class ChatbotSetupScreenComponent: Component { private let scrollView: ScrollView private let navigationTitle = ComponentView() + private let titleTransformContainer: UIView private let icon = ComponentView() private let subtitle = ComponentView() private let nameSection = ComponentView() @@ -190,6 +193,9 @@ final class ChatbotSetupScreenComponent: Component { } self.scrollView.alwaysBounceVertical = true + self.titleTransformContainer = UIView() + self.titleTransformContainer.isUserInteractionEnabled = false + super.init(frame: frame) self.scrollView.delegate = self @@ -203,6 +209,7 @@ final class ChatbotSetupScreenComponent: Component { } deinit { + self.titleTransformContainer.removeFromSuperview() } func scrollToTop() { @@ -210,10 +217,21 @@ final class ChatbotSetupScreenComponent: Component { } func attemptNavigation(complete: @escaping () -> Void) -> Bool { - guard let component = self.component else { + guard let component = self.component, let environemnt = self.environment else { return true } + if let botResolutionState = self.botResolutionState, case let .found(_, isInstalled) = botResolutionState.state, !isInstalled { + let alertController = textAlertController(context: component.context, title: environemnt.strings.ChatbotSetup_SetupNotCompleted_Title, text: environemnt.strings.ChatbotSetup_SetupNotCompleted_Text, actions: [ + TextAlertAction(type: .defaultAction, title: environemnt.strings.Common_Cancel, action: {}), + TextAlertAction(type: .genericAction, title: environemnt.strings.ChatbotSetup_SetupNotCompleted_Leave, action: { + complete() + }) + ]) + environemnt.controller()?.present(alertController, in: .window(.root)) + return false + } + var mappedCategories: TelegramBusinessRecipients.Categories = [] if self.additionalPeerList.categories.contains(.existingChats) { mappedCategories.insert(.existingChats) @@ -251,33 +269,29 @@ final class ChatbotSetupScreenComponent: Component { self.updateScrolling(transition: .immediate) } - var scrolledUp = true private func updateScrolling(transition: ComponentTransition) { - let navigationRevealOffsetY: CGFloat = 0.0 - - let navigationAlphaDistance: CGFloat = 16.0 - let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) - if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { - transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) - transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + guard let environment = self.environment else { + return } - var scrolledUp = false - if navigationAlpha < 0.5 { - scrolledUp = true - } else if navigationAlpha > 0.5 { - scrolledUp = false - } + let titleCenterY: CGFloat = environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) * 0.5 + let titleTransformDistance: CGFloat = 20.0 + let titleY: CGFloat = max(titleCenterY, self.titleTransformContainer.center.y - self.scrollView.contentOffset.y) - if self.scrolledUp != scrolledUp { - self.scrolledUp = scrolledUp - if !self.isUpdating { - self.state?.updated() - } - } + transition.setSublayerTransform(view: self.titleTransformContainer, transform: CATransform3DMakeTranslation(0.0, titleY - self.titleTransformContainer.center.y, 0.0)) + let titleYDistance: CGFloat = titleY - titleCenterY + let titleTransformFraction: CGFloat = 1.0 - max(0.0, min(1.0, titleYDistance / titleTransformDistance)) + let titleMinScale: CGFloat = 17.0 / 24.0 + let titleScale: CGFloat = 1.0 * (1.0 - titleTransformFraction) + titleMinScale * titleTransformFraction if let navigationTitleView = self.navigationTitle.view { - transition.setAlpha(view: navigationTitleView, alpha: 1.0) + transition.setScale(view: navigationTitleView, scale: titleScale) + } + + if let controller = environment.controller(), let navigationBar = controller.navigationBar, let edgeEffectView = navigationBar.edgeEffectView { + let edgeEffectAlphaDistance = max(1.0, self.titleTransformContainer.center.y - titleCenterY) + let edgeEffectAlpha = max(0.0, min(1.0, self.scrollView.contentOffset.y / edgeEffectAlphaDistance)) + transition.setAlpha(view: edgeEffectView, alpha: edgeEffectAlpha) } } @@ -645,20 +659,41 @@ final class ChatbotSetupScreenComponent: Component { let navigationTitleSize = self.navigationTitle.update( transition: transition, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.ChatbotSetup_TitleItem, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + text: .plain(NSAttributedString(string: environment.strings.ChatbotSetup_TitleItem, font: Font.bold(24.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 100.0) ) - let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) - if let navigationTitleView = self.navigationTitle.view { - if navigationTitleView.superview == nil { - if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { - navigationBar.view.addSubview(navigationTitleView) + let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.navigationHeight + 76.0), size: navigationTitleSize) + let overlaySuperview: UIView? + if let controller = environment.controller(), let navigationBar = controller.navigationBar, let navigationBarSuperview = navigationBar.view.superview { + overlaySuperview = navigationBarSuperview + } else { + overlaySuperview = self + } + if let overlaySuperview { + if self.titleTransformContainer.superview !== overlaySuperview { + self.titleTransformContainer.removeFromSuperview() + if let controller = environment.controller(), let navigationBar = controller.navigationBar, overlaySuperview === navigationBar.view.superview { + overlaySuperview.insertSubview(self.titleTransformContainer, aboveSubview: navigationBar.view) + } else { + overlaySuperview.addSubview(self.titleTransformContainer) } } - transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) + } + if let navigationTitleView = self.navigationTitle.view { + if navigationTitleView.superview !== self.titleTransformContainer { + navigationTitleView.removeFromSuperview() + self.titleTransformContainer.addSubview(navigationTitleView) + } + transition.setPosition(view: self.titleTransformContainer, position: navigationTitleFrame.center) + transition.setBounds(view: self.titleTransformContainer, bounds: CGRect(origin: CGPoint(), size: navigationTitleFrame.size)) + transition.setBounds(view: navigationTitleView, bounds: CGRect(origin: CGPoint(), size: navigationTitleFrame.size)) + transition.setPosition(view: navigationTitleView, position: CGPoint( + x: navigationTitleFrame.size.width * 0.5, + y: navigationTitleFrame.size.height * 0.5 + )) } let bottomContentInset: CGFloat = 24.0 @@ -675,13 +710,13 @@ final class ChatbotSetupScreenComponent: Component { let iconSize = self.icon.update( transition: .immediate, component: AnyComponent(LottieComponent( - content: LottieComponent.AppBundleContent(name: "BotEmoji"), + content: LottieComponent.AppBundleContent(name: "ChatAutomation"), loop: false )), environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) + containerSize: CGSize(width: 140.0, height: 140.0) ) - let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 8.0), size: iconSize) + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight - 50.0), size: iconSize) if let iconView = self.icon.view as? LottieComponent.View { if iconView.superview == nil { self.scrollView.addSubview(iconView) @@ -691,11 +726,11 @@ final class ChatbotSetupScreenComponent: Component { iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size) } - contentHeight += 129.0 + contentHeight += 115.0 let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.ChatbotSetup_Text, attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.freeTextColor), - bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.freeTextColor), + body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor), link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemAccentColor), linkAttribute: { attributes in return ("URL", "") @@ -732,7 +767,7 @@ final class ChatbotSetupScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 64.0, height: 1000.0) ) let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: contentHeight), size: subtitleSize) if let subtitleView = self.subtitle.view { @@ -780,7 +815,7 @@ final class ChatbotSetupScreenComponent: Component { strings: environment.strings, content: mappedContent, installAction: { [weak self] in - guard let self else { + guard let self, let component = self.component, let environment = self.environment, let controller = self.environment?.controller() else { return } self.endEditing(true) @@ -797,6 +832,14 @@ final class ChatbotSetupScreenComponent: Component { }) ]), in: .window(.root)) } + controller.present(UndoOverlayController( + presentationData: presentationData, + content: .invitedToVoiceChat(context: component.context, peer: peer, title: nil, text: environment.strings.ChatbotSetup_BotInstalled(peer.compactDisplayTitle).string, action: nil, duration: 2.0), + elevatedLayout: false, + position: .bottom, + animateInAsReplacement: false, + action: { _ in return true } + ), in: .current) } }, removeAction: { [weak self] in diff --git a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/BUILD b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/BUILD index 142a36bbaa..ae1ca4f927 100644 --- a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift index e563f3385a..12946d256d 100644 --- a/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/LanguageSelectionScreen/Sources/LanguageSelectionScreen.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import SwiftSignalKit import TelegramCore import TelegramPresentationData diff --git a/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/BUILD b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/BUILD index 2fdef7364d..2fc20a7fe0 100644 --- a/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/NewSessionInfoScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD b/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD index 38a7d38057..6d5ffb2ae0 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD @@ -11,7 +11,6 @@ swift_library( ], deps = [ "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift index dd7f756419..a857c63436 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/PeerNameColorProfilePreviewItem.swift @@ -4,7 +4,6 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore -import Postbox import TelegramPresentationData import TelegramUIPreferences import ItemListUI diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift index 0cb04d686f..d0fa4a8c28 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift @@ -522,7 +522,9 @@ final class PeerSelectionScreenComponent: Component { let timingFunction: String switch curve { case .easeInOut: - timingFunction = CAMediaTimingFunctionName.easeOut.rawValue + timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue + case .easeIn: + timingFunction = CAMediaTimingFunctionName.easeIn.rawValue case .linear: timingFunction = CAMediaTimingFunctionName.linear.rawValue case .spring: diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift index a155b1c6f0..c9e6afe3e3 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/ItemListReactionItem.swift @@ -4,7 +4,6 @@ import Display import AsyncDisplayKit import SwiftSignalKit import TelegramPresentationData -import Postbox import TelegramCore import ItemListUI import EmojiStatusComponent diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/QuickReactionSetupController.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/QuickReactionSetupController.swift index 697a6a5d6e..38cbafd692 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/QuickReactionSetupController.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/QuickReactionSetupController.swift @@ -245,10 +245,10 @@ public func quickReactionSetupController( } ) - let settings = context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings]) + let settings = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.reactionSettings)) |> map { preferencesView -> ReactionSettings in let reactionSettings: ReactionSettings - if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) { + if let entry = preferencesView, let value = entry.get(ReactionSettings.self) { reactionSettings = value } else { reactionSettings = .default diff --git a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD index 99a8b1bbc7..63ff3980c5 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD +++ b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift index b78cc739f1..466a1b8bd8 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift @@ -3,7 +3,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import AccountContext @@ -50,7 +49,7 @@ public func quickReplyNameAlertController(context: AccountContext, updatedPresen component: AnyComponent( AlertInputFieldComponent( context: context, - initialValue: nil, + initialValue: value, placeholder: strings.QuickReply_ShortcutPlaceholder, characterLimit: characterLimit, hasClearButton: false, @@ -59,6 +58,21 @@ public func quickReplyNameAlertController(context: AccountContext, updatedPresen autocorrectionType: .no, isInitiallyFocused: true, externalState: inputState, + shouldChangeText: { updatedText in + if updatedText.isEmpty { + return true + } + for scalar in updatedText.unicodeScalars { + if scalar.value == 0x5f || scalar.value == 0x200c || scalar.value == 0xb7 || (scalar.value >= 0xd80 && scalar.value <= 0xdff) { + continue + } + if CharacterSet.letters.contains(scalar) || CharacterSet.decimalDigits.contains(scalar) { + continue + } + return false + } + return true + }, returnKeyAction: { applyImpl?() } diff --git a/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/BUILD b/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/BUILD index d6fd0655ff..7f23c3678e 100644 --- a/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/BUILD +++ b/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/Display", "//submodules/AsyncDisplayKit", "//submodules/SSignalKit/SwiftSignalKit", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/TelegramPresentationData", "//submodules/GradientBackground", diff --git a/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/Sources/SettingsThemeWallpaperNode.swift b/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/Sources/SettingsThemeWallpaperNode.swift index 6af509eee1..5756cf369e 100644 --- a/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/Sources/SettingsThemeWallpaperNode.swift +++ b/submodules/TelegramUI/Components/Settings/SettingsThemeWallpaperNode/Sources/SettingsThemeWallpaperNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import TelegramCore -import Postbox import SwiftSignalKit import TelegramPresentationData import AccountContext @@ -282,8 +281,8 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode { } let anyStatus = combineLatest(queue: .mainQueue(), - context.account.postbox.mediaBox.resourceStatus(convertedFullRepresentations[0].reference.resource, approximateSynchronousValue: true), - context.sharedContext.accountManager.mediaBox.resourceStatus(convertedFullRepresentations[0].reference.resource, approximateSynchronousValue: true) + context.engine.resources.status(resource: EngineMediaResource(convertedFullRepresentations[0].reference.resource), approximateSynchronousValue: true), + context.sharedContext.accountManager.resources.status(resource: EngineMediaResource(convertedFullRepresentations[0].reference.resource), approximateSynchronousValue: true) ) |> map { a, b -> Bool in switch a { @@ -368,7 +367,7 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode { } self.animatedStickerNode = animatedStickerNode self.emojiContainerNode.addSubnode(animatedStickerNode) - let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let pathPrefix = context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.resource.id)) animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix)) animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) @@ -376,7 +375,7 @@ public final class SettingsThemeWallpaperNode: ASDisplayNode { animatedStickerNode.autoplay = true animatedStickerNode.visibility = true - self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + self.stickerFetchedDisposable.set(context.engine.resources.fetch(reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource), userLocation: .other, userContentType: .other).start()) // let thumbnailDimensions = PixelDimensions(width: 512, height: 512) // self.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: thumbnailDimensions.cgSize) diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift index 762ea62da1..3acf431bae 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorController.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import Display -import Postbox import SwiftSignalKit import AsyncDisplayKit import TelegramCore @@ -187,14 +186,14 @@ public final class ThemeAccentColorController: ViewController { let resource = file.file.resource var data: Data? - if let path = strongSelf.context.account.postbox.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { data = maybeData - } else if let path = strongSelf.context.sharedContext.accountManager.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { data = maybeData } - + if let data = data { - strongSelf.context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) prepareWallpaper = .complete() } else { prepareWallpaper = .complete() diff --git a/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/BUILD b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/BUILD index 7e7c767224..24d0726a8a 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/BUILD +++ b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/Display", "//submodules/AsyncDisplayKit", "//submodules/SSignalKit/SwiftSignalKit", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/TelegramPresentationData", "//submodules/MergeLists", diff --git a/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift index c3fda9e89b..ed537cfedb 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeCarouselItem/Sources/ThemeCarouselItem.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import MergeLists @@ -462,7 +461,7 @@ private final class ThemeCarouselThemeItemIconNode: ListViewItemNode { } strongSelf.animatedStickerNode = animatedStickerNode strongSelf.emojiContainerNode.insertSubnode(animatedStickerNode, belowSubnode: strongSelf.placeholderNode) - let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + let pathPrefix = item.context.engine.resources.shortLivedResourceCachePathPrefix(id: EngineMediaResource.Id(file.resource.id)) animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix)) animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0) @@ -470,7 +469,7 @@ private final class ThemeCarouselThemeItemIconNode: ListViewItemNode { animatedStickerNode.autoplay = true animatedStickerNode.visibility = strongSelf.visibilityStatus - strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start()) + strongSelf.stickerFetchedDisposable.set(item.context.engine.resources.fetch(reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource), userLocation: .other, userContentType: .sticker).start()) let thumbnailDimensions = PixelDimensions(width: 512, height: 512) strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: thumbnailDimensions.cgSize) diff --git a/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/BUILD b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/BUILD index f0350af4f8..1c8a5b72df 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/BUILD +++ b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/Display", "//submodules/AsyncDisplayKit", "//submodules/SSignalKit/SwiftSignalKit", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/TelegramPresentationData", "//submodules/MergeLists", diff --git a/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift index 4b8a806e25..fe727d90ef 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeSettingsThemeItem/Sources/ThemeSettingsThemeItem.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import MergeLists diff --git a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/BUILD b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/BUILD index 38fdb23587..e3713f6ab9 100644 --- a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/Display", "//submodules/AsyncDisplayKit", - "//submodules/Postbox", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/TelegramCore", "//submodules/TelegramPresentationData", diff --git a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreen.swift index 28aee7a2b2..28ae629e05 100644 --- a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreen.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import SwiftSignalKit import TelegramCore import TelegramPresentationData diff --git a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift index f67d1f3b13..8ae722c397 100644 --- a/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift +++ b/submodules/TelegramUI/Components/Settings/TimezoneSelectionScreen/Sources/TimezoneSelectionScreenNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryController.swift index 9062918881..d77f6325ba 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 @@ -216,7 +215,7 @@ public class WallpaperGalleryController: ViewController { private var previousCentralEntryIndex: Int? private let centralItemSubtitle = Promise() - private let centralItemStatus = Promise() + private let centralItemStatus = Promise() private let centralItemAction = Promise() private let centralItemAttributesDisposable = DisposableSet(); @@ -566,14 +565,14 @@ public class WallpaperGalleryController: ViewController { if let resource = resource { let representation = CachedBlurredWallpaperRepresentation() var data: Data? - if let path = strongSelf.context.account.postbox.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { data = maybeData - } else if let path = strongSelf.context.sharedContext.accountManager.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { data = maybeData } if let data = data { - strongSelf.context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) let _ = (strongSelf.context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true) |> filter({ $0.complete }) |> take(1) @@ -655,14 +654,14 @@ public class WallpaperGalleryController: ViewController { if let resource = resource { let representation = CachedBlurredWallpaperRepresentation() var data: Data? - if let path = strongSelf.context.account.postbox.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { data = maybeData - } else if let path = strongSelf.context.sharedContext.accountManager.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { data = maybeData } if let data = data { - strongSelf.context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) let _ = (strongSelf.context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: representation, complete: true, fetch: true) |> filter({ $0.complete }) |> take(1) @@ -675,31 +674,31 @@ public class WallpaperGalleryController: ViewController { if wallpaper.isPattern, !file.settings.colors.isEmpty, let _ = file.settings.intensity { var data: Data? var thumbnailData: Data? - if let path = strongSelf.context.account.postbox.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { data = maybeData - } else if let path = strongSelf.context.sharedContext.accountManager.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { data = maybeData } let thumbnailResource = file.file.previewRepresentations.first?.resource if let resource = thumbnailResource { - if let path = strongSelf.context.account.postbox.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { thumbnailData = maybeData - } else if let path = strongSelf.context.sharedContext.accountManager.mediaBox.completedResourcePath(resource), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + } else if let path = strongSelf.context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(resource)), let maybeData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { thumbnailData = maybeData } } if let data = data { - strongSelf.context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) if let thumbnailResource = thumbnailResource, let thumbnailData = thumbnailData { - strongSelf.context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(thumbnailResource.id), data: thumbnailData, synchronous: true) } completion(wallpaper) } - } else if let path = strongSelf.context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - strongSelf.context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data) + } else if let path = strongSelf.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.file.resource.id)), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: data) completion(wallpaper) } } else { @@ -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/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift index 7bd2d9037d..247e0f75eb 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGalleryScreen/Sources/WallpaperGalleryItem.swift @@ -131,7 +131,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { private let colorDisposable = MetaDisposable() let subtitle = Promise(nil) - let status = Promise(.Local) + let status = Promise(.Local) let actionButton = Promise(nil) var action: (() -> Void)? var requestPatternPanel: ((Bool, TelegramWallpaper) -> Void)? @@ -615,7 +615,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> let fetchSignal: Signal - let statusSignal: Signal + let statusSignal: Signal let subtitleSignal: Signal var actionSignal: Signal = .single(nil) var colorSignal: Signal = serviceColor(from: imagePromise.get()) @@ -794,15 +794,15 @@ final class WallpaperGalleryItemNode: GalleryItemNode { } signal = wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: fileReference, representations: convertedRepresentations, alwaysShowThumbnailFirst: true, autoFetchFullSize: false) } - fetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: convertedRepresentations[convertedRepresentations.count - 1].reference) - let account = self.context.account - statusSignal = self.context.sharedContext.accountManager.mediaBox.resourceStatus(file.file.resource) + fetchSignal = context.engine.resources.fetch(reference: convertedRepresentations[convertedRepresentations.count - 1].reference, userLocation: .other, userContentType: .other) + let engine = self.context.engine + statusSignal = self.context.sharedContext.accountManager.resources.status(resource: EngineMediaResource(file.file.resource)) |> take(1) - |> mapToSignal { status -> Signal in + |> mapToSignal { status -> Signal in if case .Local = status { return .single(status) } else { - return account.postbox.mediaBox.resourceStatus(file.file.resource) + return engine.resources.status(resource: EngineMediaResource(file.file.resource)) } } if let fileSize = file.file.size { @@ -826,18 +826,18 @@ final class WallpaperGalleryItemNode: GalleryItemNode { signal = wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, alwaysShowThumbnailFirst: true, autoFetchFullSize: false) if let largestIndex = convertedRepresentations.firstIndex(where: { $0.representation == largestSize }) { - fetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: convertedRepresentations[largestIndex].reference) + fetchSignal = context.engine.resources.fetch(reference: convertedRepresentations[largestIndex].reference, userLocation: .other, userContentType: .other) } else { fetchSignal = .complete() } - let account = context.account - statusSignal = context.sharedContext.accountManager.mediaBox.resourceStatus(largestSize.resource) + let engine = context.engine + statusSignal = context.sharedContext.accountManager.resources.status(resource: EngineMediaResource(largestSize.resource)) |> take(1) - |> mapToSignal { status -> Signal in + |> mapToSignal { status -> Signal in if case .Local = status { return .single(status) } else { - return account.postbox.mediaBox.resourceStatus(largestSize.resource) + return engine.resources.status(resource: EngineMediaResource(largestSize.resource)) } } if let fileSize = largestSize.resource.size { @@ -926,8 +926,8 @@ final class WallpaperGalleryItemNode: GalleryItemNode { let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) signal = chatMessagePhoto(postbox: context.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage)) - fetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .media(media: .standalone(media: tmpImage), resource: imageResource)) - statusSignal = context.account.postbox.mediaBox.resourceStatus(imageResource) + fetchSignal = context.engine.resources.fetch(reference: .media(media: .standalone(media: tmpImage), resource: imageResource), userLocation: .other, userContentType: .other) + statusSignal = context.engine.resources.status(resource: EngineMediaResource(imageResource)) } else { displaySize = CGSize(width: 1.0, height: 1.0) contentSize = displaySize @@ -986,7 +986,7 @@ final class WallpaperGalleryItemNode: GalleryItemNode { if case .asset = entry, let image, let data = image.jpegData(compressionQuality: 0.5) { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - strongSelf.context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + strongSelf.context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) let wallpaper: TelegramWallpaper = .image([TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], WallpaperSettings()) strongSelf.nativeNode.update(wallpaper: wallpaper, animated: false) diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift index 0871ba3e2e..6f7d3abdad 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeColorsGridController.swift @@ -108,7 +108,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina public enum Mode { case `default` case peer(EnginePeer) - + var galleryMode: WallpaperGalleryController.Mode { switch self { case .default: @@ -117,7 +117,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina return .peer(peer, false) } } - + var colorPickerMode: ThemeAccentColorController.ResultMode { switch self { case .default: @@ -127,61 +127,61 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina } } } - + private var controllerNode: ThemeColorsGridControllerNode { return self.displayNode as! ThemeColorsGridControllerNode } - + private let _ready = Promise() public override var ready: Promise { return self._ready } - + private let context: AccountContext let mode: Mode - + private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - + private var validLayout: ContainerViewLayout? - + private var previousContentOffset: GridNodeVisibleContentOffset? - + fileprivate var mainButtonState: AttachmentMainButtonState? - + var pushController: (ViewController) -> Void = { _ in } var dismissControllers: (() -> Void)? var openGallery: (() -> Void)? - + public init(context: AccountContext, mode: Mode = .default, canDelete: Bool = false) { self.context = context self.mode = mode self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass)) - + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - + self.scrollToTop = { [weak self] in if let strongSelf = self { strongSelf.controllerNode.scrollToTop() } } - + self.presentationDataDisposable = (context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme let previousStrings = strongSelf.presentationData.strings - + strongSelf.presentationData = presentationData - + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { strongSelf.updateThemeAndStrings() } } }) - + if case .peer = mode { self.title = self.presentationData.strings.Conversation_Theme_ChooseColorTitle self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) @@ -189,40 +189,40 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina } else { self.title = self.presentationData.strings.WallpaperColors_Title } - + self.pushController = { [weak self] controller in self?.push(controller) } - + self.mainButtonState = AttachmentMainButtonState(text: self.presentationData.strings.Conversation_Theme_SetPhotoWallpaper, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false) } - + required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + deinit { self.presentationDataDisposable?.dispose() } - + @objc private func cancelPressed() { self.dismiss() } - + @objc private func customPressed() { self.presentColorPicker() } - + private func updateThemeAndStrings() { self.title = self.presentationData.strings.WallpaperColors_Title self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData, style: .glass), transition: .immediate) - + if self.isNodeLoaded { self.controllerNode.updatePresentationData(self.presentationData) } } - + private func presentColorPicker() { let _ = (self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationThemeSettings]) |> take(1) @@ -231,7 +231,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina return } let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings - + let autoNightModeTriggered = strongSelf.presentationData.autoNightModeTriggered let themeReference: PresentationThemeReference if autoNightModeTriggered { @@ -239,7 +239,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina } else { themeReference = settings.theme } - + let controller = ThemeAccentColorController(context: strongSelf.context, mode: .background(themeReference: themeReference), resultMode: strongSelf.mode.colorPickerMode) controller.completion = { [weak self, weak controller] in if let strongSelf = self { @@ -268,7 +268,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina strongSelf.pushController(controller) }) } - + public override func loadDisplayNode() { var dark = false if case .default = self.mode { @@ -285,7 +285,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina strongSelf.presentColorPicker() } }) - + let transitionOffset: CGFloat switch self.mode { case .default: @@ -293,7 +293,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina case .peer: transitionOffset = 2.0 } - + self.controllerNode.gridNode.visibleContentOffsetChanged = { [weak self] offset in if let strongSelf = self { var previousContentOffsetValue: CGFloat? @@ -312,29 +312,29 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina case .unknown, .none: strongSelf.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate) } - + strongSelf.previousContentOffset = offset } } - + self._ready.set(self.controllerNode.ready.get()) - + self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) - + self.displayNodeDidLoad() } - + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } - + @objc fileprivate func mainButtonPressed() { self.dismiss(animated: true) self.openGallery?() } - + public var requestAttachmentMenuExpansion: () -> Void = {} public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } public var parentController: () -> ViewController? = { @@ -346,7 +346,7 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } public var isMinimized: Bool = false - + public var mediaPickerContext: AttachmentMediaPickerContext? { return ThemeColorsGridContext(controller: self) } @@ -354,15 +354,15 @@ public final class ThemeColorsGridController: ViewController, AttachmentContaina private final class ThemeColorsGridContext: AttachmentMediaPickerContext { private weak var controller: ThemeColorsGridController? - + public var mainButtonState: Signal { return .single(self.controller?.mainButtonState) } - + init(controller: ThemeColorsGridController) { self.controller = controller } - + func mainButtonAction() { self.controller?.mainButtonPressed() } @@ -376,9 +376,7 @@ public func standaloneColorPickerController( push: @escaping (ViewController) -> Void, openGallery: @escaping () -> Void ) -> ViewController { - let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { - return nil - }) + let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false) controller.requestController = { _, present in let colorPickerController = ThemeColorsGridController(context: context, mode: .peer(peer)) colorPickerController.pushController = { controller in diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift index 55d0c49b87..b7d86b3f0f 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import LegacyComponents @@ -10,7 +9,6 @@ import TelegramPresentationData import TelegramUIPreferences import OverlayStatusController import AccountContext -import ShareController import SearchUI import HexColor import PresentationDataUtils @@ -466,7 +464,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/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift index c65730aeb2..13f0696dca 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerItem.swift @@ -4,7 +4,6 @@ import Display import TelegramCore import SwiftSignalKit import AsyncDisplayKit -import Postbox import AccountContext import GridMessageSelectionNode import SettingsThemeWallpaperNode diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift index 5d59dd666e..c316cecebb 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridControllerNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import UniversalMediaPlayer diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift index 842c90037f..e3d5a34364 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSearchColorsItem.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import AsyncDisplayKit -import Postbox import Display import SwiftSignalKit import TelegramCore diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSelectionPanelNode.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSelectionPanelNode.swift index 856ae1ae6e..3430c4001a 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSelectionPanelNode.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridSelectionPanelNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperUtils.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperUtils.swift index fafe52e6e9..04af498c4c 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperUtils.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/WallpaperUtils.swift @@ -18,16 +18,16 @@ public func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperG case let .wallpaper(wallpaper, _): switch wallpaper { case let .file(file): - if let path = context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data) + if let path = context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.file.resource.id)), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: data) let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start() } case let .image(representations, _): for representation in representations { let resource = representation.resource - if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data) + if let path = context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() } } @@ -64,7 +64,7 @@ public func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperG } if let imageResource = imageResource { - imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource)) + imageSignal = .single(context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(imageResource.id))) |> mapToSignal { path -> Signal in if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) { return .single(image) @@ -100,12 +100,12 @@ public func uploadCustomWallpaper(context: AccountContext, wallpaper: WallpaperG if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) - context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(thumbnailResource.id), data: thumbnailData, synchronous: true) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(thumbnailResource.id), data: thumbnailData, synchronous: true) let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) let autoNightModeTriggered = context.sharedContext.currentPresentationData.with {$0 }.autoNightModeTriggered let accountManager = context.sharedContext.accountManager @@ -198,7 +198,7 @@ public func getTemporaryCustomPeerWallpaper(context: AccountContext, wallpaper: } if let imageResource = imageResource { - imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource)) + imageSignal = .single(context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(imageResource.id))) |> mapToSignal { path -> Signal in if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) { return .single(image) @@ -234,12 +234,12 @@ public func getTemporaryCustomPeerWallpaper(context: AccountContext, wallpaper: if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) - context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(thumbnailResource.id), data: thumbnailData, synchronous: true) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(thumbnailResource.id), data: thumbnailData, synchronous: true) let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start() @@ -264,8 +264,8 @@ public func uploadCustomPeerWallpaper(context: AccountContext, wallpaper: Wallpa imageSignal = .complete() switch wallpaper { case let .file(file): - if let path = context.account.postbox.mediaBox.completedResourcePath(file.file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead), let image = UIImage(data: data) { - context.sharedContext.accountManager.mediaBox.storeResourceData(file.file.resource.id, data: data, synchronous: true) + if let path = context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.file.resource.id)), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead), let image = UIImage(data: data) { + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: data, synchronous: true) let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(file.file.resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start() @@ -274,8 +274,8 @@ public func uploadCustomPeerWallpaper(context: AccountContext, wallpaper: Wallpa case let .image(representations, _): for representation in representations { let resource = representation.resource - if let path = context.account.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + if let path = context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(resource.id)), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 720.0, height: 720.0), mode: .aspectFit), complete: true, fetch: true).start() } } @@ -311,7 +311,7 @@ public func uploadCustomPeerWallpaper(context: AccountContext, wallpaper: Wallpa } if let imageResource = imageResource { - imageSignal = .single(context.account.postbox.mediaBox.completedResourcePath(imageResource)) + imageSignal = .single(context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(imageResource.id))) |> mapToSignal { path -> Signal in if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let image = UIImage(data: data) { return .single(image) @@ -347,12 +347,12 @@ public func uploadCustomPeerWallpaper(context: AccountContext, wallpaper: Wallpa if let data = croppedImage.jpegData(compressionQuality: 0.8), let thumbnailImage = thumbnailImage, let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.4) { let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.sharedContext.accountManager.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) - context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData, synchronous: true) + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(thumbnailResource.id), data: thumbnailData, synchronous: true) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(thumbnailResource.id), data: thumbnailData, synchronous: true) let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) let _ = context.sharedContext.accountManager.mediaBox.cachedResourceRepresentation(resource, representation: CachedBlurredWallpaperRepresentation(), complete: true, fetch: true).start() diff --git a/submodules/TelegramUI/Components/ShareExtensionContext/BUILD b/submodules/TelegramUI/Components/ShareExtensionContext/BUILD index becdf07725..1b1e2be9d1 100644 --- a/submodules/TelegramUI/Components/ShareExtensionContext/BUILD +++ b/submodules/TelegramUI/Components/ShareExtensionContext/BUILD @@ -33,12 +33,16 @@ swift_library( "//submodules/ActivityIndicator", "//submodules/DebugSettingsUI", "//submodules/ManagedFile", + "//submodules/ContextUI", "//submodules/TelegramUI/Components/TelegramUIDeclareEncodables", "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", + "//submodules/TelegramUI/Components/DCTAnimationCacheImpl:DCTAnimationCacheImpl", + "//submodules/TelegramUI/Components/DCTMultiAnimationRendererImpl:DCTMultiAnimationRendererImpl", "//submodules/TelegramUI/Components/TelegramAccountAuxiliaryMethods", "//submodules/TelegramUI/Components/PeerSelectionController", "//submodules/TelegramUI/Components/ContextMenuScreen", + "//submodules/TelegramUI/Components/ContextControllerImpl", "//submodules/TelegramUI/Components/NavigationBarImpl", ], visibility = [ diff --git a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift index 9d236de495..e7b05cc9f1 100644 --- a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift @@ -27,11 +27,15 @@ import ManagedFile import TelegramUIDeclareEncodables import AnimationCache import MultiAnimationRenderer +import DCTAnimationCacheImpl +import DCTMultiAnimationRendererImpl import TelegramUIDeclareEncodables import TelegramAccountAuxiliaryMethods import PeerSelectionController import ContextMenuScreen import NavigationBarImpl +import ContextUI +import ContextControllerImpl private var installedSharedLogger = false @@ -105,14 +109,14 @@ private final class ShareControllerAccountContextExtension: ShareControllerAccou self.stateManager = stateManager self.engineData = TelegramEngine.EngineData(accountPeerId: stateManager.accountPeerId, postbox: stateManager.postbox) let cacheStorageBox = stateManager.postbox.mediaBox.cacheStorageBox - self.animationCache = AnimationCacheImpl(basePath: stateManager.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + self.animationCache = DCTAnimationCacheImpl(basePath: stateManager.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { return TempBox.shared.tempFile(fileName: "file").path }, updateStorageStats: { path, size in if let pathData = path.data(using: .utf8) { cacheStorageBox.update(id: pathData, size: size) } }) - self.animationRenderer = MultiAnimationRendererImpl() + self.animationRenderer = DCTMultiAnimationRendererImpl() self.contentSettings = contentSettings self.appConfiguration = appConfiguration } @@ -199,6 +203,18 @@ public class ShareRootControllerImpl { defaultNavigationBarImpl = { presentationData in return NavigationBarImpl(presentationData: presentationData) } + makeContextControllerImpl = { context, presentationData, configuration, recognizer, gesture, workaroundUseLegacyImplementation, disableScreenshots, hideReactionPanelTail in + return ContextControllerImpl( + context: context, + presentationData: presentationData, + configuration: configuration, + recognizer: recognizer, + gesture: gesture, + workaroundUseLegacyImplementation: workaroundUseLegacyImplementation, + disableScreenshots: disableScreenshots, + hideReactionPanelTail: hideReactionPanelTail + ) + } } deinit { @@ -410,7 +426,7 @@ public class ShareRootControllerImpl { let accountData: Signal<(ShareControllerEnvironment, ShareControllerAccountContext, [ShareControllerSwitchableAccount]), NoError> = accountManager.accountRecords() |> take(1) |> mapToSignal { view -> Signal<(ShareControllerEnvironment, ShareControllerAccountContext, [ShareControllerSwitchableAccount]), NoError> in - var signals: [Signal<(AccountRecordId, AccountStateManager, Peer)?, NoError>] = [] + var signals: [Signal<(AccountRecordId, AccountStateManager, EnginePeer)?, NoError>] = [] for record in view.records { if record.attributes.contains(where: { attribute in if case .loggedOut = attribute { @@ -434,14 +450,14 @@ public class ShareRootControllerImpl { rootPath: rootPath, auxiliaryMethods: makeTelegramAccountAuxiliaryMethods(uploadInBackground: nil) ) - |> mapToSignal { result -> Signal<(AccountRecordId, AccountStateManager, Peer)?, NoError> in + |> mapToSignal { result -> Signal<(AccountRecordId, AccountStateManager, EnginePeer)?, NoError> in if let result { - return result.postbox.transaction { transaction -> (AccountRecordId, AccountStateManager, Peer)? in + return result.postbox.transaction { transaction -> (AccountRecordId, AccountStateManager, EnginePeer)? in guard let peer = transaction.getPeer(result.accountPeerId) else { return nil } - - return (record.id, result, peer) + + return (record.id, result, EnginePeer(peer)) } } else { return .single(nil) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift index ddb1aafd17..3c3ad2573a 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift @@ -9,7 +9,6 @@ import ComponentDisplayAdapters import TelegramPresentationData import AccountContext import TelegramCore -import Postbox import MultilineTextComponent import PresentationDataUtils import ButtonComponent @@ -19,6 +18,8 @@ import TelegramStringFormatting import LottieComponent import UndoUI import CountrySelectionUI +import GlassBarButtonComponent +import BundleIconComponent final class CountriesMultiselectionScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -239,8 +240,8 @@ final class CountriesMultiselectionScreenComponent: Component { self.navigationContainerView.addSubview(self.navigationBackgroundView) self.navigationContainerView.layer.addSublayer(self.navigationSeparatorLayer) - self.containerView.addSubview(self.bottomBackgroundView) - self.containerView.layer.addSublayer(self.bottomSeparatorLayer) + //self.containerView.addSubview(self.bottomBackgroundView) + //self.containerView.layer.addSublayer(self.bottomSeparatorLayer) self.containerView.addSubnode(self.indexNode) @@ -751,7 +752,7 @@ final class CountriesMultiselectionScreenComponent: Component { if let searchStateContext = self.searchStateContext, searchStateContext.subject == .countriesSearch(query: self.navigationTextFieldState.text) { } else { self.searchStateDisposable?.dispose() - let searchStateContext = CountriesMultiselectionScreen.StateContext(context: component.context, subject: .countriesSearch(query: self.navigationTextFieldState.text)) + let searchStateContext = CountriesMultiselectionScreen.StateContext(context: component.context, subject: .countriesSearch(query: self.navigationTextFieldState.text), showFragment: component.stateContext.showFragment) var applyState = false self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { @@ -788,7 +789,6 @@ final class CountriesMultiselectionScreenComponent: Component { var sections: [ItemLayout.Section] = [] if let stateValue = self.effectiveStateValue { - var id: Int = 0 for (_, countries) in stateValue.sections { sections.append(ItemLayout.Section( @@ -803,21 +803,30 @@ final class CountriesMultiselectionScreenComponent: Component { let containerInset: CGFloat = environment.statusBarHeight - var navigationHeight: CGFloat = 56.0 + var navigationHeight: CGFloat = 76.0 let navigationSideInset: CGFloat = 16.0 var navigationButtonsWidth: CGFloat = 0.0 let navigationLeftButtonSize = self.navigationLeftButton.update( transition: transition, - component: AnyComponent(Button( - content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), - action: { [weak self] in + component: AnyComponent(GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: environment.theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: environment.theme.chat.inputPanel.panelControlColor + ) + )), + action: { [weak self] _ in guard let self, let environment = self.environment, let controller = environment.controller() as? CountriesMultiselectionScreen else { return } controller.requestDismiss() } - ).minSize(CGSize(width: navigationHeight, height: navigationHeight))), + )), environment: {}, containerSize: CGSize(width: availableSize.width, height: navigationHeight) ) @@ -830,21 +839,25 @@ final class CountriesMultiselectionScreenComponent: Component { } navigationButtonsWidth += navigationLeftButtonSize.width + navigationSideInset - let actionButtonTitle = environment.strings.CountriesList_SaveCountries - let title = environment.strings.CountriesList_SelectCountries - let subtitle = environment.strings.CountriesList_SelectUpTo(component.context.userLimits.maxGiveawayCountriesCount) + let actionButtonTitle = environment.strings.CountriesList_SaveCountries + var titleComponents: [AnyComponentWithIdentity] = [ + AnyComponentWithIdentity( + id: "title", + component: AnyComponent(Text(text: environment.strings.CountriesList_SelectCountries, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)) + ) + ] - let titleComponent = AnyComponent( - List([ - AnyComponentWithIdentity( - id: "title", - component: AnyComponent(Text(text: title, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)) - ), + if let maxCount = component.stateContext.maxCount { + titleComponents.append( AnyComponentWithIdentity( id: "subtitle", - component: AnyComponent(Text(text: subtitle, font: Font.regular(13.0), color: environment.theme.rootController.navigationBar.secondaryTextColor)) + component: AnyComponent(Text(text: environment.strings.CountriesList_SelectUpTo(maxCount), font: Font.regular(13.0), color: environment.theme.rootController.navigationBar.secondaryTextColor)) ) - ], + ) + } + + let titleComponent = AnyComponent( + List(titleComponents, centerAlignment: true) ) @@ -884,10 +897,12 @@ final class CountriesMultiselectionScreenComponent: Component { let badge = self.selectedCountries.count + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let actionButtonSize = self.actionButton.update( transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) @@ -917,15 +932,15 @@ final class CountriesMultiselectionScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: containerWidth - navigationSideInset * 2.0, height: 50.0) + containerSize: CGSize(width: containerWidth - buttonInsets.left - buttonInsets.right, height: 52.0) ) - if environment.inputHeight != 0.0 { bottomPanelHeight += environment.inputHeight + 8.0 + actionButtonSize.height } else { - bottomPanelHeight += 10.0 + environment.safeInsets.bottom + actionButtonSize.height + bottomPanelHeight += actionButtonSize.height + bottomPanelHeight += buttonInsets.bottom } - let actionButtonFrame = CGRect(origin: CGPoint(x: containerSideInset + navigationSideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + let actionButtonFrame = CGRect(origin: CGPoint(x: containerSideInset + buttonInsets.left, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) if let actionButtonView = self.actionButton.view { if actionButtonView.superview == nil { self.containerView.addSubview(actionButtonView) @@ -1104,7 +1119,9 @@ public extension CountriesMultiselectionScreen { var stateValue: State? public let subject: Subject + public let maxCount: Int32? public let initialSelectedCountries: [String] + public let showFragment: Bool private var stateDisposable: Disposable? private let stateSubject = Promise() @@ -1120,15 +1137,23 @@ public extension CountriesMultiselectionScreen { public init( context: AccountContext, subject: Subject = .countries, - initialSelectedCountries: [String] = [] + maxCount: Int32? = nil, + initialSelectedCountries: [String] = [], + showFragment: Bool = false ) { self.subject = subject + self.maxCount = maxCount self.initialSelectedCountries = initialSelectedCountries + self.showFragment = showFragment let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let countries = localizedCountryNamesAndCodes(strings: presentationData.strings).sorted { lhs, rhs in + let countryList = localizedCountryNamesAndCodes(strings: presentationData.strings) + var countries = countryList.sorted { lhs, rhs in return lhs.0.1.lowercased() < rhs.0.1.lowercased() } + if showFragment, let index = countries.firstIndex(where: { $0.1 == "FR" }) { + countries.insert((("Fragment", "Fragment"), "FT", [888]), at: index) + } switch subject { case .countries: @@ -1137,8 +1162,8 @@ public extension CountriesMultiselectionScreen { var currentSection: String? var currentCountries: [CountryItem] = [] for country in countries { - let section = String(country.0.1.prefix(1)) - if currentSection != section { + let section = String(country.0.1.prefix(1)).uppercased() + if currentSection != section && country.1 != "FT" { if let currentSection { sections.append((currentSection, currentCountries)) } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 120b2b2f5c..32161ebabb 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -774,10 +774,10 @@ final class ShareWithPeersScreenComponent: Component { if item.peer.id == context.account.peerId { continue } - if let user = item.peer as? TelegramUser, user.botInfo != nil { + if case let .user(user) = item.peer, user.botInfo != nil { continue } - peers.append(EnginePeer(item.peer)) + peers.append(item.peer) } if !list.list.isEmpty { subscriber.putNext(peers) @@ -2765,7 +2765,7 @@ final class ShareWithPeersScreenComponent: Component { let navigationLeftButtonSize = self.navigationLeftButton.update( transition: transition, component: AnyComponent(GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), + size: CGSize(width: 44.0, height: 44.0), backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, isDark: environment.theme.overallDarkAppearance, state: .generic, diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift index 5bc9be69da..816ed17c06 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift @@ -555,7 +555,7 @@ public extension ShareWithPeersScreen { continue } - peers.append(EnginePeer(participant.peer)) + peers.append(participant.peer) existingPeersIds.insert(participant.peer.id) } @@ -563,7 +563,7 @@ public extension ShareWithPeersScreen { if participant.peer.isDeleted || existingPeersIds.contains(participant.peer.id) || participant.participant.adminInfo != nil { continue } - if let user = participant.peer as? TelegramUser, user.botInfo != nil { + if case let .user(user) = participant.peer, user.botInfo != nil { continue } @@ -573,7 +573,7 @@ public extension ShareWithPeersScreen { continue } - peers.append(EnginePeer(participant.peer)) + peers.append(participant.peer) } let state = State( diff --git a/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/BUILD b/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/BUILD index e75b7cea6e..40ecdc513c 100644 --- a/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/BUILD +++ b/submodules/TelegramUI/Components/Stars/ItemShimmeringLoadingComponent/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/ComponentFlow", "//submodules/AppBundle", "//submodules/Components/HierarchyTrackingLayer", diff --git a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD index 61bc2e9d45..1e5fadadde 100644 --- a/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsIntroScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index a7531e23af..a034ae91d7 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -277,8 +277,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { descriptionText = "" count = CurrencyAmount(amount: subscription.pricing.amount, currency: .stars) date = subscription.untilDate - if let creationDate = (subscription.peer._asPeer() as? TelegramChannel)?.creationDate, creationDate > 0 { - additionalDate = creationDate + if case let .channel(channel) = subscription.peer, channel.creationDate > 0 { + additionalDate = channel.creationDate } else { additionalDate = nil } @@ -1955,7 +1955,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { return } if isProfile { - if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { navigationController.pushViewController(controller) } } else { 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 } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 91d7256e8f..2dfbc35fd6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -1285,7 +1285,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { var hasLeft = false var isKicked = false - if let channel = subscription.peer._asPeer() as? TelegramChannel { + if case let .channel(channel) = subscription.peer { switch channel.participationStatus { case .left: hasLeft = true diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD index 06a63c3c2b..0b89fa7a6c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit", "//submodules/Display", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/SSignalKit/SwiftSignalKit", "//submodules/ComponentFlow", diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift index 4b3791a899..985e199f5c 100644 --- a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift +++ b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift @@ -482,6 +482,7 @@ public class StickerPickerScreen: ViewController { var id: AnyHashable var version: Int var isPreset: Bool + var canLoadMore: Bool } private struct EmojiSearchState { @@ -501,6 +502,7 @@ public class StickerPickerScreen: ViewController { self.emojiSearchState.set(.single(self.emojiSearchStateValue)) } } + private var emojiSearchContext: EmojiSearchContext? private let stickerSearchDisposable = MetaDisposable() private let stickerSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) @@ -691,7 +693,7 @@ public class StickerPickerScreen: ViewController { } let defaultSearchState: EmojiPagerContentComponent.SearchState = emojiSearchResult.isPreset ? .active : .empty(hasResults: true) - inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState) + inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState, canLoadMore: emojiSearchResult.canLoadMore) } else if emojiSearchState.isSearching { inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emoji.contentItemGroups, itemContentUniqueId: emoji.itemContentUniqueId, emptySearchResults: emoji.emptySearchResults, searchState: .searching) } @@ -958,7 +960,7 @@ public class StickerPickerScreen: ViewController { if installed { return .complete() } else { - return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) + return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) |> map { _ in return Void() } } case .fetching: break @@ -1044,15 +1046,18 @@ public class StickerPickerScreen: ViewController { switch query { case .none: + self.emojiSearchContext = nil self.emojiSearchDisposable.set(nil) - self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) case let .text(rawQuery, languageCode): let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) if query.isEmpty { + self.emojiSearchContext = nil self.emojiSearchDisposable.set(nil) - self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + self.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) } else { + self.emojiSearchContext = nil var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) if !languageCode.lowercased().hasPrefix("en") { signal = signal @@ -1072,24 +1077,25 @@ public class StickerPickerScreen: ViewController { signal, hasPremium ) - |> mapToSignal { keywords, hasPremium -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + |> mapToSignal { keywords, hasPremium -> Signal<(groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?), NoError> in var allEmoticons: [String: String] = [:] for keyword in keywords { for emoticon in keyword.emoticons { allEmoticons[emoticon] = keyword.keyword } } - let remoteSignal: Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError> + let remoteSignal: Signal + let emojiSearchContext: EmojiSearchContext? if hasPremium { - remoteSignal = context.engine.stickers.searchEmoji(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode) + let currentEmojiSearchContext = context.engine.stickers.emojiSearchContext(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode) + emojiSearchContext = currentEmojiSearchContext + remoteSignal = currentEmojiSearchContext.state } else { - remoteSignal = .single(([], true)) + emojiSearchContext = nil + remoteSignal = .single(EmojiSearchContext.State(items: [], canLoadMore: false, isLoadingMore: false)) } return remoteSignal - |> mapToSignal { foundEmoji -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in - if foundEmoji.items.isEmpty && !foundEmoji.isFinalResult { - return .complete() - } + |> map { foundEmoji -> (groups: [EmojiPagerContentComponent.ItemGroup], canLoadMore: Bool, isSearching: Bool, searchContext: EmojiSearchContext?) in var items: [EmojiPagerContentComponent.Item] = [] let appendUnicodeEmoji = { @@ -1139,7 +1145,7 @@ public class StickerPickerScreen: ViewController { appendUnicodeEmoji() } - return .single([EmojiPagerContentComponent.ItemGroup( + return ([EmojiPagerContentComponent.ItemGroup( supergroupId: "search", groupId: "search", title: nil, @@ -1156,7 +1162,7 @@ public class StickerPickerScreen: ViewController { headerItem: nil, fillWithLoadingPlaceholders: false, items: items - )]) + )], foundEmoji.canLoadMore, foundEmoji.items.isEmpty && foundEmoji.isLoadingMore, emojiSearchContext) } } @@ -1169,11 +1175,13 @@ public class StickerPickerScreen: ViewController { return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + self.emojiSearchContext = result.searchContext + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.groups, id: AnyHashable(query), version: version, isPreset: false, canLoadMore: result.canLoadMore), isSearching: result.isSearching) version += 1 })) } case let .category(value): + self.emojiSearchContext = nil let resultSignal = context.engine.stickers.searchEmoji(category: value) |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in var items: [EmojiPagerContentComponent.Item] = [] @@ -1246,10 +1254,10 @@ public class StickerPickerScreen: ViewController { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) return } - self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) version += 1 })) } @@ -1261,6 +1269,9 @@ public class StickerPickerScreen: ViewController { self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) }, onScroll: {}, + loadMore: { [weak self] in + self?.emojiSearchContext?.loadMore() + }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, @@ -1373,7 +1384,7 @@ public class StickerPickerScreen: ViewController { if installed { return .complete() } else { - return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) + return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items) |> map { _ in return Void() } } case .fetching: break @@ -1533,10 +1544,10 @@ public class StickerPickerScreen: ViewController { fillWithLoadingPlaceholders: true, items: [] ) - ], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + ], id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) return } - strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false) + strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true, canLoadMore: false), isSearching: false) version += 1 })) } diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD index d935fec442..e1a4e986f4 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD +++ b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD @@ -12,7 +12,6 @@ swift_library( deps = [ "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/ComponentFlow:ComponentFlow", @@ -46,6 +45,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/DataUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift index 7b5f78fb54..c6cf49f953 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift @@ -911,7 +911,7 @@ final class DataUsageScreenComponent: Component { self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(value: .modeChanged))) })), environment: {}, - containerSize: CGSize(width: availableSize.width - (environment.safeInsets.left + 16.0 + 44.0 + 10.0) * 2.0, height: 100.0) + containerSize: CGSize(width: availableSize.width - (environment.safeInsets.left + 36.0) * 2.0, height: 100.0) ) let segmentedControlFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - segmentedSize.width) * 0.5), y: contentHeight), size: segmentedSize) if let segmentedControlComponentView = self.segmentedControlView.view { diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift index 1b80afc5b6..f42ef91240 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift @@ -11,7 +11,6 @@ import AccountContext import TelegramCore import MultilineTextComponent import EmojiStatusComponent -import Postbox import TelegramStringFormatting import CheckNode import AvatarNode @@ -102,35 +101,22 @@ private let smallExtensionFont = Font.with(size: 12.0, design: .round, weight: . private final class FileListItemComponent: Component { enum Icon: Equatable { case fileExtension(String) - case media(Media, TelegramMediaImageRepresentation) + case mediaFile(TelegramMediaFile, TelegramMediaImageRepresentation) + case mediaImage(TelegramMediaImage, TelegramMediaImageRepresentation) case audio - + static func ==(lhs: Icon, rhs: Icon) -> Bool { - switch lhs { - case let .fileExtension(value): - if case .fileExtension(value) = rhs { - return true - } else { - return false - } - case let .media(media, representation): - if case let .media(rhsMedia, rhsRepresentation) = rhs { - if media.id != rhsMedia.id { - return false - } - if representation != rhsRepresentation { - return false - } - return true - } else { - return false - } - case .audio: - if case .audio = rhs { - return true - } else { - return false - } + switch (lhs, rhs) { + case let (.fileExtension(l), .fileExtension(r)): + return l == r + case let (.mediaFile(lFile, lRepresentation), .mediaFile(rFile, rRepresentation)): + return lFile.fileId == rFile.fileId && lRepresentation == rRepresentation + case let (.mediaImage(lImage, lRepresentation), .mediaImage(rImage, rRepresentation)): + return lImage.imageId == rImage.imageId && lRepresentation == rRepresentation + case (.audio, .audio): + return true + default: + return false } } } @@ -445,25 +431,34 @@ private final class FileListItemComponent: Component { } } - if case let .media(media, representation) = component.icon { + let mediaRepresentation: TelegramMediaImageRepresentation? + switch component.icon { + case let .mediaFile(_, representation), let .mediaImage(_, representation): + mediaRepresentation = representation + default: + mediaRepresentation = nil + } + + if let representation = mediaRepresentation { var resetImage = false - + let iconImageNode: TransformImageNode if let current = self.iconImageNode { iconImageNode = current } else { resetImage = true - + iconImageNode = TransformImageNode() self.iconImageNode = iconImageNode self.containerButton.addSubview(iconImageNode.view) } - + let iconSize = CGSize(width: 40.0, height: 40.0) let imageSize: CGSize = representation.dimensions.cgSize - + if resetImage { - if let file = media as? TelegramMediaFile { + switch component.icon { + case let .mediaFile(file, _): iconImageNode.setSignal(chatWebpageSnippetFile( account: component.context.account, userLocation: .peer(component.messageId.peerId), @@ -471,19 +466,21 @@ private final class FileListItemComponent: Component { representation: representation, automaticFetch: false )) - } else if let image = media as? TelegramMediaImage { + case let .mediaImage(image, _): iconImageNode.setSignal(mediaGridMessagePhoto( account: component.context.account, userLocation: .peer(component.messageId.peerId), photoReference: ImageMediaReference.standalone(media: image), automaticFetch: false )) + default: + break } } - + let iconFrame = CGRect(origin: CGPoint(x: iconLeftInset + floor((leftInset - iconLeftInset - iconSize.width) / 2.0), y: floor((height - verticalInset * 2.0 - iconSize.height) / 2.0)), size: iconSize) transition.setFrame(view: iconImageNode.view, frame: iconFrame) - + let iconImageLayout = iconImageNode.asyncLayout() let iconImageApply = iconImageLayout(TransformImageArguments( corners: ImageCorners(radius: 8.0), @@ -631,11 +628,11 @@ final class StorageFileListPanelComponent: Component { typealias EnvironmentType = StorageUsagePanelEnvironment final class Item: Equatable { - let message: Message + let message: EngineMessage let size: Int64 - + init( - message: Message, + message: EngineMessage, size: Int64 ) { self.message = message @@ -704,43 +701,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 +752,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 +803,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 @@ -937,14 +979,14 @@ final class StorageFileListPanelComponent: Component { extensionIconValue = fileExtension if let representation = smallestImageRepresentation(file.previewRepresentations) { - imageIconValue = .media(file, representation) + imageIconValue = .mediaFile(file, representation) } } } else if let image = media as? TelegramMediaImage { title = environment.strings.Message_Photo if let representation = largestImageRepresentation(image.representations) { - imageIconValue = .media(image, representation) + imageIconValue = .mediaImage(image, representation) } } } @@ -975,7 +1017,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,13 +1050,16 @@ 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, component: AnyComponent(FileListItemComponent( context: component.context, theme: environment.theme, - messageId: EngineMessage.Id(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(0), id: PeerId.Id._internalFromInt64Value(0)), namespace: 0, id: 0), + messageId: EngineMessage.Id(peerId: component.context.account.peerId, namespace: 0, id: 0), title: "ABCDEF", subtitle: "ABCDEF", label: "1000", @@ -1034,6 +1079,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 +1106,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..626fc02427 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -9,7 +9,6 @@ import ComponentDisplayAdapters import TelegramPresentationData import AccountContext import TelegramCore -import Postbox import MultilineTextComponent import EmojiStatusComponent import Markdown @@ -223,7 +222,7 @@ final class StorageUsageScreenComponent: Component { return true } - func togglePeer(id: EnginePeer.Id, availableMessages: [EngineMessage.Id: Message]) -> SelectionState { + func togglePeer(id: EnginePeer.Id, availableMessages: [EngineMessage.Id: EngineMessage]) -> SelectionState { var selectedPeers = self.selectedPeers var selectedMessages = self.selectedMessages @@ -351,32 +350,32 @@ final class StorageUsageScreenComponent: Component { let peerId: EnginePeer.Id? let stats: AllStorageUsageStats let contextStats: StorageUsageStats - let messages: [MessageId: Message] - + let messages: [EngineMessage.Id: EngineMessage] + var isSelectingPeers: Bool = false private(set) var selectionState: SelectionState - + let existingCategories: Set private(set) var selectedCategories: Set - + let peerItems: StoragePeerListPanelComponent.Items? let imageItems: StorageMediaGridPanelComponent.Items? let fileItems: StorageFileListPanelComponent.Items? let musicItems: StorageFileListPanelComponent.Items? - + private let allPhotos: Set private let allVideos: Set private let allFiles: Set private let allMusic: Set - + private(set) var selectedSize: Int64 = 0 - private(set) var clearIncludeMessages: [Message] = [] - private(set) var clearExcludeMessages: [Message] = [] - + private(set) var clearIncludeMessages: [EngineMessage] = [] + private(set) var clearExcludeMessages: [EngineMessage] = [] + init( peerId: EnginePeer.Id?, stats: AllStorageUsageStats, - messages: [MessageId: Message], + messages: [EngineMessage.Id: EngineMessage], peerItems: StoragePeerListPanelComponent.Items?, imageItems: StorageMediaGridPanelComponent.Items?, fileItems: StorageFileListPanelComponent.Items?, @@ -581,16 +580,16 @@ final class StorageUsageScreenComponent: Component { } } - var clearIncludeMessages: [Message] = [] - var clearExcludeMessages: [Message] = [] - + var clearIncludeMessages: [EngineMessage] = [] + var clearExcludeMessages: [EngineMessage] = [] + if self.selectedCategories.contains(.photos) { let deselectedPhotos = self.allPhotos.subtracting(self.selectionState.selectedMessages) if !deselectedPhotos.isEmpty, let imageItems = self.imageItems { for item in imageItems.items { if deselectedPhotos.contains(item.message.id) { selectedSize -= item.size - clearExcludeMessages.append(item.message._asMessage()) + clearExcludeMessages.append(item.message) } } } @@ -600,19 +599,19 @@ final class StorageUsageScreenComponent: Component { for item in imageItems.items { if selectedPhotos.contains(item.message.id) { selectedSize += item.size - clearIncludeMessages.append(item.message._asMessage()) + clearIncludeMessages.append(item.message) } } } } - + if self.selectedCategories.contains(.videos) { let deselectedVideos = self.allVideos.subtracting(self.selectionState.selectedMessages) if !deselectedVideos.isEmpty, let imageItems = self.imageItems { for item in imageItems.items { if deselectedVideos.contains(item.message.id) { selectedSize -= item.size - clearExcludeMessages.append(item.message._asMessage()) + clearExcludeMessages.append(item.message) } } } @@ -622,12 +621,12 @@ final class StorageUsageScreenComponent: Component { for item in imageItems.items { if selectedVideos.contains(item.message.id) { selectedSize += item.size - clearIncludeMessages.append(item.message._asMessage()) + clearIncludeMessages.append(item.message) } } } } - + if self.selectedCategories.contains(.files) { let deselectedFiles = self.allFiles.subtracting(self.selectionState.selectedMessages) if !deselectedFiles.isEmpty, let fileItems = self.fileItems { @@ -649,7 +648,7 @@ final class StorageUsageScreenComponent: Component { } } } - + if self.selectedCategories.contains(.music) { let deselectedMusic = self.allMusic.subtracting(self.selectionState.selectedMessages) if !deselectedMusic.isEmpty, let musicItems = self.musicItems { @@ -1044,17 +1043,11 @@ final class StorageUsageScreenComponent: Component { if self.statsDisposable == nil { let context = component.context - let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings])) - let cacheSettingsExceptionCount: Signal<[CacheStorageSettings.PeerStorageCategory: Int32], NoError> = component.context.account.postbox.combinedView(keys: [viewKey]) - |> map { views -> AccountSpecificCacheStorageSettings in - let cacheSettings: AccountSpecificCacheStorageSettings - if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) { - cacheSettings = value - } else { - cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings - } - - return cacheSettings + let cacheSettingsExceptionCount: Signal<[CacheStorageSettings.PeerStorageCategory: Int32], NoError> = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.accountSpecificCacheStorageSettings) + ) + |> map { preferencesEntry -> AccountSpecificCacheStorageSettings in + return preferencesEntry?.get(AccountSpecificCacheStorageSettings.self) ?? AccountSpecificCacheStorageSettings.defaultSettings } |> distinctUntilChanged |> mapToSignal { accountSpecificSettings -> Signal<[CacheStorageSettings.PeerStorageCategory: Int32], NoError> in @@ -1122,8 +1115,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 } } @@ -2082,7 +2075,7 @@ final class StorageUsageScreenComponent: Component { let peerInfoController = component.context.sharedContext.makePeerInfoController( context: component.context, updatedPresentationData: nil, - peer: peer._asPeer(), + peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, @@ -2214,7 +2207,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 +2220,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 +2252,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) @@ -2388,20 +2381,20 @@ final class StorageUsageScreenComponent: Component { } class RenderResult { - var messages: [MessageId: Message] = [:] + var messages: [EngineMessage.Id: EngineMessage] = [:] var imageItems: [StorageMediaGridPanelComponent.Item] = [] var fileItems: [StorageFileListPanelComponent.Item] = [] var musicItems: [StorageFileListPanelComponent.Item] = [] } - + self.messagesDisposable = (component.context.engine.resources.renderStorageUsageStatsMessages(stats: contextStats, categories: [.files, .photos, .videos, .music], existingMessages: self.aggregatedData?.messages ?? [:]) |> deliverOn(Queue()) |> map { messages -> RenderResult in let result = RenderResult() - + result.messages = messages - var mergedMedia: [MessageId: Int64] = [:] + var mergedMedia: [EngineMessage.Id: Int64] = [:] if let categoryStats = contextStats.categories[.photos] { mergedMedia = categoryStats.messages } @@ -2429,7 +2422,7 @@ final class StorageUsageScreenComponent: Component { if matches { result.imageItems.append(StorageMediaGridPanelComponent.Item( - message: EngineMessage(message), + message: message, size: messageSize )) } @@ -2565,7 +2558,7 @@ final class StorageUsageScreenComponent: Component { chatLocation: .peer(id: message.id.peerId), chatFilterTag: nil, chatLocationContextHolder: nil, - message: message, + message: message._asMessage(), standalone: true, reverseMessageGalleryOrder: false, navigationController: self.controller?()?.navigationController as? NavigationController @@ -2764,25 +2757,21 @@ final class StorageUsageScreenComponent: Component { self.controller?()?.presentInGlobalOverlay(controller) } - private func openMessage(message: Message) { + private func openMessage(message: EngineMessage) { guard let component = self.component else { return } guard let controller = self.controller?(), let navigationController = controller.navigationController as? NavigationController else { return } - let foundGalleryMessage: Message? = message - guard let galleryMessage = foundGalleryMessage else { - return - } self.endEditing(true) - + let _ = component.context.sharedContext.openChatMessage(OpenChatMessageParams( context: component.context, chatLocation: .peer(id: message.id.peerId), chatFilterTag: nil, chatLocationContextHolder: nil, - message: galleryMessage, + message: message._asMessage(), standalone: true, reverseMessageGalleryOrder: true, navigationController: navigationController, @@ -3067,9 +3056,9 @@ final class StorageUsageScreenComponent: Component { } } - var includeMessages: [Message] = [] - var excludeMessages: [Message] = [] - + var includeMessages: [EngineMessage] = [] + var excludeMessages: [EngineMessage] = [] + for (id, message) in aggregatedData.messages { if aggregatedData.selectionState.selectedPeers.contains(id.peerId) { if !aggregatedData.selectionState.selectedMessages.contains(id) { @@ -3081,7 +3070,7 @@ final class StorageUsageScreenComponent: Component { } } } - + let _ = (component.context.engine.resources.clearStorage(peerIds: aggregatedData.selectionState.selectedPeers, includeMessages: includeMessages, excludeMessages: excludeMessages) |> deliverOnMainQueue).start(next: { [weak self] progress in guard let self else { @@ -3132,69 +3121,52 @@ final class StorageUsageScreenComponent: Component { self.controller?()?.presentInGlobalOverlay(c, with: nil) } - let viewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.accountSpecificCacheStorageSettings])) - let accountSpecificSettings: Signal = context.account.postbox.combinedView(keys: [viewKey]) - |> map { views -> AccountSpecificCacheStorageSettings in - let cacheSettings: AccountSpecificCacheStorageSettings - if let view = views.views[viewKey] as? PreferencesView, let value = view.values[PreferencesKeys.accountSpecificCacheStorageSettings]?.get(AccountSpecificCacheStorageSettings.self) { - cacheSettings = value - } else { - cacheSettings = AccountSpecificCacheStorageSettings.defaultSettings - } - - return cacheSettings + let accountSpecificSettings: Signal = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.accountSpecificCacheStorageSettings) + ) + |> map { preferencesEntry -> AccountSpecificCacheStorageSettings in + return preferencesEntry?.get(AccountSpecificCacheStorageSettings.self) ?? AccountSpecificCacheStorageSettings.defaultSettings } |> distinctUntilChanged - - let peerExceptions: Signal<[(peer: FoundPeer, value: Int32)], NoError> = accountSpecificSettings - |> mapToSignal { accountSpecificSettings -> Signal<[(peer: FoundPeer, value: Int32)], NoError> in - return context.account.postbox.transaction { transaction -> [(peer: FoundPeer, value: Int32)] in - var result: [(peer: FoundPeer, value: Int32)] = [] - + + let peerExceptions: Signal<[(peer: EnginePeer, value: Int32)], NoError> = accountSpecificSettings + |> mapToSignal { accountSpecificSettings -> Signal<[(peer: EnginePeer, value: Int32)], NoError> in + return context.engine.data.get( + EngineDataMap(accountSpecificSettings.peerStorageTimeoutExceptions.map(\.key).map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))) + ) + |> map { peers -> [(peer: EnginePeer, value: Int32)] in + var result: [(peer: EnginePeer, value: Int32)] = [] + for item in accountSpecificSettings.peerStorageTimeoutExceptions { - let peerId = item.key - let value = item.value - - guard let peer = transaction.getPeer(peerId) else { + guard let peer = peers[item.key] ?? nil else { continue } let peerCategory: CacheStorageSettings.PeerStorageCategory - var subscriberCount: Int32? - if peer is TelegramUser { + switch peer { + case .user, .secretChat: peerCategory = .privateChats - } else if peer is TelegramGroup { + case .legacyGroup: peerCategory = .groups - - if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData { - subscriberCount = (cachedData.participants?.participants.count).flatMap(Int32.init) - } - } else if let channel = peer as? TelegramChannel { + case let .channel(channel): if case .group = channel.info { peerCategory = .groups } else { peerCategory = .channels } - if peerCategory == mappedCategory { - if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData { - subscriberCount = cachedData.participantsSummary.memberCount - } - } - } else { - continue } - + if peerCategory != mappedCategory { continue } - - result.append((peer: FoundPeer(peer: peer, subscribers: subscriberCount), value: value)) + + result.append((peer: peer, value: item.value)) } - + return result.sorted(by: { lhs, rhs in if lhs.value != rhs.value { return lhs.value < rhs.value } - return lhs.peer.peer.debugDisplayTitle < rhs.peer.peer.debugDisplayTitle + return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle }) } } @@ -3289,7 +3261,7 @@ final class StorageUsageScreenComponent: Component { } }))) } else { - subItems.append(.custom(MultiplePeerAvatarsContextItem(context: context, peers: peerExceptions.prefix(3).map { EnginePeer($0.peer.peer) }, totalCount: peerExceptions.count, action: { c, _ in + subItems.append(.custom(MultiplePeerAvatarsContextItem(context: context, peers: peerExceptions.prefix(3).map { $0.peer }, totalCount: peerExceptions.count, action: { c, _ in c.dismiss(completion: { }) diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 3e4d303edd..22f8fc3cfa 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -1320,7 +1320,7 @@ public final class PeerListItemComponent: Component { var mediaReference: AnyMediaReference? - if let peer = component.peer, let peerReference = PeerReference(peer._asPeer()) { + if let peer = component.peer, let peerReference = PeerReference(peer) { if let story = component.story { mediaReference = .story(peer: peerReference, id: story.id, media: story.media._asMedia()) } else if let message = component.message { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift index c80f66635f..03d040b559 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/LiveChatReactionStreamView.swift @@ -107,12 +107,12 @@ private func makePeerBadgeImage(engine: TelegramEngine, peer: EnginePeer, count: textSize.height = ceil(textSize.height) var avatarSourceImage: UIImage? - if let resource = smallestImageRepresentation(peer.profileImageRepresentations)?.resource, let peerReference = PeerReference(peer._asPeer()) { - let disposable = fetchedMediaResource(mediaBox: engine.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .avatar, reference: .avatar(peer: peerReference, resource: resource)).startStrict() - let signal = engine.account.postbox.mediaBox.resourceData(resource) - |> filter { $0.complete } + if let resource = smallestImageRepresentation(peer.profileImageRepresentations)?.resource, let peerReference = PeerReference(peer) { + let disposable = engine.resources.fetch(reference: .avatar(peer: peerReference, resource: resource), userLocation: .peer(peer.id), userContentType: .avatar).startStrict() + let signal = engine.resources.data(resource: EngineMediaResource(resource)) + |> filter { $0.isComplete } |> map { value -> Data? in - if value.complete { + if value.isComplete { return try? Data(contentsOf: URL(fileURLWithPath: value.path)) } else { return nil diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 9bcaa14158..fbda84417d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -207,9 +207,9 @@ public final class StoryContentContextImpl: StoryContentContext { if let cachedUserData = cachedPeerDataView.cachedPeerData as? CachedUserData { var isMuted = false if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { - isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: notificationSettings, topSearchPeers: []) + isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer, peerSettings: notificationSettings, topSearchPeers: []) } else { - isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: nil, topSearchPeers: []) + isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer, peerSettings: nil, topSearchPeers: []) } additionalPeerData = StoryContentContextState.AdditionalPeerData( isMuted: isMuted, @@ -1010,7 +1010,7 @@ public final class StoryContentContextImpl: StoryContentContext { for i in 0 ..< min(possibleItems.count, 3) { let peer = possibleItems[i].0 let item = possibleItems[i].1 - if let peerReference = PeerReference(peer._asPeer()), let mediaId = item.media.id { + if let peerReference = PeerReference(peer), let mediaId = item.media.id { var reactions: [MessageReaction.Reaction] = [] for mediaArea in item.mediaAreas { if case let .reaction(_, reaction, _) = mediaArea { @@ -1284,7 +1284,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { return } - let isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: []) + let isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer, peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: []) let additionalPeerData = StoryContentContextState.AdditionalPeerData( isMuted: isMuted, @@ -1596,7 +1596,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { let stateValue: StoryContentContextState if let focusedIndex, let (peer, presence, areVoiceMessagesAvailable, canViewStats, notificationSettings, globalNotificationSettings, isPremiumRequiredForMessaging, boostsToUnrestrict, appliedBoosts, sendPaidMessageStars) = data?.data, let peer { - let isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: []) + let isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer, peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: []) let additionalPeerData = StoryContentContextState.AdditionalPeerData( isMuted: isMuted, areVoiceMessagesAvailable: areVoiceMessagesAvailable, @@ -1714,7 +1714,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { for i in 0 ..< min(possibleItems.count, 3) { let peer = possibleItems[i].0 let item = possibleItems[i].1 - if let peerReference = PeerReference(peer._asPeer()), let mediaId = item.storyItem.media.id { + if let peerReference = PeerReference(peer), let mediaId = item.storyItem.media.id { var reactions: [MessageReaction.Reaction] = [] for mediaArea in item.storyItem.mediaAreas { if case let .reaction(_, reaction, _) = mediaArea { @@ -1916,13 +1916,13 @@ public func preloadStoryMedia(context: AccountContext, info: StoryPreloadInfo) - return combineLatest(files.map { file -> Signal in return Signal { subscriber in - let loadSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: file.resource)) + let loadSignal = context.engine.resources.fetch(reference: .standalone(resource: file.resource), userLocation: .other, userContentType: .sticker) |> ignoreValues |> `catch` { _ -> Signal in return .complete() } - let statusSignal = context.account.postbox.mediaBox.resourceStatus(file.resource) + let statusSignal = context.engine.resources.status(resource: EngineMediaResource(file.resource)) |> filter { status in if case .Local = status { return true @@ -1963,13 +1963,13 @@ public func preloadStoryMedia(context: AccountContext, info: StoryPreloadInfo) - return combineLatest(files.map { file -> Signal in return Signal { subscriber in - let loadSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: file.resource)) + let loadSignal = context.engine.resources.fetch(reference: .standalone(resource: file.resource), userLocation: .other, userContentType: .sticker) |> ignoreValues |> `catch` { _ -> Signal in return .complete() } - let statusSignal = context.account.postbox.mediaBox.resourceStatus(file.resource) + let statusSignal = context.engine.resources.status(resource: EngineMediaResource(file.resource)) |> filter { status in if case .Local = status { return true @@ -2028,7 +2028,7 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine guard let peerValue else { return .complete() } - guard let peer = PeerReference(peerValue._asPeer()) else { + guard let peer = PeerReference(peerValue) else { return .complete() } @@ -2065,9 +2065,9 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine case let .image(image): if let representation = largestImageRepresentation(image.representations) { statusSignals.append( - context.account.postbox.mediaBox.resourceData(representation.resource) + context.engine.resources.data(resource: EngineMediaResource(representation.resource)) |> filter { data in - return data.complete + return data.isComplete } |> take(1) |> ignoreValues @@ -2091,7 +2091,7 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine } statusSignals.append( - context.account.postbox.mediaBox.resourceRangesStatus(file.resource) + context.engine.resources.resourceRangesStatus(resource: EngineMediaResource(file.resource)) |> filter { ranges in if let fetchRange { return ranges.isSuperset(of: RangeSet(fetchRange.0)) @@ -2152,13 +2152,13 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine return combineLatest(files.map { file -> Signal in return Signal { subscriber in - let loadSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: file.resource)) + let loadSignal = context.engine.resources.fetch(reference: .standalone(resource: file.resource), userLocation: .other, userContentType: .sticker) |> ignoreValues |> `catch` { _ -> Signal in return .complete() } - let statusSignal = context.account.postbox.mediaBox.resourceStatus(file.resource) + let statusSignal = context.engine.resources.status(resource: EngineMediaResource(file.resource)) |> filter { status in if case .Local = status { return true @@ -2201,13 +2201,13 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine return combineLatest(files.map { file -> Signal in return Signal { subscriber in - let loadSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: file.resource)) + let loadSignal = context.engine.resources.fetch(reference: .standalone(resource: file.resource), userLocation: .other, userContentType: .sticker) |> ignoreValues |> `catch` { _ -> Signal in return .complete() } - let statusSignal = context.account.postbox.mediaBox.resourceStatus(file.resource) + let statusSignal = context.engine.resources.status(resource: EngineMediaResource(file.resource)) |> filter { status in if case .Local = status { return true @@ -2487,9 +2487,9 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { if let cachedUserData = cachedPeerDataView.cachedPeerData as? CachedUserData { var isMuted = false if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { - isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: notificationSettings, topSearchPeers: []) + isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer, peerSettings: notificationSettings, topSearchPeers: []) } else { - isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: nil, topSearchPeers: []) + isMuted = resolvedAreStoriesMuted(globalSettings: globalNotificationSettings._asGlobalNotificationSettings(), peer: peer, peerSettings: nil, topSearchPeers: []) } additionalPeerData = StoryContentContextState.AdditionalPeerData( isMuted: isMuted, @@ -2974,7 +2974,7 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { for i in 0 ..< min(possibleItems.count, 3) { let peer = possibleItems[i].0 let item = possibleItems[i].1 - if let peerReference = PeerReference(peer._asPeer()), let mediaId = item.media.id { + if let peerReference = PeerReference(peer), let mediaId = item.media.id { var reactions: [MessageReaction.Reaction] = [] for mediaArea in item.mediaAreas { if case let .reaction(_, reaction, _) = mediaArea { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 68a5ca4adc..8b09a8053c 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 @@ -983,6 +982,9 @@ private final class StoryContainerScreenComponent: Component { if let environment = self.environment, case .regular = environment.metrics.widthClass { if result.isDescendant(of: self.backgroundEffectView) { if let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { + if point.x < itemSetView.frame.minX || point.x > itemSetView.frame.maxX { + return result + } return itemSetView.view.view } } @@ -1246,6 +1248,13 @@ private final class StoryContainerScreenComponent: Component { } } } + + fileprivate func navigateWithKeyShortcut(direction: StoryItemSetContainerComponent.NavigationDirection) { + guard !hasFirstResponder(self) else { + return + } + self.navigate(direction: direction) + } func presentExternalTooltip(_ tooltipScreen: UndoOverlayController) { guard let stateValue = self.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View else { @@ -2013,7 +2022,7 @@ private final class StoryContainerScreenComponent: Component { } } -public class StoryContainerScreen: ViewControllerComponentContainer { +public class StoryContainerScreen: ViewControllerComponentContainer, KeyShortcutResponder { public struct TransitionState: Equatable { public var sourceSize: CGSize public var destinationSize: CGSize @@ -2153,6 +2162,56 @@ public class StoryContainerScreen: ViewControllerComponentContainer { } } } + + public var keyShortcuts: [KeyShortcut] { + if self.isViewLoaded, hasFirstResponder(self.view) { + return [] + } + var keyShortcuts: [KeyShortcut] = [] + keyShortcuts.append( + KeyShortcut( + title: "", + input: UIKeyCommand.inputUpArrow, + modifiers: [.command], + action: { [weak self] in + self?.dismiss() + } + ) + ) + keyShortcuts.append( + KeyShortcut( + title: "", + input: "W", + modifiers: [.command], + action: { [weak self] in + self?.dismiss() + } + ) + ) + keyShortcuts.append( + KeyShortcut( + input: UIKeyCommand.inputLeftArrow, + modifiers: [], + action: { [weak self] in + if let componentView = self?.node.hostView.componentView as? StoryContainerScreenComponent.View { + componentView.navigateWithKeyShortcut(direction: .previous) + } + } + ) + ) + keyShortcuts.append( + KeyShortcut( + input: UIKeyCommand.inputRightArrow, + modifiers: [], + action: { [weak self] in + if let componentView = self?.node.hostView.componentView as? StoryContainerScreenComponent.View { + componentView.navigateWithKeyShortcut(direction: .next) + } + } + ) + ) + return keyShortcuts + } public func presentExternalTooltip(_ tooltipScreen: UndoOverlayController) { if let componentView = self.node.hostView.componentView as? StoryContainerScreenComponent.View { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 49d6030c89..7dbb255eae 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -4,7 +4,6 @@ import Display import ComponentFlow import MultilineTextComponent import AccountContext -import Postbox import TelegramCore import TextNodeWithEntities import TextFormat @@ -70,7 +69,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 +88,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 +755,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/StoryContentLiveChatComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift index dd9701b798..25646317a7 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift @@ -367,7 +367,7 @@ final class StoryContentLiveChatComponent: Component { rank: nil, subscriptionUntilDate: nil ), - peer: author._asPeer() + peer: author )], mode: .liveStream( messageCount: 1, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 75fe51d2a7..8bbee1abe6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -335,7 +335,7 @@ final class StoryItemContentComponent: Component { return } - if case let .file(file) = currentMessageMedia, let peerReference = PeerReference(component.peer._asPeer()) { + if case let .file(file) = currentMessageMedia, let peerReference = PeerReference(component.peer) { if self.videoNode == nil { let videoNode = UniversalVideoNode( context: component.context, @@ -843,7 +843,7 @@ final class StoryItemContentComponent: Component { let startTime = CFAbsoluteTimeGetCurrent() - let peerReference = PeerReference(component.peer._asPeer()) + let peerReference = PeerReference(component.peer) let selectedMedia: EngineMedia var messageMedia: EngineMedia? diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift index a5dcac6959..adf39cf105 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AccountContext import TelegramCore -import Postbox import SwiftSignalKit import ComponentFlow import TinyThumbnail @@ -84,7 +83,7 @@ final class StoryItemImageView: UIView { dimensions = representation.dimensions.cgSize if isMediaUpdated { - if attemptSynchronous, let path = context.account.postbox.mediaBox.completedResourcePath(id: representation.resource.id, pathExtension: nil) { + if attemptSynchronous, let path = context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(representation.resource.id), pathExtension: nil) { if #available(iOS 15.0, *) { if let image = UIImage(contentsOfFile: path)?.preparingForDisplay() { self.updateImage(image: image, isCaptureProtected: isCaptureProtected) @@ -103,12 +102,12 @@ final class StoryItemImageView: UIView { } } - if let peerReference = PeerReference(peer._asPeer()) { + if let peerReference = PeerReference(peer) { self.fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .story, reference: .media(media: .story(peer: peerReference, id: storyId, media: media._asMedia()), resource: representation.resource), ranges: nil).start() } - self.disposable = (context.account.postbox.mediaBox.resourceData(representation.resource, option: .complete(waitUntilFetchStatus: false)) + self.disposable = (context.engine.resources.data(resource: EngineMediaResource(representation.resource)) |> map { result -> UIImage? in - if result.complete { + if result.isComplete { if #available(iOS 15.0, *) { if let image = UIImage(contentsOfFile: result.path)?.preparingForDisplay() { return image @@ -165,9 +164,9 @@ final class StoryItemImageView: UIView { } let fullSize = context.account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedVideoFirstFrameRepresentation(), complete: true, fetch: true, attemptSynchronously: false) - var previewSize: Signal = .single(nil) + var previewSize: Signal = .single(nil) if let representation = file.previewRepresentations.first { - previewSize = context.account.postbox.mediaBox.resourceData(representation.resource, option: .complete(waitUntilFetchStatus: false)) + previewSize = context.engine.resources.data(resource: EngineMediaResource(representation.resource)) |> map(Optional.init) } @@ -190,7 +189,7 @@ final class StoryItemImageView: UIView { return nil } } - } else if let previewResult, previewResult.complete { + } else if let previewResult, previewResult.isComplete { if #available(iOS 15.0, *) { if let image = UIImage(contentsOfFile: previewResult.path)?.preparingForDisplay() { return image diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift index 72f8685b71..61cdc25fd4 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemOverlaysView.swift @@ -425,7 +425,7 @@ final class StoryItemOverlaysView: UIView { self.directStickerView = directStickerView self.customEmojiLoadDisposable?.dispose() - self.customEmojiLoadDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: file.resource)).start() + self.customEmojiLoadDisposable = context.engine.resources.fetch(reference: .standalone(resource: file.resource), userLocation: .other, userContentType: .sticker).start() } var color: UIColor? if file.isCustomTemplateEmoji { @@ -474,7 +474,7 @@ final class StoryItemOverlaysView: UIView { customEmojiView.updateTextColor(flags.contains(.isDark) ? .white : .black) self.customEmojiLoadDisposable?.dispose() - self.customEmojiLoadDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: file.resource)).start() + self.customEmojiLoadDisposable = context.engine.resources.fetch(reference: .standalone(resource: file.resource), userLocation: .other, userContentType: .sticker).start() customEmojiView.isUserInteractionEnabled = false self.customEmojiView = customEmojiView @@ -594,7 +594,7 @@ final class StoryItemOverlaysView: UIView { self.file = file self.customEmojiLoadDisposable?.dispose() - self.customEmojiLoadDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: file.resource)).start() + self.customEmojiLoadDisposable = context.engine.resources.fetch(reference: .standalone(resource: file.resource), userLocation: .other, userContentType: .sticker).start() let _ = self.directStickerView.update( transition: .immediate, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index e1b6ffa7c0..242dc1e3d3 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -3066,7 +3066,7 @@ public final class StoryItemSetContainerComponent: Component { sendAsConfiguration = sendAsPeer.flatMap { value in return MessageInputPanelComponent.SendAsConfiguration( - currentPeer: EnginePeer(value.peer), + currentPeer: value.peer, subscriberCount: value.subscribers.flatMap(Int.init), isPremiumLocked: value.isPremiumRequired, isSelecting: self.sendMessageContext.isSelectingSendAsPeer, @@ -5626,7 +5626,7 @@ public final class StoryItemSetContainerComponent: Component { } navigationController.setViewControllers(currentViewControllers, animated: true) } else { - guard let chatController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + guard let chatController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { return } @@ -5967,7 +5967,7 @@ public final class StoryItemSetContainerComponent: Component { } private func requestSave() { - guard let component = self.component, let peerReference = PeerReference(component.slice.effectivePeer._asPeer()) else { + guard let component = self.component, let peerReference = PeerReference(component.slice.effectivePeer) else { return } @@ -5977,7 +5977,7 @@ public final class StoryItemSetContainerComponent: Component { let stringSaving = component.strings.Story_TooltipSaving let stringSaved = component.strings.Story_TooltipSaved - let disposable = (saveToCameraRoll(context: component.context, postbox: component.context.account.postbox, userLocation: .peer(peerReference.id), customUserContentType: .story, mediaReference: .story(peer: peerReference, id: component.slice.item.storyItem.id, media: component.slice.item.storyItem.media._asMedia())) + let disposable = (saveToCameraRoll(context: component.context, userLocation: .peer(peerReference.id), customUserContentType: .story, mediaReference: .story(peer: peerReference, id: component.slice.item.storyItem.id, media: component.slice.item.storyItem.media._asMedia())) |> deliverOnMainQueue).start(next: { [weak saveScreen] progress in guard let saveScreen else { return @@ -6243,7 +6243,7 @@ public final class StoryItemSetContainerComponent: Component { } } - if !emojiFileIds.isEmpty || hasLinkedStickers, let peerReference = PeerReference(component.slice.effectivePeer._asPeer()) { + if !emojiFileIds.isEmpty || hasLinkedStickers, let peerReference = PeerReference(component.slice.effectivePeer) { let context = component.context tip = .animatedEmoji(text: nil, arguments: nil, file: nil, action: nil) @@ -6697,7 +6697,7 @@ public final class StoryItemSetContainerComponent: Component { } } - if component.slice.item.storyItem.isPublic && (component.slice.effectivePeer.addressName != nil || !component.slice.effectivePeer._asPeer().usernames.isEmpty) && (component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) || component.slice.item.storyItem.isPinned) { + if component.slice.item.storyItem.isPublic && (component.slice.effectivePeer.addressName != nil || !component.slice.effectivePeer.usernames.isEmpty) && (component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) || component.slice.item.storyItem.isPinned) { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -6953,7 +6953,7 @@ public final class StoryItemSetContainerComponent: Component { }))) } - if component.slice.item.storyItem.isPublic && (component.slice.effectivePeer.addressName != nil || !component.slice.effectivePeer._asPeer().usernames.isEmpty) && (component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) || component.slice.item.storyItem.isPinned) { + if component.slice.item.storyItem.isPublic && (component.slice.effectivePeer.addressName != nil || !component.slice.effectivePeer.usernames.isEmpty) && (component.slice.item.storyItem.expirationTimestamp > Int32(Date().timeIntervalSince1970) || component.slice.item.storyItem.isPinned) { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -7221,7 +7221,7 @@ public final class StoryItemSetContainerComponent: Component { items.append(.separator) } - let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: component.slice.effectivePeer._asPeer(), peerSettings: settings._asNotificationSettings(), topSearchPeers: topSearchPeers) + let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: component.slice.effectivePeer, peerSettings: settings._asNotificationSettings(), topSearchPeers: topSearchPeers) if !component.slice.effectivePeer.isService && isContact { items.append(.action(ContextMenuActionItem(text: isMuted ? component.strings.StoryFeed_ContextNotifyOn : component.strings.StoryFeed_ContextNotifyOff, icon: { theme in @@ -7271,7 +7271,7 @@ public final class StoryItemSetContainerComponent: Component { }))) } - if !component.slice.effectivePeer.isService && component.slice.item.storyItem.isPublic && (component.slice.effectivePeer.addressName != nil || !component.slice.effectivePeer._asPeer().usernames.isEmpty) { + if !component.slice.effectivePeer.isService && component.slice.item.storyItem.isPublic && (component.slice.effectivePeer.addressName != nil || !component.slice.effectivePeer.usernames.isEmpty) { items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -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) 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 @@ -7614,7 +7616,7 @@ public final class StoryItemSetContainerComponent: Component { if let controller = component.context.sharedContext.makePeerInfoController( context: component.context, updatedPresentationData: nil, - peer: peer._asPeer(), + peer: peer, mode: .myProfile, avatarInitiallyExpanded: false, fromChat: false, @@ -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 diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 1d9f246881..36cce86732 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? @@ -62,13 +62,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { case text case media } - + private var context: AccountContext? private weak var view: StoryItemSetContainerComponent.View? 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? @@ -76,59 +76,59 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { weak var menuController: ViewController? var progressPauseContext = StoryProgressPauseContext() var isViewingAttachedStickers = false - + var currentTooltipUpdateTimer: Foundation.Timer? - + var currentInputMode: InputMode = .text private var needsInputActivation = false - + var audioRecorderValue: ManagedAudioRecorder? var audioRecorder = Promise() var recordedAudioPreview: ChatRecordedMediaPreview? - + var videoRecorderValue: InstantVideoController? var videoRecorder = Promise() var hasRecordedVideoPreview = false - + var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? var inputMediaNodeDataDisposable: Disposable? var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext() var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction? var inputMediaNode: ChatEntityKeyboardInputNode? - + let controllerNavigationDisposable = MetaDisposable() let enqueueMediaMessageDisposable = MetaDisposable() let navigationActionDisposable = MetaDisposable() let resolvePeerByNameDisposable = MetaDisposable() - + var currentSpeechHolder: SpeechSynthesizerHolder? - + var currentLiveStreamMessageStars: StarsAmount? weak var currentSendStarsUndoController: UndoOverlayController? var currentLiveStreamStarsIsActive: Bool = false var currentLiveStreamStarsIsActiveTimer: Foundation.Timer? - + struct SendAsData: Equatable { var isPremium: Bool var availablePeers: [SendAsPeer] - + init(isPremium: Bool, availablePeers: [SendAsPeer]) { self.isPremium = isPremium self.availablePeers = availablePeers } } - + var sendAsData: SendAsData? var currentSendAsPeer: SendAsPeer? var isSelectingSendAsPeer: Bool = false var sendAsDisposable: Disposable? - + private(set) var isMediaRecordingLocked: Bool = false var wasRecordingDismissed: Bool = false - + init() { } - + deinit { self.controllerNavigationDisposable.dispose() self.enqueueMediaMessageDisposable.dispose() @@ -138,12 +138,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.currentTooltipUpdateTimer?.invalidate() self.sendAsDisposable?.dispose() } - + func setup(component: StoryItemSetContainerComponent, view: StoryItemSetContainerComponent.View, inputPanelExternalState: MessageInputPanelComponent.ExternalState, keyboardInputData: Signal) { self.context = component.context self.inputPanelExternalState = inputPanelExternalState self.view = view - + if self.inputMediaNodeDataDisposable == nil { self.inputMediaNodeDataDisposable = (keyboardInputData |> deliverOnMainQueue).start(next: { [weak self] value in @@ -153,7 +153,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.inputMediaNodeData = value }) } - + self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction( sendSticker: { [weak self] fileReference, _, _, _, _, _, _, _, _ in if let self, let view = self.view { @@ -192,7 +192,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }, dismissTextInput: { - + }, insertText: { [weak self] text in if let self { @@ -229,7 +229,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } ) self.inputMediaInteraction?.forceTheme = defaultDarkColorPresentationTheme - + if let peerId = component.slice.item.peerId { self.sendAsDisposable = combineLatest( queue: Queue.mainQueue(), @@ -241,12 +241,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let accountPeer else { return } - + let isPremium = accountPeer.isPremium - + var availablePeers: [SendAsPeer] = [] availablePeers.append(SendAsPeer( - peer: accountPeer._asPeer(), + peer: accountPeer, subscribers: nil, isPremiumRequired: false )) @@ -256,7 +256,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } availablePeers.append(peer) } - + let sendAsData = SendAsData( isPremium: isPremium, availablePeers: availablePeers @@ -270,7 +270,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) } } - + func toggleInputMode() { guard let view = self.view else { return @@ -284,12 +284,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.currentInputMode = .text } } - + func updateInputMediaNode(view: StoryItemSetContainerComponent.View, availableSize: CGSize, bottomInset: CGFloat, bottomContainerInset: CGFloat, inputHeight: CGFloat, effectiveInputHeight: CGFloat, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, transition: ComponentTransition) -> CGFloat { guard let context = self.context, let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { return 0.0 } - + var height: CGFloat = 0.0 if let component = self.view?.component, case .media = self.currentInputMode, let inputData = self.inputMediaNodeData { var updatedInputData = inputData @@ -297,7 +297,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if case .liveStream = component.slice.item.storyItem.media { isLiveStream = true } - + if isLiveStream { updatedInputData = ChatEntityKeyboardInputNode.InputData( emoji: updatedInputData.emoji, @@ -306,7 +306,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { availableGifSearchEmojies: [] ) } - + var animateIn = false let inputMediaNode: ChatEntityKeyboardInputNode if let current = self.inputMediaNode { @@ -341,7 +341,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } self.inputMediaNode = inputMediaNode } - + let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) let presentationInterfaceState = ChatPresentationInterfaceState( chatWallpaper: .builtin(WallpaperSettings()), @@ -357,7 +357,6 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { mode: .standard(.default), chatLocation: .peer(id: context.account.peerId), subject: nil, - peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, @@ -368,22 +367,22 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { accountPeerColor: nil, businessIntro: nil ) - + let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false) let inputNodeHeight = heightAndOverflow.0 let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight)) - + if animateIn { let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight) ComponentTransition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) } - + transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame) - + height = heightAndOverflow.0 } else if let inputMediaNode = self.inputMediaNode { self.inputMediaNode = nil - + var targetFrame = inputMediaNode.frame targetFrame.origin.y = availableSize.height transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in @@ -396,17 +395,17 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }) } - + if self.needsInputActivation { self.needsInputActivation = false Queue.mainQueue().justDispatch { inputPanelView.activateInput() } } - + return height } - + func animateOut(bounds: CGRect) { if let inputMediaNode = self.inputMediaNode { inputMediaNode.layer.animatePosition( @@ -420,20 +419,20 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { inputMediaNode.layer.animateAlpha(from: inputMediaNode.alpha, to: 0.0, duration: 0.3, removeOnCompletion: false) } } - + private func presentMessageSentTooltip(view: StoryItemSetContainerComponent.View, peer: EnginePeer, messageId: EngineMessage.Id?, isScheduled: Bool = false) { guard let component = view.component, let controller = component.controller() as? StoryContainerScreen else { return } - + if let tooltipScreen = self.tooltipScreen { tooltipScreen.dismiss(animated: true) } - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - + let text = isScheduled ? presentationData.strings.Story_TooltipMessageScheduled : presentationData.strings.Story_TooltipMessageSent - + let tooltipScreen = UndoOverlayController( presentationData: presentationData, content: .actionSucceeded(title: "", text: text, cancel: messageId != nil ? presentationData.strings.Story_ToastViewInChat : "", destructive: false), @@ -451,31 +450,31 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { controller.present(tooltipScreen, in: .current) self.tooltipScreen = tooltipScreen view.updateIsProgressPaused() - + HapticFeedback().success() } - + func presentSendMessageOptions(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) { guard let component = view.component, let controller = component.controller() as? StoryContainerScreen else { return } - + view.dismissAllTooltips() - + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) var items: [ContextMenuItem] = [] - + if case .liveStream = component.slice.item.storyItem.media { items.append(.action(ContextMenuActionItem(text: self.currentLiveStreamMessageStars != nil ? presentationData.strings.LiveStream_InputContextMenuEditStars : presentationData.strings.LiveStream_InputContextMenuAddStars, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/AccessoryIconSuggestPost"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak view] _, a in a(.default) - + guard let self, let view else { return } self.performPaidMessageAction(view: view) }))) - + var canRemoveStars = self.currentLiveStreamMessageStars != nil if let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { if let liveChatStateValue = visibleItemView.liveChatState { @@ -484,12 +483,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + if canRemoveStars { items.append(.action(ContextMenuActionItem(text: presentationData.strings.LiveStream_InputContextMenuRemoveStars, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/RemovePrice"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak view] _, a in a(.default) - + guard let self, let view else { return } @@ -502,34 +501,34 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if let presence = component.slice.additionalPeerData.presence, case .present = presence.status { sendWhenOnlineAvailable = true } - + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak view] _, a in a(.default) - + guard let self, let view else { return } self.performSendMessageAction(view: view, silentPosting: true) }))) - + if sendWhenOnlineAvailable { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendWhenOnline, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak view] _, a in a(.default) - + guard let self, let view else { return } self.performSendMessageAction(view: view, scheduleTime: scheduleWhenOnlineTimestamp) }))) } - + if component.slice.additionalPeerData.sendPaidMessageStars == nil { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak view] _, a in a(.default) - + guard let self, let view else { return } @@ -537,9 +536,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }))) } } - + let contextItems = ContextController.Items(content: .list(items)) - + let contextController = makeContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .top)), items: .single(contextItems), gesture: gesture) contextController.dismissed = { [weak view] in guard let view else { @@ -552,7 +551,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { view.updateIsProgressPaused() controller.present(contextController, in: .window(.root)) } - + func presentScheduleTimePicker( view: StoryItemSetContainerComponent.View ) { @@ -565,12 +564,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } let _ = peerId let controller = component.controller() as? StoryContainerScreen - + var sendWhenOnlineAvailable = false if let presence = component.slice.additionalPeerData.presence, case .present = presence.status { sendWhenOnlineAvailable = true } - + let timeController = ChatScheduleTimeController(context: component.context, updatedPresentationData: nil, mode: .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable), style: .media, currentTime: nil, minimalTime: nil, dismissByTapOutside: true, completion: { [weak self, weak view] time in guard let self, let view else { return @@ -586,18 +585,18 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } view.endEditing(true) controller?.present(timeController, in: .window(.root)) - + self.actionSheet = timeController view.updateIsProgressPaused() } - + func presentPaidMessageAlertIfNeeded(view: StoryItemSetContainerComponent.View, completion: @escaping () -> Void) { guard let component = view.component, let sendPaidMessageStars = component.slice.additionalPeerData.sendPaidMessageStars else { completion() return } let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) - + let controller = chatMessagePaymentAlertController( context: component.context, presentationData: presentationData, @@ -614,13 +613,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { ) component.controller()?.present(controller, in: .window(.root)) } - + func performWithPossibleStealthModeConfirmation(view: StoryItemSetContainerComponent.View, action: @escaping () -> Void) { guard let component = view.component, component.stealthModeTimeout != nil else { action() return } - + let _ = (combineLatest( component.context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState() @@ -629,16 +628,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { ) |> deliverOnMainQueue).start(next: { [weak self, weak view] data, noticeCount in let config = data - + guard let self, let view, let component = view.component else { return } - + let timestamp = Int32(Date().timeIntervalSince1970) if noticeCount < 1, let activeUntilTimestamp = config.stealthModeState.actualizedNow().activeUntilTimestamp, activeUntilTimestamp > timestamp { 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 alertController = AlertScreen( title: component.strings.Story_AlertStealthModeActiveTitle, text: component.strings.Story_AlertStealthModeActiveText, @@ -659,9 +658,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } self.actionSheet = alertController view.updateIsProgressPaused() - + component.controller()?.presentInGlobalOverlay(alertController) - + #if DEBUG #else let _ = ApplicationSpecificNotice.incrementStoryStealthModeReplyCount(accountManager: component.context.sharedContext.accountManager).start() @@ -671,7 +670,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }) } - + func performSendMessageAction( view: StoryItemSetContainerComponent.View, silentPosting: Bool = false, @@ -682,7 +681,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } if case .liveStream = component.slice.item.storyItem.media { if let visibleItem = view.visibleItems[component.slice.item.id], let itemView = visibleItem.view.view as? StoryItemContentComponent.View { - + var sendPaidMessageStars = self.currentLiveStreamMessageStars var isAdmin = false var sendAsPeer: SendAsPeer? @@ -697,7 +696,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { sendPaidMessageStars = StarsAmount(value: minMessagePrice, nanos: 0) } } - + if let currentSendAsPeer = self.currentSendAsPeer { sendAsPeer = currentSendAsPeer } else { @@ -705,7 +704,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return self.sendAsData?.availablePeers.first(where: { $0.peer.id == defaultSendAs }) } } - + if liveChatStateValue.isAdmin { if let sendAsPeer { isAdmin = sendAsPeer.peer.id == component.context.account.peerId @@ -715,7 +714,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + if let call = itemView.mediaStreamCall { let focusedItem = component.slice.item guard let peerId = focusedItem.peerId else { @@ -724,7 +723,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { return } - + switch inputPanelView.getSendMessageInput() { case let .text(text): if !text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { @@ -734,7 +733,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let params = GroupCallMessagesContext.getStarAmountParamMapping(params: paramSets, value: isAdmin ? 1000000000 : sendPaidMessageStars?.value ?? 0) maxInputLength = params.maxLength maxEmojiCount = params.emojiCount - + var sendDisabledMinStars: Int64? if let maxInputLength, text.string.count > maxInputLength { sendDisabledMinStars = paramSets.paramSets.sorted(by: { $0.minStars < $1.minStars }).first(where: { $0.maxMessageLength >= text.string.count })?.minStars ?? 1000000000 @@ -763,36 +762,36 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if emojiCount > absMaxEmojiCount { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) view.component?.controller()?.present(textAlertController(context: component.context, updatedPresentationData: (presentationData, .single(presentationData)), title: nil, text: presentationData.strings.LiveStream_ErrorMaxAllowedEmoji_Text(Int32(absMaxEmojiCount)), actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - + return } - + sendDisabledMinStars = paramSets.paramSets.sorted(by: { $0.minStars < $1.minStars }).first(where: { $0.maxEmojiCount >= emojiCount })?.minStars ?? 1000000000 } } - + if let sendDisabledMinStars { self.performPaidMessageAction(view: view, minStars: Int(sendDisabledMinStars)) return } - + if let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View { if !(visibleItemView.liveChatState?.isExpanded ?? true) { visibleItemView.toggleLiveChatExpanded() } visibleItemView.scheduleScrollLiveChatToBottom() } - + let entities = generateChatInputTextEntities(text) - + call.sendMessage(fromId: sendAsPeer?.peer.id, isAdmin: isAdmin, text: text.string, entities: entities, paidStars: sendPaidMessageStars?.value) - + component.storyItemSharedState.replyDrafts.removeValue(forKey: StoryId(peerId: peerId, id: focusedItem.storyItem.id)) inputPanelView.clearSendMessageInput(updateState: true) - + self.currentLiveStreamMessageStars = nil view.state?.updated(transition: .spring(duration: 0.4)) - + let controller = component.controller() as? StoryContainerScreen controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) } @@ -801,7 +800,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } return } - + self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak self, weak view] in guard let self, let view else { return @@ -809,7 +808,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let component = view.component else { return } - + let focusedItem = component.slice.item guard let peerId = focusedItem.peerId else { return @@ -819,27 +818,27 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } let peer = component.slice.effectivePeer - + let controller = component.controller() as? StoryContainerScreen - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self, weak view] in guard let self, let view else { return } if let recordedAudioPreview = self.recordedAudioPreview, case let .audio(audio) = recordedAudioPreview { self.recordedAudioPreview = nil - + let waveformBuffer = audio.waveform.makeBitstream() - + var messageAttributes: [MessageAttribute] = [] if let sendPaidMessageStars = component.slice.additionalPeerData.sendPaidMessageStars { messageAttributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false)) } - + let messages: [EnqueueMessage] = [.message(text: "", attributes: messageAttributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: audio.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: Int(audio.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] - + let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: messages).start() - + view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } else if self.hasRecordedVideoPreview, let videoRecorderValue = self.videoRecorderValue { videoRecorderValue.send() @@ -868,7 +867,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) component.storyItemSharedState.replyDrafts.removeValue(forKey: StoryId(peerId: peerId, id: focusedItem.storyItem.id)) inputPanelView.clearSendMessageInput(updateState: true) - + self.currentInputMode = .text if hasFirstResponder(view) { view.endEditing(true) @@ -882,7 +881,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) } - + func performSendStickerAction(view: StoryItemSetContainerComponent.View, fileReference: FileMediaReference) { self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak self, weak view] in guard let self, let view else { @@ -897,9 +896,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id) let peer = component.slice.effectivePeer - + let controller = component.controller() as? StoryContainerScreen - + if let navigationController = controller?.navigationController as? NavigationController { var controllers = navigationController.viewControllers for controller in controllers.reversed() { @@ -910,14 +909,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } navigationController.setViewControllers(controllers, animated: true) - + controller?.window?.forEachController({ controller in if let controller = controller as? StickerPackScreenImpl { controller.dismiss() } }) } - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self, weak view] in guard let self, let view else { return @@ -935,7 +934,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } }) - + self.currentInputMode = .text if hasFirstResponder(view) { view.endEditing(true) @@ -946,8 +945,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) } - - func performSendContextResultAction(view: StoryItemSetContainerComponent.View, results: ChatContextResultCollection, result: ChatContextResult) { + + func performSendContextResultAction(view: StoryItemSetContainerComponent.View, results: ChatContextResultCollection, result: ChatContextResult, silentPosting: Bool = false, scheduleTime: Int32? = nil) { guard let component = view.component else { return } @@ -957,9 +956,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } let focusedStoryId = StoryId(peerId: peerId, id: focusedItem.storyItem.id) let peer = component.slice.effectivePeer - + let controller = component.controller() as? StoryContainerScreen - + if let navigationController = controller?.navigationController as? NavigationController { var controllers = navigationController.viewControllers for controller in controllers.reversed() { @@ -970,14 +969,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } navigationController.setViewControllers(controllers, animated: true) - + controller?.window?.forEachController({ controller in if let controller = controller as? StickerPackScreenImpl { controller.dismiss() } }) } - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self, weak view] in guard let self, let view else { return @@ -987,6 +986,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { replyTo: nil, storyId: focusedStoryId, content: .contextResult(results, result), + silentPosting: silentPosting, + scheduleTime: scheduleTime, sendPaidMessageStars: component.slice.additionalPeerData.sendPaidMessageStars ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in Queue.mainQueue().after(0.3) { @@ -995,7 +996,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } }) - + self.currentInputMode = .text if hasFirstResponder(view) { view.endEditing(true) @@ -1005,7 +1006,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { controller?.requestLayout(forceUpdate: true, transition: .animated(duration: 0.3, curve: .spring)) }) } - + func enqueueGifData(view: StoryItemSetContainerComponent.View, data: Data) { guard let component = view.component else { return @@ -1022,15 +1023,15 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) } - + func enqueueStickerImage(view: StoryItemSetContainerComponent.View, image: UIImage, isMemoji: Bool) { guard let component = view.component else { return } let peer = component.slice.effectivePeer - + let size = image.size.aspectFitted(CGSize(width: 512.0, height: 512.0)) - + func scaleImage(_ image: UIImage, size: CGSize, boundiingSize: CGSize) -> UIImage? { let format = UIGraphicsImageRendererFormat() format.scale = 1.0 @@ -1045,13 +1046,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if let targetSize = targetSize, let scaledImage = scaleImage(image, size: targetSize, boundiingSize: targetSize) { image = scaledImage } - + return Signal { subscriber in if let data = try? WebP.convert(toWebP: image, quality: quality * 100.0) { subscriber.putNext(data) } subscriber.putCompletion() - + return EmptyDisposable } |> runOn(Queue.concurrentDefaultQueue()) } @@ -1059,16 +1060,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let _ = (convertToWebP(image: image, targetSize: size, targetBoundingSize: size, quality: 0.9) |> deliverOnMainQueue).start(next: { [weak self, weak view] data in if let self, let view, !data.isEmpty { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - component.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - + component.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) + var fileAttributes: [TelegramMediaFileAttribute] = [] fileAttributes.append(.FileName(fileName: "sticker.webp")) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) fileAttributes.append(.ImageSize(size: PixelDimensions(size))) - + let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: Int64(data.count), attributes: fileAttributes, alternativeRepresentations: []) let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in guard let self else { return @@ -1078,7 +1079,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }) } - + func setMediaRecordingActive( view: StoryItemSetContainerComponent.View, isActive: Bool, @@ -1086,7 +1087,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { sendAction: Bool ) { self.isMediaRecordingLocked = false - + guard let component = view.component else { return } @@ -1102,7 +1103,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let view, let component = view.component, let peer else { return } - + if isActive { if isVideo { if self.videoRecorderValue == nil { @@ -1140,8 +1141,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view else { return } - self.presentScheduleTimePicker(view: view, peer: peer, completion: { time, repeatPeriod in - done(time) + self.presentScheduleTimePicker(view: view, peer: peer, completion: { time, repeatPeriod, silentPosting in + done(time, silentPosting) }) }))) } @@ -1159,24 +1160,24 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let component = view.component else { return } - + self.wasRecordingDismissed = !sendAction self.audioRecorder.set(.single(nil)) - + guard let data else { return } - + if data.duration < 0.5 || !sendAction { HapticFeedback().error() } else { let randomId = Int64.random(in: Int64.min ... Int64.max) - + let resource = LocalFileMediaResource(fileId: randomId) - component.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) - + component.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data.compressedData) + let waveformBuffer: Data? = data.waveform - + self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak self, weak view] in guard let self, let view else { return @@ -1186,7 +1187,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) - + HapticFeedback().tap() }) }) @@ -1194,24 +1195,24 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) } else if let videoRecorderValue = self.videoRecorderValue { self.wasRecordingDismissed = !sendAction - + if sendAction { videoRecorderValue.completeVideo() } else { self.videoRecorder.set(.single(nil)) } self.hasRecordedVideoPreview = false - + view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } }) } - + func lockMediaRecording() { self.isMediaRecordingLocked = true } - + func stopMediaRecording(view: StoryItemSetContainerComponent.View) { if let audioRecorderValue = self.audioRecorderValue { let _ = (audioRecorderValue.takenRecordedData() |> deliverOnMainQueue).start(next: { [weak self, weak view] data in @@ -1219,7 +1220,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } self.audioRecorder.set(.single(nil)) - + guard let data else { return } @@ -1227,8 +1228,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { HapticFeedback().error() } else if let waveform = data.waveform { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: Int64(data.compressedData.count)) - - component.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) + + component.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data.compressedData) self.recordedAudioPreview = .audio(ChatRecordedMediaPreview.Audio(resource: resource, fileSize: Int32(data.compressedData.count), duration: Int32(data.duration), waveform: AudioWaveform(bitstream: waveform, bitsPerSample: 5))) view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } @@ -1242,7 +1243,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + func discardMediaRecordingPreview(view: StoryItemSetContainerComponent.View) { if self.recordedAudioPreview != nil { self.recordedAudioPreview = nil @@ -1255,7 +1256,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { view.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))) } } - + func performShareAction(view: StoryItemSetContainerComponent.View) { guard let component = view.component else { return @@ -1267,16 +1268,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let peerId = focusedItem.peerId else { return } - + if focusedItem.storyItem.isForwardingDisabled { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) let actionSheet = ActionSheetController(presentationData: presentationData) - + actionSheet.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Story_Context_CopyLink, color: .accent, action: { [weak self, weak view, weak actionSheet] in actionSheet?.dismissAnimated() - + guard let self, let view else { return } @@ -1289,7 +1290,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) ]) ]) - + actionSheet.dismissed = { [weak self, weak view] _ in guard let self, let view else { return @@ -1299,7 +1300,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } self.actionSheet = actionSheet view.updateIsProgressPaused() - + component.presentController(actionSheet, nil) } else { var preferredAction: ShareControllerPreferredAction? @@ -1309,7 +1310,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { |> deliverOnMainQueue).start(next: { link in if let link { UIPasteboard.general.string = link - + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) component.presentController(UndoOverlayController( presentationData: presentationData, @@ -1322,106 +1323,104 @@ 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)) } } - + func performPaidMessageAction(view: StoryItemSetContainerComponent.View, minStars: Int? = nil) { Task { @MainActor [weak view] in guard let view else { @@ -1440,22 +1439,22 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let peerId = focusedItem.peerId else { return } - + var inputText = NSAttributedString(string: "") switch inputPanelView.getSendMessageInput() { case let .text(text): inputText = text } - + guard let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View else { return } - + var minAmount: Int64 = 1 if let minMessagePrice = visibleItemView.liveChatState?.minMessagePrice { minAmount = minMessagePrice } - + var currentAmount: Int? = (self.currentLiveStreamMessageStars?.value).flatMap { Int($0) } if let minStars { if let currentAmountValue = currentAmount { @@ -1464,7 +1463,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { currentAmount = minStars } } - + let initialData = await ChatSendStarsScreen.initialDataLiveStreamMessage( context: component.context, peerId: peerId, @@ -1475,7 +1474,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view else { return } - + self.currentLiveStreamMessageStars = StarsAmount(value: amount, nanos: 0) view.state?.updated(transition: .spring(duration: 0.4)) } @@ -1489,7 +1488,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + func performShareTextAction(view: StoryItemSetContainerComponent.View, text: String) { guard let component = view.component else { return @@ -1497,31 +1496,29 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let controller = component.controller() else { return } - + 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)) } - + func performTranslateTextAction(view: StoryItemSetContainerComponent.View, text: String, entities: [MessageTextEntity]) { guard let component = view.component else { return } - + let _ = (component.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) |> take(1) |> deliverOnMainQueue).start(next: { [weak self, weak view] sharedData in @@ -1529,25 +1526,25 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } let peer = component.slice.effectivePeer - + let _ = self - + let translationSettings: TranslationSettings if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { translationSettings = current } else { translationSettings = TranslationSettings.defaultSettings } - + var showTranslateIfTopical = false if case let .channel(channel) = peer, !(channel.addressName ?? "").isEmpty { showTranslateIfTopical = true } - + let (_, language) = canTranslateText(context: component.context, text: text, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: showTranslateIfTopical, ignoredLanguages: translationSettings.ignoredLanguages) - + let _ = ApplicationSpecificNotice.incrementTranslationSuggestion(accountManager: component.context.sharedContext.accountManager, timestamp: Int32(Date().timeIntervalSince1970)).start() - + let translateController = TranslateScreen(context: component.context, forceTheme: defaultDarkPresentationTheme, text: text, entities: entities, canCopy: true, fromLanguage: language, ignoredLanguages: translationSettings.ignoredLanguages) translateController.pushController = { [weak view] c in guard let view, let component = view.component else { @@ -1561,10 +1558,10 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } component.controller()?.present(c, in: .window(.root)) } - + self.actionSheet = translateController view.updateIsProgressPaused() - + translateController.wasDismissed = { [weak self, weak view] in guard let self, let view else { return @@ -1572,11 +1569,11 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.actionSheet = nil view.updateIsProgressPaused() } - + component.controller()?.present(translateController, in: .window(.root)) }) } - + func performLookupTextAction(view: StoryItemSetContainerComponent.View, text: String) { guard let component = view.component else { return @@ -1586,38 +1583,38 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { controller.popoverPresentationController?.sourceView = window controller.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0)) window.rootViewController?.present(controller, animated: true) - + final class DeinitWatcher: NSObject { let f: () -> Void - + init(_ f: @escaping () -> Void) { self.f = f } - + deinit { f() } } - + self.lookupController = controller view.updateIsProgressPaused() - + objc_setAssociatedObject(controller, &ObjCKey_DeinitWatcher, DeinitWatcher { [weak self, weak view] in guard let self, let view else { return } - + self.lookupController = nil view.updateIsProgressPaused() }, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } - + func performCopyLinkAction(view: StoryItemSetContainerComponent.View) { guard let component = view.component else { return } - + let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id) |> deliverOnMainQueue).start(next: { [weak view] link in guard let view, let component = view.component else { @@ -1625,7 +1622,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } if let link { UIPasteboard.general.string = link - + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) component.presentController(UndoOverlayController( presentationData: presentationData, @@ -1637,13 +1634,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }) } - + private func clearInputText(view: StoryItemSetContainerComponent.View) { guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { return } inputPanelView.clearSendMessageInput(updateState: true) - + guard let component = view.component else { return } @@ -1653,11 +1650,11 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } component.storyItemSharedState.replyDrafts.removeValue(forKey: StoryId(peerId: peerId, id: focusedItem.storyItem.id)) } - + enum AttachMenuSubject { case `default` } - + func presentAttachmentMenu( view: StoryItemSetContainerComponent.View, subject: AttachMenuSubject @@ -1673,13 +1670,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { return } - + var inputText = NSAttributedString(string: "") switch inputPanelView.getSendMessageInput() { case let .text(text): inputText = text } - + let _ = (component.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) @@ -1690,19 +1687,19 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let peer else { return } - + let inputIsActive = !"".isEmpty - + self.currentInputMode = .text view.endEditing(true) - + var banSendText: (Int32, Bool)? var bannedSendPhotos: (Int32, Bool)? var bannedSendVideos: (Int32, Bool)? var bannedSendFiles: (Int32, Bool)? - + let _ = bannedSendFiles - + var canSendPolls = true if case let .user(peer) = peer, peer.botInfo == nil { canSendPolls = false @@ -1741,7 +1738,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { canSendPolls = false } } - + var availableButtons: [AttachmentButtonType] = [.gallery, .file] if banSendText == nil { availableButtons.append(.location) @@ -1750,9 +1747,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if canSendPolls { availableButtons.insert(.poll, at: max(0, availableButtons.count - 1)) } - + let isScheduledMessages = !"".isEmpty - + var peerType: AttachMenuBots.Bot.PeerFlags = [] if case let .user(user) = peer { if let _ = user.botInfo { @@ -1769,7 +1766,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { peerType = .group } } - + let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError> if !isScheduledMessages { buttons = component.context.engine.messages.attachMenuBots() @@ -1785,7 +1782,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { case .gift: initialButton = .gift*/ } - + if !"".isEmpty { for bot in attachMenuBots.reversed() { var peerType = peerType @@ -1796,7 +1793,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let button: AttachmentButtonType = .app(bot) if !bot.peerTypes.intersection(peerType).isEmpty { buttons.insert(button, at: 1) - + /*if case let .bot(botId, _, _) = subject { if initialButton == nil && bot.peer.id == botId { initialButton = button @@ -1806,18 +1803,18 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { allButtons.insert(button, at: 1) } } - + return (buttons, allButtons, initialButton) } } else { buttons = .single((availableButtons, availableButtons, .gallery)) } - + let dataSettings = component.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) return entry ?? GeneratedMediaStoreSettings.defaultSettings } - + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) let premiumGiftOptions: [CachedPremiumGiftOption] if !premiumConfiguration.isPremiumDisabled && premiumConfiguration.showPremiumGiftInAttachMenu, case let .user(user) = peer, !user.isPremium && !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { @@ -1826,45 +1823,35 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } else { premiumGiftOptions = [] } - + let _ = combineLatest(queue: Queue.mainQueue(), buttons, dataSettings).start(next: { [weak self, weak view] buttonsAndInitialButton, dataSettings in guard let self, let view, let component = view.component else { return } - + var (buttons, allButtons, initialButton) = buttonsAndInitialButton if !premiumGiftOptions.isEmpty { buttons.insert(.gift, at: 1) } let _ = allButtons - + guard let initialButton = initialButton else { return } - + let currentMediaController = Atomic(value: nil) let currentFilesController = Atomic(value: nil) let currentLocationController = Atomic(value: nil) - + 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 attachmentController = AttachmentController( context: component.context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: peer.id), buttons: buttons, - initialButton: initialButton, - makeEntityInputView: { [weak view] in - guard let view, let component = view.component else { - return nil - } - return EntityInputView( - context: component.context, - isDark: true, - areCustomEmojiEnabled: true //TODO:check custom emoji - ) - } + initialButton: initialButton ) attachmentController.didDismiss = { [weak self, weak view] in guard let self, let view else { @@ -1935,7 +1922,7 @@ 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 controller = component.context.sharedContext.makeAttachmentFileController(context: component.context, updatedPresentationData: updatedPresentationData, audio: false, bannedSendMedia: bannedSendFiles, presentGallery: { [weak self, weak view, weak attachmentController] in guard let self, let view else { return @@ -2008,7 +1995,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) completion(controller, controller.mediaPickerContext) - + let _ = currentLocationController.swap(controller) }) return true @@ -2031,9 +2018,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let (peers, _, silent, scheduleTime, text, _) = peers else { return } - + let targetPeer = peer - + var textEnqueueMessage: EnqueueMessage? if let text = text, text.length > 0 { var attributes: [EngineMessage.Attribute] = [] @@ -2052,11 +2039,11 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { var media: TelegramMediaContact? switch peer { case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + guard case let .user(contact) = contact, let phoneNumber = contact.phone else { continue } let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - + let phone = contactData.basicData.phoneNumbers[0].value media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil) case let .deviceContact(_, basicData): @@ -2064,17 +2051,17 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { continue } let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - + let phone = contactData.basicData.phoneNumbers[0].value media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil) } - + if let media = media { let message = EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) enqueueMessages.append(message) } } - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in guard let self else { return @@ -2085,7 +2072,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let dataSignal: Signal<(EnginePeer?, DeviceContactExtendedData?), NoError> switch peer { case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + guard case let .user(contact) = contact, let phoneNumber = contact.phone else { return } let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") @@ -2103,7 +2090,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + if let stableId = stableId { return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) |> take(1) @@ -2134,7 +2121,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { enqueueMessages.append(textEnqueueMessage) } enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in guard let self else { return @@ -2142,7 +2129,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.sendMessages(view: view, peer: targetPeer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime) }) } else { - let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: component.context), environment: ShareControllerAppEnvironment(sharedContext: component.context.sharedContext), subject: .filter(peer: peerAndContactData.0?._asPeer(), contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in + let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: component.context), environment: ShareControllerAppEnvironment(sharedContext: component.context.sharedContext), subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in guard let self, let view else { return } @@ -2152,13 +2139,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let phone = contactData.basicData.phoneNumbers[0].value if let vCardData = contactData.serializedVCard() { let media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: peer?.id, vCardData: vCardData) - + var enqueueMessages: [EnqueueMessage] = [] if let textEnqueueMessage = textEnqueueMessage { enqueueMessages.append(textEnqueueMessage) } enqueueMessages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])) - + self.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in guard let self else { return @@ -2188,7 +2175,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) completion(controller, controller.mediaPickerContext) strongSelf.controllerNavigationDisposable.set(nil) - + let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id).start() }*/ //TODO:gift controller @@ -2240,7 +2227,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.attachmentController = attachmentController view.updateIsProgressPaused() } - + if inputIsActive { Queue.mainQueue().after(0.15, { present() @@ -2251,7 +2238,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) } - + private func presentMediaPicker( view: StoryItemSetContainerComponent.View, peer: EnginePeer, @@ -2306,8 +2293,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view else { return } - self.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod in - done(time) + self.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod, silentPosting in + done(time, silentPosting) }) } controller.presentTimerPicker = { [weak self, weak view] done in @@ -2329,7 +2316,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } present(controller, mediaPickerContext) } - + private func presentOldMediaPicker(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, fileMode: Bool, editingMedia: Bool, push: @escaping (ViewController) -> Void, completion: @escaping ([Any], Bool, Int32) -> Void) { guard let component = view.component else { return @@ -2342,11 +2329,11 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { case let .text(text): inputText = text } - + let engine = component.context.engine let _ = (component.context.sharedContext.accountManager.transaction { transaction -> Signal<(GeneratedMediaStoreSettings, EngineConfiguration.SearchBots), NoError> in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) - + return engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) |> map { configuration -> (GeneratedMediaStoreSettings, EngineConfiguration.SearchBots) in return (entry ?? GeneratedMediaStoreSettings.defaultSettings, configuration) @@ -2363,13 +2350,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { selectionLimit = 10 slowModeEnabled = true } - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - + let _ = legacyAssetPicker(context: component.context, presentationData: presentationData, editingMedia: editingMedia, fileMode: fileMode, peer: peer._asPeer(), threadTitle: nil, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, selectionLimit: selectionLimit).start(next: { [weak self, weak view] generator in if let view, let component = view.component, let controller = component.controller() { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - + let legacyController = LegacyController(presentation: fileMode ? .navigation : .custom, theme: presentationData.theme, initialLayout: controller.currentlyAppliedLayout) legacyController.navigationPresentation = .modal legacyController.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style @@ -2378,15 +2365,15 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { legacyController?.view.disablesInteractiveModalDismiss = true } let controller = generator(legacyController.context) - + legacyController.bind(controller: controller) legacyController.deferScreenEdgeGestures = [.top] - + configureLegacyAssetPicker(controller, context: component.context, peer: peer._asPeer(), chatLocation: .peer(id: peer.id), initialCaption: inputText, hasSchedule: peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak view, weak legacyController] in if let view, let component = view.component { 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 controller = WebSearchController(context: component.context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { [weak view] results, selectionState, editingState, silentPosting in + let controller = WebSearchController(context: component.context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { [weak view] results, selectionState, editingState, silentPosting, scheduleTime in if let legacyController = legacyController { legacyController.dismiss() } @@ -2395,14 +2382,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak view] result in if let strongSelf = self, let view { - strongSelf.enqueueChatContextResult(view: view, peer: peer, replyMessageId: replyMessageId, storyId: replyToStoryId, results: results, result: result, hideVia: true) + strongSelf.enqueueChatContextResult(view: view, peer: peer, replyMessageId: replyMessageId, storyId: replyToStoryId, results: results, result: result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime) } }, enqueueMediaMessages: { [weak view] signals in if let strongSelf = self, let view { if editingMedia { strongSelf.editMessageMediaWithLegacySignals(view: view, signals: signals) } else { - strongSelf.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: replyMessageId, replyToStoryId: replyToStoryId, signals: signals, silentPosting: silentPosting) + strongSelf.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: replyMessageId, replyToStoryId: replyToStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) } } }) @@ -2428,8 +2415,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { component.controller()?.present(textAlertController(context: component.context, updatedPresentationData: (presentationData, .single(presentationData)), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }, presentSchedulePicker: { [weak view] media, done in if let strongSelf = self, let view { - strongSelf.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod in - done(time) + strongSelf.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod, silentPosting in + done(time, silentPosting) }) } }, presentTimerPicker: { [weak view] done in @@ -2462,7 +2449,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) } - + private func presentFileGallery(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, editingMessage: Bool = false) { self.presentOldMediaPicker(view: view, peer: peer, replyMessageId: replyMessageId, replyToStoryId: replyToStoryId, fileMode: true, editingMedia: editingMessage, push: { [weak view] c in view?.component?.controller()?.push(c) @@ -2477,7 +2464,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }) } - + private func presentICloudFileGallery(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, replyToStoryId: StoryId?) { guard let component = view.component else { return @@ -2493,9 +2480,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } let (accountPeer, limits, premiumLimits) = result let isPremium = accountPeer?.isPremium ?? false - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - + component.controller()?.present(legacyICloudFilePicker(theme: presentationData.theme, hasMultiselection: true, completion: { [weak self, weak view] urls in if let strongSelf = self, let view, !urls.isEmpty { var signals: [Signal] = [] @@ -2528,7 +2515,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + var groupingKey: Int64? var fileTypes: (music: Bool, other: Bool) = (false, false) if results.count > 1 { @@ -2546,14 +2533,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if fileTypes.music != fileTypes.other { groupingKey = Int64.random(in: Int64.min ... Int64.max) } - + var messages: [EnqueueMessage] = [] for item in results { if let item = item { let fileId = Int64.random(in: Int64.min ... Int64.max) let mimeType = guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension) var previewRepresentations: [TelegramMediaImageRepresentation] = [] - if mimeType.hasPrefix("image/") || mimeType == "application/pdf" { + if mimeType.hasPrefix("image/") || mimeType == "application/pdf" || item.audioMetadata?.hasAudioArtwork == true { previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } var attributes: [TelegramMediaFileAttribute] = [] @@ -2561,7 +2548,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if let audioMetadata = item.audioMetadata { attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil)) } - + let file = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes, alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }, replyToStoryId: replyToStoryId, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) messages.append(message) @@ -2570,7 +2557,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { groupingKey = Int64.random(in: Int64.min ... Int64.max) } } - + if !messages.isEmpty { strongSelf.presentPaidMessageAlertIfNeeded(view: view, completion: { [weak self] in guard let strongSelf = self else { @@ -2585,7 +2572,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }), in: .window(.root)) }) } - + func presentMediaPasteboard(view: StoryItemSetContainerComponent.View, subjects: [MediaPickerScreenImpl.Subject.Media]) { // guard let component = view.component else { // return @@ -2598,13 +2585,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { // guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { // return // } -// +// // var inputText = NSAttributedString(string: "") // switch inputPanelView.getSendMessageInput() { // case let .text(text): // inputText = text // } -// +// // let peer = component.slice.effectivePeer // let theme = defaultDarkPresentationTheme // let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) @@ -2653,12 +2640,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { // controller.navigationPresentation = .flatModal // component.controller()?.push(controller) } - - private func enqueueChatContextResult(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, storyId: StoryId?, results: ChatContextResultCollection, result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) { - if !canSendMessagesToPeer(peer._asPeer()) { + + private func enqueueChatContextResult(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, storyId: StoryId?, results: ChatContextResultCollection, result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, resetTextInputState: Bool = true) { + if !canSendMessagesToPeer(peer) { return } - + let sendMessage: (Int32?) -> Void = { [weak self, weak view] scheduleTime in guard let self, let view, let component = view.component else { return @@ -2677,15 +2664,15 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { postpone: false ) { } - + if let attachmentController = self.attachmentController { attachmentController.dismiss(animated: true) } } - - sendMessage(nil) + + sendMessage(scheduleTime) } - + private func presentWebSearch(view: StoryItemSetContainerComponent.View, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) { guard let component = view.component else { return @@ -2693,21 +2680,21 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let context = component.context let peer = component.slice.effectivePeer let storyId = component.slice.item.storyItem.id - + 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 _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) |> deliverOnMainQueue).start(next: { [weak self, weak view] configuration in if let self { - let controller = WebSearchController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: configuration, mode: .media(attachment: true, completion: { [weak self] results, selectionState, editingState, silentPosting in + let controller = WebSearchController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: configuration, mode: .media(attachment: true, completion: { [weak self] results, selectionState, editingState, silentPosting, scheduleTime in legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self, weak view] result in if let self, let view { - self.performSendContextResultAction(view: view, results: results, result: result) + self.performSendContextResultAction(view: view, results: results, result: result, silentPosting: silentPosting, scheduleTime: scheduleTime) } }, enqueueMediaMessages: { [weak self, weak view] signals in if let self, let view, !signals.isEmpty { - self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: StoryId(peerId: peer.id, id: storyId), signals: signals, silentPosting: false) + self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: StoryId(peerId: peer.id, id: storyId), signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) } }) }), activateOnDisplay: activateOnDisplay) @@ -2722,7 +2709,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } }) } - + private func getCaptionPanelView(view: StoryItemSetContainerComponent.View, peer: EnginePeer, mediaPicker: MediaPickerScreenImpl? = nil) -> TGCaptionPanelView? { guard let component = view.component else { return nil @@ -2758,14 +2745,19 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { view.updateIsProgressPaused() } view.component?.controller()?.push(c) - + view.updateIsProgressPaused() } else { view.component?.controller()?.presentInGlobalOverlay(c) } + }, getNavigationController: { [weak view] in + guard let controller = view?.component?.controller() else { + return nil + } + return controller.navigationController as? NavigationController }) as? TGCaptionPanelView } - + private func openCamera(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyToMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, cameraView: TGAttachmentCameraView? = nil) { guard let component = view.component else { return @@ -2773,13 +2765,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else { return } - + var inputText = NSAttributedString(string: "") switch inputPanelView.getSendMessageInput() { case let .text(text): inputText = text } - + let _ = (component.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) return entry ?? GeneratedMediaStoreSettings.defaultSettings @@ -2788,17 +2780,17 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let component = view.component, let parentController = component.controller() else { return } - + var enablePhoto = true var enableVideo = true - + if let callManager = component.context.sharedContext.callManager, callManager.hasActiveCall { enableVideo = false } - + var bannedSendPhotos: (Int32, Bool)? var bannedSendVideos: (Int32, Bool)? - + if case let .channel(channel) = peer { if let value = channel.hasBannedPermission(.banSendPhotos) { bannedSendPhotos = value @@ -2814,16 +2806,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { bannedSendVideos = (Int32.max, false) } } - + if bannedSendPhotos != nil { enablePhoto = false } if bannedSendVideos != nil { enableVideo = false } - + let storeCapturedMedia = peer.id.namespace != Namespaces.Peer.SecretChat - + presentedLegacyCamera(context: component.context, peer: peer._asPeer(), chatLocation: .peer(id: peer.id), cameraView: cameraView, menuController: nil, parentController: parentController, attachmentController: self.attachmentController, editingMedia: false, saveCapturedPhotos: storeCapturedMedia, mediaGrouping: true, initialCaption: inputText, hasSchedule: peer.id.namespace != Namespaces.Peer.SecretChat, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self, weak view] signals, silentPosting, scheduleTime, parameters in guard let self, let view else { return @@ -2842,8 +2834,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view else { return } - self.presentScheduleTimePicker(view: view, peer: peer, style: .media, completion: { time, repeatPeriod in - done(time) + self.presentScheduleTimePicker(view: view, peer: peer, style: .media, completion: { time, repeatPeriod, silentPosting in + done(time, silentPosting) }) }, presentTimerPicker: { [weak self, weak view] done in guard let self, let view else { @@ -2870,14 +2862,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) }) } - + private func presentScheduleTimePicker( view: StoryItemSetContainerComponent.View, peer: EnginePeer, style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, - completion: @escaping (Int32, Int32?) -> Void + completion: @escaping (Int32, Int32?, Bool) -> Void ) { guard let component = view.component else { return @@ -2889,7 +2881,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let view, let component = view.component else { return } - + var sendWhenOnlineAvailable = false if let presence, case .present = presence.status { sendWhenOnlineAvailable = true @@ -2897,43 +2889,52 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { sendWhenOnlineAvailable = false } - - let mode: ChatScheduleTimeControllerMode + + let mode: ChatScheduleTimeScreen.Mode //ChatScheduleTimeControllerMode if peer.id == component.context.account.peerId { mode = .reminders } else { - mode = .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable) + mode = .scheduledMessages(peerId: peer.id, sendWhenOnlineAvailable: sendWhenOnlineAvailable) } - let theme = component.theme - let controller = ChatScheduleTimeController(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), mode: mode, style: style, currentTime: selectedTime, minimalTime: nil, dismissByTapOutside: dismissByTapOutside, completion: { time in - completion(time, nil) - }) + let controller = ChatScheduleTimeScreen( + context: component.context, + mode: mode, + currentTime: selectedTime, + currentRepeatPeriod: nil, + suggestedTime: nil, + minimalTime: nil, + silentPosting: false, + isDark: style == .media, + completion: { result in + completion(result.time, nil, result.silentPosting) + } + ) view.endEditing(true) view.component?.controller()?.present(controller, in: .window(.root)) }) } - - private func presentTimerPicker(view: StoryItemSetContainerComponent.View, peer: EnginePeer, style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { + + private func presentTimerPicker(view: StoryItemSetContainerComponent.View, peer: EnginePeer, style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, completion: @escaping (Int32) -> Void) { guard let component = view.component else { return } let theme = component.theme - let controller = ChatTimerScreen(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), style: style, currentTime: selectedTime, dismissByTapOutside: dismissByTapOutside, completion: { time in + let controller = ChatTimerScreen(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), style: style, currentTime: selectedTime, completion: { time in completion(time) }) view.endEditing(true) component.controller()?.present(controller, in: .window(.root)) } - + private func transformEnqueueMessages(view: StoryItemSetContainerComponent.View, messages: [EnqueueMessage], silentPosting: Bool, scheduleTime: Int32? = nil) -> [EnqueueMessage] { var focusedStoryId: StoryId? if let component = view.component, let peerId = component.slice.item.peerId { focusedStoryId = StoryId(peerId: peerId, id: component.slice.item.storyItem.id) } - + return messages.map { message in var message = message - + if let focusedStoryId { switch message { case let .message(text, attributes, inlineStickers, mediaReference, threadId, replyToMessageId, _, localGroupingKey, correlationId, bubbleUpEmojiOrStickersets): @@ -2944,7 +2945,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { break } } - + return message.withUpdatedAttributes { attributes in var attributes = attributes if silentPosting || scheduleTime != nil { @@ -2970,7 +2971,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + private func sendMessages(view: StoryItemSetContainerComponent.View, peer: EnginePeer, messages: [EnqueueMessage], silentPosting: Bool = false, scheduleTime: Int32? = nil) { guard let component = view.component else { return @@ -2983,29 +2984,29 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } }) - + donateSendMessageIntent(account: component.context.account, sharedContext: component.context.sharedContext, intentContext: .chat, peerIds: [peer.id]) - + if let attachmentController = self.attachmentController { attachmentController.dismiss(animated: true) } } - + private func enqueueMediaMessages(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyToMessageId: EngineMessage.Id?, replyToStoryId: StoryId?, signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, parameters: ChatSendMessageActionSheetController.SendParameters? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { guard let component = view.component else { return } - + self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: component.context, account: component.context.account, signals: signals!) |> deliverOnMainQueue).start(next: { [weak self, weak view] items in if let strongSelf = self, let view { var mappedMessages: [EnqueueMessage] = [] var addedTransitions: [(Int64, [String], () -> Void)] = [] - + var groupedCorrelationIds: [Int64: Int64] = [:] - + var skipAddingTransitions = false - + for item in items { var message = item.message if message.groupingKey != nil { @@ -3015,7 +3016,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } else if items.count > 3 { skipAddingTransitions = true } - + if let uniqueId = item.uniqueId, !item.isFile && !skipAddingTransitions { let correlationId: Int64 var addTransition = scheduleTime == nil @@ -3062,12 +3063,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } strongSelf.sendMessages(view: view, peer: peer, messages: mappedMessages.map { $0.withUpdatedReplyToMessageId(replyToMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil, innerSubject: nil) }).withUpdatedReplyToStoryId(replyToStoryId) }, silentPosting: silentPosting, scheduleTime: scheduleTime) - + completion() } })) } - + private func editMessageMediaWithLegacySignals(view: StoryItemSetContainerComponent.View, signals: [Any]) { guard let component = view.component else { return @@ -3075,12 +3076,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let _ = (legacyAssetPickerEnqueueMessages(context: component.context, account: component.context.account, signals: signals) |> deliverOnMainQueue).start() } - + func openResolved(view: StoryItemSetContainerComponent.View, result: ResolvedUrl, forceExternal: Bool = false, concealed: Bool = false, completion: (() -> Void)? = nil) { guard let component = view.component, let navigationController = component.controller()?.navigationController as? NavigationController else { return } - + self.progressPauseContext.update = { [weak self, weak view] controller in guard let self, let view else { return @@ -3088,7 +3089,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.progressPauseContext.externalController = controller view.updateIsProgressPaused() } - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) let peerId = component.slice.effectivePeer.id @@ -3117,7 +3118,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { })) } case .info: - let _ = (context.account.postbox.loadedPeerWithId(peerId.id) + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId.id)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> take(1) |> deliverOnMainQueue).start(next: { [weak navigationController] peer in if peer.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) == nil { @@ -3146,7 +3154,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } controller.present(c, in: .window(.root), with: a) - }, + }, dismissInput: { [weak view] in guard let view else { return @@ -3158,7 +3166,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { completion: completion ) } - + func navigateToMessage(view: StoryItemSetContainerComponent.View, messageId: EngineMessage.Id, completion: (() -> Void)?) { guard let component = view.component else { return @@ -3174,14 +3182,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { completion?() }) } - + func openPeerMention(view: StoryItemSetContainerComponent.View, name: String, sourceMessageId: MessageId? = nil) { guard let component = view.component, let parentController = component.controller() else { return } let disposable = self.resolvePeerByNameDisposable var resolveSignal = component.context.engine.peers.resolvePeerByName(name: name, referrer: nil, ageLimit: 10) - + var cancelImpl: (() -> Void)? let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let progressSignal = Signal { [weak self, weak view, weak parentController] subscriber in @@ -3189,14 +3197,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { cancelImpl?() })) parentController?.present(controller, in: .window(.root)) - + self?.statusController = controller view?.updateIsProgressPaused() - + return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() - + self?.statusController = nil view?.updateIsProgressPaused() } @@ -3205,7 +3213,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { |> runOn(Queue.mainQueue()) |> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() - + resolveSignal = resolveSignal |> afterDisposed { Queue.mainQueue().async { @@ -3246,14 +3254,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } })) } - + func openHashtag(view: StoryItemSetContainerComponent.View, hashtag: String, peerName: String?) { guard let component = view.component, let parentController = component.controller() else { return } - + let peerId = component.slice.effectivePeer.id - + var resolveSignal: Signal if let peerName = peerName { resolveSignal = component.context.engine.peers.resolvePeerByName(name: peerName, referrer: nil) @@ -3271,8 +3279,15 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } else { - resolveSignal = component.context.account.postbox.loadedPeerWithId(peerId) - |> map(Optional.init) + resolveSignal = component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + |> map { Optional($0._asPeer()) } } var cancelImpl: (() -> Void)? let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } @@ -3281,14 +3296,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { cancelImpl?() })) parentController?.present(controller, in: .window(.root)) - + self?.statusController = controller view?.updateIsProgressPaused() - + return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() - + self?.statusController = nil view?.updateIsProgressPaused() } @@ -3297,7 +3312,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { |> runOn(Queue.mainQueue()) |> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() - + resolveSignal = resolveSignal |> afterDisposed { Queue.mainQueue().async { @@ -3329,7 +3344,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } })) } - + func openPeerMention(view: StoryItemSetContainerComponent.View, peerId: EnginePeer.Id) { guard let component = view.component else { return @@ -3342,13 +3357,21 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.openPeer(view: view, peer: peer) }) } - + func openPeer(view: StoryItemSetContainerComponent.View, peer: EnginePeer, expandAvatar: Bool = false, peerTypes: ReplyMarkupButtonAction.PeerTypes? = nil) { guard let component = view.component else { return } - - let peerSignal: Signal = component.context.account.postbox.loadedPeerWithId(peer.id) |> map(Optional.init) + + let peerSignal: Signal = component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + |> map(Optional.init) self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { [weak view] peer in guard let view, let component = view.component, let peer else { return @@ -3366,16 +3389,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } })) } - + func presentTextEntityActions(view: StoryItemSetContainerComponent.View, action: StoryContentCaptionComponent.Action, openUrl: @escaping (String, Bool) -> Void) { guard let component = view.component else { return } - + let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: component.theme, fontSize: .regular), allowInputInset: false) - + var canOpenIn = false - + let title: String let value: String var openAction: String? = component.strings.Conversation_LinkDialogOpen @@ -3405,10 +3428,10 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { case .customEmoji: return } - + var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: title)) - + if let openAction { items.append(ActionSheetButtonItem(title: openAction, color: .accent, action: { [weak self, weak view, weak actionSheet] in actionSheet?.dismissAnimated() @@ -3438,19 +3461,19 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } })) } - + items.append(ActionSheetButtonItem(title: copyAction, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() UIPasteboard.general.string = value })) - + if case let .url(url, _) = action, let link = URL(string: url) { items.append(ActionSheetButtonItem(title: component.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) })) } - + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: component.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() @@ -3463,18 +3486,18 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.actionSheet = nil view.updateIsProgressPaused() } - + component.controller()?.present(actionSheet, in: .window(.root)) - + self.actionSheet = actionSheet view.updateIsProgressPaused() } - + func openAttachedStickers(view: StoryItemSetContainerComponent.View, packs: Signal<[StickerPackReference], NoError>) { guard let component = view.component else { return } - + guard let parentController = component.controller() as? StoryContainerScreen else { return } @@ -3483,14 +3506,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let progressSignal = Signal { [weak parentController, weak self, weak view] subscriber in let progressController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) parentController?.present(progressController, in: .window(.root), with: nil) - + self?.statusController = progressController view?.updateIsProgressPaused() - + return ActionDisposable { [weak progressController] in Queue.mainQueue().async() { progressController?.dismiss() - + self?.statusController = nil view?.updateIsProgressPaused() } @@ -3499,7 +3522,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { |> runOn(Queue.mainQueue()) |> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() - + let signal = packs |> afterDisposed { Queue.mainQueue().async { @@ -3537,16 +3560,16 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) parentController?.present(controller, in: .window(.root), with: nil) }) - + self.isViewingAttachedStickers = true view.updateIsProgressPaused() } - + func requestStealthMode(view: StoryItemSetContainerComponent.View) { guard let component = view.component else { return } - + let _ = (component.context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState(), TelegramEngine.EngineData.Item.Configuration.App() @@ -3555,11 +3578,11 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let component = view.component, let controller = component.controller() else { return } - + let timestamp = Int32(Date().timeIntervalSince1970) if let activeUntilTimestamp = config.stealthModeState.actualizedNow().activeUntilTimestamp, activeUntilTimestamp > timestamp { let remainingActiveSeconds = activeUntilTimestamp - timestamp - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) let text = component.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string let tooltipScreen = UndoOverlayController( @@ -3583,24 +3606,24 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.currentTooltipUpdateTimer = nil return } - + let timestamp = Int32(Date().timeIntervalSince1970) let remainingActiveSeconds = max(1, activeUntilTimestamp - timestamp) - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) let text = component.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string tooltipScreenValue.content = .actionSucceeded(title: component.strings.Story_ToastStealthModeActiveTitle, text: text, cancel: "", destructive: false) }) - + self.tooltipScreen?.dismiss(animated: true) self.tooltipScreen = tooltipScreen controller.present(tooltipScreen, in: .current) - + view.updateIsProgressPaused() - + return } - + let pastPeriod: Int32 let futurePeriod: Int32 if let data = appConfig.data, let futurePeriodF = data["stories_stealth_future_period"] as? Double, let pastPeriodF = data["stories_stealth_past_period"] as? Double { @@ -3610,7 +3633,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { pastPeriod = 5 * 60 futurePeriod = 25 * 60 } - + let sheet = StoryStealthModeSheetScreen( context: component.context, mode: .control(external: false, cooldownUntilTimestamp: config.stealthModeState.actualizedNow().cooldownUntilTimestamp), @@ -3621,13 +3644,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let component = view.component else { return } - + let _ = (component.context.engine.messages.enableStoryStealthMode() |> deliverOnMainQueue).start(completed: { [weak self, weak view] in guard let self, let view, let component = view.component, let controller = component.controller() else { return } - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) let text = component.strings.Story_ToastStealthModeActivatedText(timeIntervalString(strings: presentationData.strings, value: pastPeriod), timeIntervalString(strings: presentationData.strings, value: futurePeriod)).string let tooltipScreen = UndoOverlayController( @@ -3642,9 +3665,9 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.tooltipScreen?.dismiss(animated: true) self.tooltipScreen = tooltipScreen controller.present(tooltipScreen, in: .current) - + view.updateIsProgressPaused() - + HapticFeedback().success() }) } @@ -3661,12 +3684,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { controller.push(sheet) }) } - + func presentStealthModeUpgrade(view: StoryItemSetContainerComponent.View, action: @escaping () -> Void) { guard let component = view.component else { return } - + let _ = (component.context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.StoryConfigurationState(), TelegramEngine.EngineData.Item.Configuration.App() @@ -3675,7 +3698,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view, let component = view.component, let controller = component.controller() else { return } - + let pastPeriod: Int32 let futurePeriod: Int32 if let data = appConfig.data, let futurePeriodF = data["stories_stealth_future_period"] as? Double, let pastPeriodF = data["stories_stealth_past_period"] as? Double { @@ -3685,7 +3708,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { pastPeriod = 5 * 60 futurePeriod = 25 * 60 } - + let sheet = StoryStealthModeSheetScreen( context: component.context, mode: .upgrade, @@ -3708,12 +3731,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { controller.push(sheet) }) } - + func presentQualityUpgrade(view: StoryItemSetContainerComponent.View, action: @escaping () -> Void) { guard let component = view.component, let controller = component.controller() else { return } - + let sheet = StoryQualityUpgradeSheetScreen( context: component.context, buttonAction: { @@ -3731,20 +3754,20 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { view.updateIsProgressPaused() controller.push(sheet) } - + private var selectedMediaArea: MediaArea? func activateMediaArea(view: StoryItemSetContainerComponent.View, mediaArea: MediaArea, position: CGPoint? = nil, immediate: Bool = false) { guard let component = view.component, let controller = component.controller() else { return } - + let theme = defaultDarkColorPresentationTheme 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 context = component.context - + var useGesturePosition = false - + var actions: [ContextMenuAction] = [] switch mediaArea { case let .venue(coordinates, venue): @@ -3863,18 +3886,18 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { action() })) } - + self.selectedMediaArea = mediaArea - + let referenceSize = view.controlsContainerView.frame.size let size = CGSize(width: 16.0, height: mediaArea.coordinates.height / 100.0 * referenceSize.height * 1.1) var frame = CGRect(x: mediaArea.coordinates.x / 100.0 * referenceSize.width - size.width / 2.0, y: mediaArea.coordinates.y / 100.0 * referenceSize.height - size.height / 2.0, width: size.width, height: size.height) frame = view.controlsContainerView.convert(frame, to: nil) - + if useGesturePosition, let position { frame = CGRect(origin: position.offsetBy(dx: 0.0, dy: 44.0), size: .zero) } - + let node = controller.displayNode let menuController = makeContextMenuController(actions: actions, blurred: true) menuController.centerHorizontally = true @@ -3905,17 +3928,17 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.menuController = menuController view.updateIsProgressPaused() } - + func activateInlineReaction(view: StoryItemSetContainerComponent.View, reactionView: UIView, reaction: MessageReaction.Reaction) { guard let component = view.component else { return } - + let animateWithReactionItem: (ReactionItem) -> Void = { [weak self, weak view] reactionItem in guard let self, let view else { return } - + self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak view] in guard let view, let component = view.component else { return @@ -3923,26 +3946,26 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if component.slice.effectivePeer.id != component.context.account.peerId { let _ = component.context.engine.messages.setStoryReaction(peerId: component.slice.effectivePeer.id, id: component.slice.item.storyItem.id, reaction: reaction).start() } - + let targetFrame = reactionView.convert(reactionView.bounds, to: view) - + let targetView = UIView(frame: targetFrame) targetView.isUserInteractionEnabled = false view.addSubview(targetView) - + let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false) view.componentContainerView.addSubview(standaloneReactionAnimation.view) - + if let standaloneReactionAnimation = view.standaloneReactionAnimation { view.standaloneReactionAnimation = nil - + let standaloneReactionAnimationView = standaloneReactionAnimation.view standaloneReactionAnimation.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimationView] _ in standaloneReactionAnimationView?.removeFromSuperview() }) } view.standaloneReactionAnimation = standaloneReactionAnimation - + standaloneReactionAnimation.frame = view.bounds standaloneReactionAnimation.animateReactionSelection( context: component.context, @@ -3958,13 +3981,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let view else { return } - + if let standaloneReactionAnimation = view.standaloneReactionAnimation { view.standaloneReactionAnimation = nil standaloneReactionAnimation.view.removeFromSuperview() } view.standaloneReactionAnimation = standaloneReactionAnimation - + standaloneReactionAnimation.frame = view.bounds view.componentContainerView.addSubview(standaloneReactionAnimation.view) }, @@ -3975,7 +3998,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { ) }) } - + switch reaction { case .builtin: if let availableReactions = component.availableReactions { @@ -4015,7 +4038,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + func openSendStars(view: StoryItemSetContainerComponent.View) { Task { @MainActor [weak view] in guard let view else { @@ -4031,7 +4054,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let peerId = focusedItem.peerId else { return } - + var topPeers: [ReactionsMessageAttribute.TopPeer] = [] var minAmount: Int64 = 1 var sendAsPeer: SendAsPeer? @@ -4047,11 +4070,11 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { ) } } - + if let minMessagePrice = visibleItemView.liveChatState?.minMessagePrice { minAmount = minMessagePrice } - + if let currentSendAsPeer = self.currentSendAsPeer { sendAsPeer = currentSendAsPeer } else { @@ -4060,12 +4083,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + let initialData = await ChatSendStarsScreen.initialData( context: component.context, peerId: peerId, - myPeer: (sendAsPeer?.peer).flatMap(EnginePeer.init), - reactSubject: .liveStream(peerId: peerId, storyId: focusedItem.storyItem.id, minAmount: Int(minAmount), liveChatMessageParams: LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 })), availableSendAsPeers: component.isEmbeddedInCamera ? [] : (self.sendAsData?.availablePeers.map({ EnginePeer($0.peer) }) ?? []), isDisplayOnly: component.isEmbeddedInCamera), + myPeer: sendAsPeer?.peer, + reactSubject: .liveStream(peerId: peerId, storyId: focusedItem.storyItem.id, minAmount: Int(minAmount), liveChatMessageParams: LiveChatMessageParams(appConfig: component.context.currentAppConfiguration.with({ $0 })), availableSendAsPeers: component.isEmbeddedInCamera ? [] : (self.sendAsData?.availablePeers.map({ $0.peer }) ?? []), isDisplayOnly: component.isEmbeddedInCamera), topPeers: topPeers, completion: { [weak self, weak view] amount, privacy, _, _ in guard let self, let view else { @@ -4090,30 +4113,30 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + func performSendStars(view: StoryItemSetContainerComponent.View, buttonView: UIView?, count: Int, isFromExpandedView: Bool) { Task { @MainActor [weak self, weak view] in guard let self, let view, let component = view.component else { return } - + guard let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View else { return } - + if isFromExpandedView { if let currentSendStarsUndoController = self.currentSendStarsUndoController { self.currentSendStarsUndoController = nil currentSendStarsUndoController.dismiss() } - + self.commitSendStars(view: view, count: count, delay: false) } else { let starsContextState = await component.context.starsContext?.state.get() guard let balance = starsContextState?.balance else { return } - + var totalExpectedStars = count if let pendingMyStars = visibleItemView.liveChatState?.starStats?.pendingMyStars, pendingMyStars > 0 { totalExpectedStars += Int(pendingMyStars) @@ -4125,7 +4148,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let targetPeerId = component.slice.item.peerId else { return } - + let customTheme = component.theme let options = await component.context.engine.payments.starsTopUpOptions().get() let controller = component.context.sharedContext.makeStarsPurchaseScreen( @@ -4138,10 +4161,10 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { completion: { _ in } ) navigationController.pushViewController(controller) - + return } - + var reactionItem: ReactionItem? if let availableReactions = await component.context.availableReactions.get() { for item in availableReactions.reactions { @@ -4152,7 +4175,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let aroundAnimation = item.aroundAnimation else { continue } - + reactionItem = ReactionItem( reaction: ReactionItem.Reaction(rawValue: item.value), appearAnimation: item.appearAnimation, @@ -4167,27 +4190,27 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + if let reactionItem, let buttonView { let targetFrame = buttonView.convert(buttonView.bounds, to: view) - + let targetView = UIView(frame: targetFrame) targetView.isUserInteractionEnabled = false view.addSubview(targetView) - + let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false) view.componentContainerView.addSubview(standaloneReactionAnimation.view) - + if let standaloneReactionAnimation = view.standaloneReactionAnimation { view.standaloneReactionAnimation = nil - + let standaloneReactionAnimationView = standaloneReactionAnimation.view standaloneReactionAnimation.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak standaloneReactionAnimationView] _ in standaloneReactionAnimationView?.removeFromSuperview() }) } view.standaloneReactionAnimation = standaloneReactionAnimation - + standaloneReactionAnimation.frame = view.bounds standaloneReactionAnimation.animateReactionSelection( context: component.context, @@ -4203,13 +4226,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let view else { return } - + if let standaloneReactionAnimation = view.standaloneReactionAnimation { view.standaloneReactionAnimation = nil standaloneReactionAnimation.view.removeFromSuperview() } view.standaloneReactionAnimation = standaloneReactionAnimation - + standaloneReactionAnimation.frame = view.bounds view.componentContainerView.addSubview(standaloneReactionAnimation.view) }, @@ -4219,7 +4242,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } ) } - + self.currentLiveStreamStarsIsActive = true self.currentLiveStreamStarsIsActiveTimer?.invalidate() self.currentLiveStreamStarsIsActiveTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self, weak view] _ in @@ -4229,7 +4252,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.currentLiveStreamStarsIsActive = false view.state?.updated(transition: .spring(duration: 0.4)) }) - + var totalStars = 0 if let pendingMyStars = visibleItemView.liveChatState?.starStats?.pendingMyStars, pendingMyStars > 0 { totalStars += count @@ -4240,10 +4263,10 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { var count = count count = max(Int(minAmount), count) totalStars += count - + self.commitSendStars(view: view, count: count, delay: true) } - + let title: String /*if case .anonymous = privacy { title = self.presentationData.strings.Chat_ToastStarsSent_AnonymousTitle(Int32(self.currentSendStarsUndoCount)) @@ -4253,12 +4276,12 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } else*/ do { title = component.strings.Chat_ToastStarsSent_Title(Int32(totalStars)) } - + let textItems = AnimatedTextComponent.extractAnimatedTextString(string: component.strings.Chat_ToastStarsSent_Text("", ""), id: "text", mapping: [ 0: .number(totalStars, minDigits: 1), 1: .text(component.strings.Chat_ToastStarsSent_TextStarAmount(Int32(totalStars))) ]) - + if let current = self.currentSendStarsUndoController { current.content = .starsSent(context: component.context, title: title, text: textItems, hasUndo: true) } else { @@ -4290,7 +4313,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + private func commitSendStars(view: StoryItemSetContainerComponent.View, count: Int, delay: Bool) { guard let component = view.component else { return @@ -4314,7 +4337,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return self.sendAsData?.availablePeers.first(where: { $0.peer.id == defaultSendAs }) } } - + if liveChatStateValue.isAdmin { if let sendAsPeer { isAdmin = sendAsPeer.peer.id == component.context.account.peerId @@ -4323,10 +4346,10 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } } - + call.sendStars(fromId: sendAsPeer?.peer.id, isAdmin: isAdmin, amount: Int64(count), delay: delay) } - + func openSendAsSelection(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) { guard let component = view.component, let sendAsData = self.sendAsData, let controller = component.controller() else { return @@ -4334,7 +4357,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let visibleItemView = view.visibleItems[component.slice.item.id]?.view.view as? StoryItemContentComponent.View else { return } - + var currentSendAsPeer: SendAsPeer? if let currentSendAsPeerValue = self.currentSendAsPeer { currentSendAsPeer = currentSendAsPeerValue @@ -4343,13 +4366,13 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return self.sendAsData?.availablePeers.first(where: { $0.peer.id == defaultSendAs }) } } - + let focusedItem = component.slice.item guard let peerId = focusedItem.peerId else { return } let isPremium = sendAsData.isPremium - + var items: [ContextMenuItem] = [] items.append(.custom(ChatSendAsPeerTitleContextItem(text: component.strings.Conversation_SendMesageAs.uppercased()), false)) items.append(.custom(ChatSendAsPeerListContextItem( @@ -4372,7 +4395,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { return } HapticFeedback().impact() - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) controller.present(UndoOverlayController(presentationData: presentationData, content: .invitedToVoiceChat(context: component.context, peer: peer, title: nil, text: presentationData.strings.Conversation_SendMesageAsPremiumInfo, action: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, duration: 3), elevatedLayout: false, action: { [weak view] action in guard let view, let component = view.component, let controller = component.controller() else { @@ -4380,7 +4403,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } if case .undo = action { view.endEditing(true) - + let introScreen = PremiumIntroScreen(context: component.context, source: .settings) controller.push(introScreen) } @@ -4389,25 +4412,25 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }), false )) - + final class ReferenceSource: ContextReferenceContentSource { let controller: ViewController let sourceView: UIView let insets: UIEdgeInsets let contentInsets: UIEdgeInsets - + init(controller: ViewController, sourceView: UIView, insets: UIEdgeInsets, contentInsets: UIEdgeInsets = UIEdgeInsets()) { self.controller = controller self.sourceView = sourceView self.insets = insets self.contentInsets = contentInsets } - + func transitionInfo() -> ContextControllerReferenceViewInfo? { return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets, actionsPosition: .top) } } - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceSource(controller: controller, sourceView: sourceView, insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0))), items: .single(ContextController.Items(content: .list(items))), gesture: gesture, workaroundUseLegacyImplementation: false) contextController.dismissed = { [weak self, weak view] in @@ -4418,19 +4441,19 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { view.state?.updated(transition: .spring(duration: 0.4)) } controller.presentInGlobalOverlay(contextController) - + self.isSelectingSendAsPeer = true view.state?.updated(transition: .spring(duration: 0.4)) } - + func displayLiveStreamSettings(view: StoryItemSetContainerComponent.View) { Task { @MainActor [weak self, weak view] in guard let self, let view, let component = view.component, let controller = component.controller(), let mediaStreamCall = view.mediaStreamCall else { return } - + let callState = await (mediaStreamCall.state |> take(1)).get() - + let stateContext = LiveStreamSettingsScreen.StateContext( context: component.context, mode: .edit( @@ -4467,7 +4490,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { public class StoryProgressPauseContext { fileprivate weak var externalController: ViewController? public fileprivate(set) var update: (ViewController?) -> Void = { _ in } - + var hasExternalController: Bool { return self.externalController != nil } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index f5b2a60cb2..1abccc6051 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -377,6 +377,7 @@ public final class StoryPeerListComponent: Component { self.scrollView.alwaysBounceVertical = false self.scrollView.alwaysBounceHorizontal = true self.scrollView.clipsToBounds = false + self.scrollView.scrollsToTop = false self.scrollContainerView = UIView() self.scrollContainerView.clipsToBounds = true @@ -1689,14 +1690,15 @@ public final class StoryPeerListComponent: Component { NSAttributedString.Key.font: Font.semibold(17.0), NSAttributedString.Key.foregroundColor: component.theme.rootController.navigationBar.primaryTextColor ]) - var boundingRect = attributedText.boundingRect(with: CGSize(width: max(0.0, component.maxTitleX - component.minTitleX - 30.0), height: 100.0), options: .usesLineFragmentOrigin, context: nil) - boundingRect.size.width = ceil(boundingRect.size.width) - boundingRect.size.height = ceil(boundingRect.size.height) + + let cachedLayout = TextNode.calculateLayout(attributedString: attributedText, minimumNumberOfLines: 1, maximumNumberOfLines: 1, truncationType: .end, backgroundColor: nil, constrainedSize: CGSize(width: max(0.0, component.maxTitleX - component.minTitleX - 46.0), height: 100.0), alignment: .left, verticalAlignment: .middle, lineSpacingFactor: 0.0, cutout: nil, insets: UIEdgeInsets(), lineColor: nil, textShadowColor: nil, textShadowBlur: nil, textStroke: nil, displaySpoilers: false, displayEmbeddedItemsUnderSpoilers: false, customTruncationToken: nil) - let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: boundingRect.size)) + let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: cachedLayout.size)) let image = renderer.image { context in UIGraphicsPushContext(context.cgContext) - attributedText.draw(at: CGPoint()) + + TextNode.draw(CGRect(origin: CGPoint(), size: cachedLayout.size), withParameters: TextNode.DrawingParameters(cachedLayout: cachedLayout, renderContentTypes: .all), isCancelled: { return false }, isRasterizing: false) + UIGraphicsPopContext() } self.titleView.image = image diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index cc012a79f3..99d913e031 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -965,7 +965,7 @@ public final class StoryPeerListItemComponent: Component { var titleTransition = transition if let previousAnimation = previousComponent?.ringAnimation, case .progress = previousAnimation, component.ringAnimation == nil { if let titleView = self.title.view, let snapshotView = titleView.snapshotView(afterScreenUpdates: false) { - self.button.addSubview(snapshotView) + self.titleContainer.addSubview(snapshotView) snapshotView.frame = titleView.frame snapshotView.layer.animateAlpha(from: component.expandedAlphaFraction, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() diff --git a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/BUILD b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/BUILD index 49b18603a1..2ec4c22c32 100644 --- a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/BUILD +++ b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/BUILD @@ -15,7 +15,6 @@ swift_library( "//submodules/TelegramPresentationData", "//submodules/AvatarNode", "//submodules/SSignalKit/SwiftSignalKit", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/PhotoResources", "//submodules/AccountContext", diff --git a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift index 2ff507bc33..446e8d1910 100644 --- a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift @@ -4,7 +4,6 @@ import Display import ComponentFlow import TelegramPresentationData import TelegramCore -import Postbox import SwiftSignalKit import AccountContext import PhotoResources @@ -183,7 +182,7 @@ public final class StorySetIndicatorComponent: Component { init(context: AccountContext, item: StorySetIndicatorComponent.Item, displayAvatars: Bool, updated: @escaping () -> Void) { self.updated = updated - let peerReference = PeerReference(item.peer._asPeer()) + let peerReference = PeerReference(item.peer) var messageMedia: EngineMedia? switch item.storyItem.media { diff --git a/submodules/TelegramUI/Components/SubcodecAnimationCacheImpl/BUILD b/submodules/TelegramUI/Components/SubcodecAnimationCacheImpl/BUILD new file mode 100644 index 0000000000..cf937fcc4c --- /dev/null +++ b/submodules/TelegramUI/Components/SubcodecAnimationCacheImpl/BUILD @@ -0,0 +1,22 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SubcodecAnimationCacheImpl", + module_name = "SubcodecAnimationCacheImpl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/CryptoUtils:CryptoUtils", + "//submodules/ManagedFile:ManagedFile", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//third-party/subcodec:SubcodecObjC", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SubcodecAnimationCacheImpl/Sources/SubcodecAnimationCacheImpl.swift b/submodules/TelegramUI/Components/SubcodecAnimationCacheImpl/Sources/SubcodecAnimationCacheImpl.swift new file mode 100644 index 0000000000..52481a826b --- /dev/null +++ b/submodules/TelegramUI/Components/SubcodecAnimationCacheImpl/Sources/SubcodecAnimationCacheImpl.swift @@ -0,0 +1,800 @@ +import Foundation +import UIKit +import SwiftSignalKit +import CryptoUtils +import ManagedFile +import AnimationCache +import SubcodecObjC + +public struct MbsMetadata { + public let frameCount: Int + public let frameDurations: [Double] +} + +private func md5Hash(_ string: String) -> String { + let hashData = string.data(using: .utf8)!.withUnsafeBytes { bytes -> Data in + return CryptoMD5(bytes.baseAddress!, Int32(bytes.count)) + } + return hashData.withUnsafeBytes { bytes -> String in + let uintBytes = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + return String(format: "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", uintBytes[0], uintBytes[1], uintBytes[2], uintBytes[3], uintBytes[4], uintBytes[5], uintBytes[6], uintBytes[7], uintBytes[8], uintBytes[9], uintBytes[10], uintBytes[11], uintBytes[12], uintBytes[13], uintBytes[14], uintBytes[15]) + } +} + +private func itemSubpath(hashString: String, width: Int, height: Int) -> (directory: String, fileName: String) { + assert(hashString.count == 32) + let directory = String(hashString[hashString.startIndex ..< hashString.index(hashString.startIndex, offsetBy: 2)]) + return (directory, "\(hashString)_\(width)x\(height)") +} + +private func roundUp(_ numToRound: Int, multiple: Int) -> Int { + if multiple == 0 { + return numToRound + } + let remainder = numToRound % multiple + if remainder == 0 { + return numToRound + } + return numToRound + multiple - remainder +} + +private func convertARGBToYUVA420( + argb: UnsafePointer, + width: Int, + height: Int, + bytesPerRow: Int +) -> (y: Data, cb: Data, cr: Data, alpha: Data, yStride: Int, cbStride: Int, crStride: Int, alphaStride: Int) { + let chromaWidth = width / 2 + let chromaHeight = height / 2 + + var yData = Data(count: width * height) + var cbData = Data(count: chromaWidth * chromaHeight) + var crData = Data(count: chromaWidth * chromaHeight) + var alphaData = Data(count: width * height) + + yData.withUnsafeMutableBytes { yBuf in + cbData.withUnsafeMutableBytes { cbBuf in + crData.withUnsafeMutableBytes { crBuf in + alphaData.withUnsafeMutableBytes { aBuf in + let yPtr = yBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let cbPtr = cbBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let crPtr = crBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let aPtr = aBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + + for row in 0 ..< height { + let srcRow = argb.advanced(by: row * bytesPerRow) + for col in 0 ..< width { + let px = srcRow.advanced(by: col * 4) + // BGRA layout (CoreGraphics premultiplied) + let b = Int(px[0]) + let g = Int(px[1]) + let r = Int(px[2]) + let a = Int(px[3]) + + // Un-premultiply + let rr: Int + let gg: Int + let bb: Int + if a > 0 { + rr = min(255, r * 255 / a) + gg = min(255, g * 255 / a) + bb = min(255, b * 255 / a) + } else { + rr = 0 + gg = 0 + bb = 0 + } + + // BT.709 + let y = 16 + (65 * rr + 129 * gg + 25 * bb + 128) / 256 + yPtr[row * width + col] = UInt8(clamping: y) + aPtr[row * width + col] = UInt8(a) + } + } + + // Chroma at half resolution (average 2x2 blocks) + for row in 0 ..< chromaHeight { + for col in 0 ..< chromaWidth { + var sumR = 0 + var sumG = 0 + var sumB = 0 + for dy in 0 ..< 2 { + for dx in 0 ..< 2 { + let srcRow = argb.advanced(by: (row * 2 + dy) * bytesPerRow) + let px = srcRow.advanced(by: (col * 2 + dx) * 4) + let b = Int(px[0]) + let g = Int(px[1]) + let r = Int(px[2]) + let a = Int(px[3]) + if a > 0 { + sumR += min(255, r * 255 / a) + sumG += min(255, g * 255 / a) + sumB += min(255, b * 255 / a) + } + } + } + let avgR = sumR / 4 + let avgG = sumG / 4 + let avgB = sumB / 4 + + let cb = 128 + (-38 * avgR - 74 * avgG + 112 * avgB + 128) / 256 + let cr = 128 + (112 * avgR - 94 * avgG - 18 * avgB + 128) / 256 + cbPtr[row * chromaWidth + col] = UInt8(clamping: cb) + crPtr[row * chromaWidth + col] = UInt8(clamping: cr) + } + } + } + } + } + } + + return (yData, cbData, crData, alphaData, width, chromaWidth, chromaWidth, width) +} + +private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { + struct CompressedResult { + var mbsPath: String + var metaPath: String + } + + var queue: Queue { + return self.innerQueue + } + let innerQueue: Queue + var isCancelled: Bool = false + + private let mbsOutputPath: String + private let metaOutputPath: String + private let completion: (CompressedResult?) -> Void + + private var spriteExtractor: SCSprite? + private var frameDurations: [Double] = [] + private var isFailed: Bool = false + private var isFinished: Bool = false + private var spriteWidth: Int = 0 + private var spriteHeight: Int = 0 + + private let lock = Lock() + + init?(queue: Queue, allocateTempFile: @escaping () -> String, completion: @escaping (CompressedResult?) -> Void) { + self.innerQueue = queue + self.mbsOutputPath = allocateTempFile() + self.metaOutputPath = allocateTempFile() + self.completion = completion + } + + func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int, insertKeyframe: Bool) { + self.lock.locked { + if self.isFailed || self.isFinished { + return + } + + let width = roundUp(proposedWidth, multiple: 16) + let height = roundUp(proposedHeight, multiple: 16) + + if width == 0 || height == 0 { + self.isFailed = true + return + } + + // Create extractor on first frame + if self.spriteExtractor == nil { + self.spriteWidth = width + self.spriteHeight = height + let spriteSize = max(width, height) + do { + self.spriteExtractor = try SCSprite.extractor(withSpriteSize: Int32(spriteSize), qp: 26, outputPath: self.mbsOutputPath) + } catch { + self.isFailed = true + return + } + } + + guard self.spriteWidth == width && self.spriteHeight == height else { + self.isFailed = true + return + } + + // Allocate ARGB surface + let bytesPerRow = width * 4 + let bufferSize = height * bytesPerRow + let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) + defer { buffer.deallocate() } + memset(buffer, 0, bufferSize) + + guard let duration = drawingBlock(AnimationCacheItemDrawingSurface( + argb: buffer, + width: width, + height: height, + bytesPerRow: bytesPerRow, + length: bufferSize + )) else { + return + } + + // Convert ARGB → YUV planes + let planes = convertARGBToYUVA420( + argb: buffer, + width: width, + height: height, + bytesPerRow: bytesPerRow + ) + + // Feed to extractor + do { + try self.spriteExtractor?.addFrameY( + planes.y, yStride: Int32(planes.yStride), + cb: planes.cb, cbStride: Int32(planes.cbStride), + cr: planes.cr, crStride: Int32(planes.crStride), + alpha: planes.alpha, alphaStride: Int32(planes.alphaStride) + ) + } catch { + self.isFailed = true + return + } + + self.frameDurations.append(duration) + } + } + + func finish() { + var result: CompressedResult? + + self.lock.locked { + if self.isFinished { + return + } + self.isFinished = true + + if self.isFailed || self.spriteExtractor == nil { + return + } + + do { + try self.spriteExtractor?.finalizeExtraction() + } catch { + self.isFailed = true + return + } + + // Write metadata file: frame count + durations + var metaData = Data() + var frameCount = UInt32(self.frameDurations.count) + metaData.append(Data(bytes: &frameCount, count: 4)) + for duration in self.frameDurations { + var d = Float32(duration) + metaData.append(Data(bytes: &d, count: 4)) + } + do { + try metaData.write(to: URL(fileURLWithPath: self.metaOutputPath)) + } catch { + self.isFailed = true + return + } + + result = CompressedResult(mbsPath: self.mbsOutputPath, metaPath: self.metaOutputPath) + } + + if !self.isFailed { + self.completion(result) + } else { + let _ = try? FileManager.default.removeItem(atPath: self.mbsOutputPath) + let _ = try? FileManager.default.removeItem(atPath: self.metaOutputPath) + self.completion(nil) + } + } +} + +private func decodeSingleFrameFromMbs(mbsPath: String, metadata: MbsMetadata) -> AnimationCacheItemFrame? { + guard let mbsData = try? Data(contentsOf: URL(fileURLWithPath: mbsPath), options: .mappedIfSafe) else { + return nil + } + guard mbsData.count >= 14 else { + return nil + } + guard mbsData[0] == 0x4D, mbsData[1] == 0x42, mbsData[2] == 0x53, mbsData[3] == 0x36 else { + return nil + } + let _ = Int(mbsData[4]) | (Int(mbsData[5]) << 8) + let heightMbs = Int(mbsData[6]) | (Int(mbsData[7]) << 8) + let qp = Int(mbsData[10]) + + let spriteContentSize = heightMbs * 16 - 32 + guard spriteContentSize > 0 else { + return nil + } + + var nalData: Data? + let sinkBlock: (Data) -> Void = { data in + if nalData == nil { + nalData = data + } else { + nalData!.append(data) + } + } + + guard let surface = try? SCMuxSurface.create( + withSpriteWidth: Int32(spriteContentSize), + spriteHeight: Int32(spriteContentSize), + maxSlots: 1, + qp: Int32(qp), + sink: sinkBlock + ) else { + return nil + } + + guard let region = try? surface.addSprite(atPath: mbsPath) else { + return nil + } + + nalData = nil + guard let _ = try? surface.advanceFrame(sink: sinkBlock) else { + return nil + } + + guard let streamData = nalData else { + return nil + } + + guard let decoder = try? SCVideoToolboxDecoder.createDecoder() else { + return nil + } + guard let frames = try? decoder.decodeStream(streamData) else { + return nil + } + guard let decodedFrame = frames.first else { + return nil + } + + return extractSpriteFrame( + decodedFrame: decodedFrame, + region: region, + spriteContentWidth: spriteContentSize, + spriteContentHeight: spriteContentSize + ) +} + +private func extractSpriteFrame( + decodedFrame: SCDecodedFrame, + region: SCSpriteRegion, + spriteContentWidth: Int, + spriteContentHeight: Int +) -> AnimationCacheItemFrame? { + let colorRect = region.colorRect + let alphaRect = region.alphaRect + + let frameWidth = Int(colorRect.width) + let frameHeight = Int(colorRect.height) + + guard frameWidth > 0 && frameHeight > 0 else { + return nil + } + + let decodedWidth = Int(decodedFrame.width) + + let bytesPerRow = frameWidth * 4 + var argbData = Data(count: frameHeight * bytesPerRow) + + decodedFrame.y.withUnsafeBytes { yBuf in + decodedFrame.cb.withUnsafeBytes { cbBuf in + decodedFrame.cr.withUnsafeBytes { crBuf in + argbData.withUnsafeMutableBytes { outBuf in + let yPtr = yBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let cbPtr = cbBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let crPtr = crBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let outPtr = outBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + + let colorX = Int(colorRect.origin.x) + let colorY = Int(colorRect.origin.y) + let alphaX = Int(alphaRect.origin.x) + let alphaY = Int(alphaRect.origin.y) + + let chromaWidth = decodedWidth / 2 + + for row in 0 ..< frameHeight { + for col in 0 ..< frameWidth { + let srcY = yPtr[(colorY + row) * decodedWidth + (colorX + col)] + let srcCb = cbPtr[((colorY + row) / 2) * chromaWidth + ((colorX + col) / 2)] + let srcCr = crPtr[((colorY + row) / 2) * chromaWidth + ((colorX + col) / 2)] + let srcA = yPtr[(alphaY + row) * decodedWidth + (alphaX + col)] + + let yVal = Int(srcY) - 16 + let cbVal = Int(srcCb) - 128 + let crVal = Int(srcCr) - 128 + + var r = (298 * yVal + 459 * crVal + 128) >> 8 + var g = (298 * yVal - 55 * cbVal - 136 * crVal + 128) >> 8 + var b = (298 * yVal + 541 * cbVal + 128) >> 8 + + r = max(0, min(255, r)) + g = max(0, min(255, g)) + b = max(0, min(255, b)) + + let a = Int(srcA) + + let pr = (r * a + 127) / 255 + let pg = (g * a + 127) / 255 + let pb = (b * a + 127) / 255 + + let outOffset = (row * frameWidth + col) * 4 + outPtr[outOffset + 0] = UInt8(pb) + outPtr[outOffset + 1] = UInt8(pg) + outPtr[outOffset + 2] = UInt8(pr) + outPtr[outOffset + 3] = UInt8(a) + } + } + } + } + } + } + + return AnimationCacheItemFrame( + format: .rgba(data: argbData, width: frameWidth, height: frameHeight, bytesPerRow: bytesPerRow), + duration: 1.0 / 30.0 + ) +} + +private func loadMbsMetadata(metaPath: String) -> MbsMetadata? { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: metaPath), options: .mappedIfSafe) else { + return nil + } + guard data.count >= 4 else { + return nil + } + var frameCount: UInt32 = 0 + withUnsafeMutableBytes(of: &frameCount) { buf in + data.copyBytes(to: buf.baseAddress!.assumingMemoryBound(to: UInt8.self), from: 0 ..< 4) + } + let expectedSize = 4 + Int(frameCount) * 4 + guard data.count >= expectedSize else { + return nil + } + var durations: [Double] = [] + for i in 0 ..< Int(frameCount) { + var d: Float32 = 0 + let offset = 4 + i * 4 + withUnsafeMutableBytes(of: &d) { buf in + data.copyBytes(to: buf.baseAddress!.assumingMemoryBound(to: UInt8.self), from: offset ..< offset + 4) + } + durations.append(Double(d)) + } + return MbsMetadata(frameCount: Int(frameCount), frameDurations: durations) +} + +public final class SubcodecAnimationCacheImpl: AnimationCache { + private final class Impl { + private struct ItemKey: Hashable { + var id: String + var width: Int + var height: Int + } + + private final class ItemContext { + let subscribers = Bag<(AnimationCacheItemResult) -> Void>() + let disposable = MetaDisposable() + + deinit { + self.disposable.dispose() + } + } + + private let queue: Queue + private let basePath: String + private let allocateTempFile: () -> String + private let updateStorageStats: (String, Int64) -> Void + + private let fetchQueues: [Queue] + private var nextFetchQueueIndex: Int = 0 + + private var itemContexts: [ItemKey: ItemContext] = [:] + + init(queue: Queue, basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { + self.queue = queue + + let fetchQueueCount: Int + if ProcessInfo.processInfo.processorCount > 2 { + fetchQueueCount = 3 + } else { + fetchQueueCount = 2 + } + self.fetchQueues = (0 ..< fetchQueueCount).map { i in Queue(name: "SubcodecAnimationCacheImpl-Fetch\(i)", qos: .default) } + self.basePath = basePath + self.allocateTempFile = allocateTempFile + self.updateStorageStats = updateStorageStats + } + + func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" + let mbsPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs" + let metaPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs.meta" + + if FileManager.default.fileExists(atPath: mbsPath), let metadata = loadMbsMetadata(metaPath: metaPath) { + if let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) { + let item = AnimationCacheItem(numFrames: metadata.frameCount, advanceImpl: { _, _ in + return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false) + }, resetImpl: {}) + updateResult(AnimationCacheItemResult(item: item, isFinal: true)) + return EmptyDisposable + } + } + + let key = ItemKey(id: sourceId, width: Int(size.width), height: Int(size.height)) + + let itemContext: ItemContext + var beginFetch = false + if let current = self.itemContexts[key] { + itemContext = current + } else { + itemContext = ItemContext() + self.itemContexts[key] = itemContext + beginFetch = true + } + + let queue = self.queue + let index = itemContext.subscribers.add(updateResult) + + updateResult(AnimationCacheItemResult(item: nil, isFinal: false)) + + if beginFetch { + let fetchQueueIndex = self.nextFetchQueueIndex + self.nextFetchQueueIndex += 1 + let allocateTempFile = self.allocateTempFile + let updateStorageStats = self.updateStorageStats + + guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: allocateTempFile, completion: { [weak self, weak itemContext] result in + queue.async { + guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { + return + } + + strongSelf.itemContexts.removeValue(forKey: key) + + guard let result = result else { + for f in itemContext.subscribers.copyItems() { + f(AnimationCacheItemResult(item: nil, isFinal: true)) + } + return + } + + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + return + } + let _ = try? FileManager.default.removeItem(atPath: mbsPath) + let _ = try? FileManager.default.removeItem(atPath: metaPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.mbsPath, toPath: mbsPath) else { + return + } + guard let _ = try? FileManager.default.moveItem(atPath: result.metaPath, toPath: metaPath) else { + return + } + + if let size = SubcodecAnimationCacheImpl.fileSize(mbsPath) { + updateStorageStats(mbsPath, size) + } + + guard let metadata = loadMbsMetadata(metaPath: metaPath) else { + return + } + + for f in itemContext.subscribers.copyItems() { + if let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) { + let item = AnimationCacheItem(numFrames: metadata.frameCount, advanceImpl: { _, _ in + return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false) + }, resetImpl: {}) + f(AnimationCacheItemResult(item: item, isFinal: true)) + } else { + f(AnimationCacheItemResult(item: nil, isFinal: true)) + } + } + } + }) else { + return EmptyDisposable + } + + let fetchDisposable = MetaDisposable() + fetchDisposable.set(fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: false))) + + itemContext.disposable.set(ActionDisposable { [weak writer] in + if let writer = writer { + writer.isCancelled = true + } + fetchDisposable.dispose() + }) + } + + return ActionDisposable { [weak self, weak itemContext] in + queue.async { + guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[key] else { + return + } + itemContext.subscribers.remove(index) + if itemContext.subscribers.isEmpty { + itemContext.disposable.dispose() + strongSelf.itemContexts.removeValue(forKey: key) + } + } + } + } + } + + private static func fileSize(_ path: String) -> Int64? { + var value = stat() + if stat(path, &value) == 0 { + return value.st_size + } + return nil + } + + private let queue: Queue + private let basePath: String + private let impl: QueueLocalObject + private let allocateTempFile: () -> String + private let updateStorageStats: (String, Int64) -> Void + + public init(basePath: String, allocateTempFile: @escaping () -> String, updateStorageStats: @escaping (String, Int64) -> Void) { + let queue = Queue() + self.queue = queue + self.basePath = basePath + self.allocateTempFile = allocateTempFile + self.updateStorageStats = updateStorageStats + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, basePath: basePath, allocateTempFile: allocateTempFile, updateStorageStats: updateStorageStats) + }) + } + + // MARK: - AnimationCache protocol + + public func get(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { result in + subscriber.putNext(result) + if result.isFinal { + subscriber.putCompletion() + } + })) + } + return disposable + } + |> runOn(self.queue) + } + + public func getFirstFrameSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" + let mbsPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs" + let metaPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs.meta" + + guard FileManager.default.fileExists(atPath: mbsPath), let metadata = loadMbsMetadata(metaPath: metaPath) else { + return nil + } + guard let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) else { + return nil + } + return AnimationCacheItem(numFrames: 1, advanceImpl: { _, _ in + return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false) + }, resetImpl: {}) + } + + public func getFirstFrame(queue: Queue, sourceId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let disposable = MetaDisposable() + let basePath = self.basePath + let allocateTempFile = self.allocateTempFile + let updateStorageStats = self.updateStorageStats + + queue.async { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" + let mbsPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs" + let metaPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs.meta" + + if FileManager.default.fileExists(atPath: mbsPath), let metadata = loadMbsMetadata(metaPath: metaPath) { + if let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) { + let item = AnimationCacheItem(numFrames: 1, advanceImpl: { _, _ in + return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false) + }, resetImpl: {}) + completion(AnimationCacheItemResult(item: item, isFinal: true)) + return + } + } + + if let fetch = fetch { + completion(AnimationCacheItemResult(item: nil, isFinal: false)) + guard let writer = AnimationCacheItemWriterImpl(queue: queue, allocateTempFile: allocateTempFile, completion: { result in + queue.async { + guard let result = result else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + let _ = try? FileManager.default.removeItem(atPath: mbsPath) + let _ = try? FileManager.default.removeItem(atPath: metaPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.mbsPath, toPath: mbsPath) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + guard let _ = try? FileManager.default.moveItem(atPath: result.metaPath, toPath: metaPath) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + if let size = SubcodecAnimationCacheImpl.fileSize(mbsPath) { + updateStorageStats(mbsPath, size) + } + guard let metadata = loadMbsMetadata(metaPath: metaPath) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + guard let frame = decodeSingleFrameFromMbs(mbsPath: mbsPath, metadata: metadata) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + let item = AnimationCacheItem(numFrames: 1, advanceImpl: { _, _ in + return AnimationCacheItem.AdvanceResult(frame: frame, didLoop: false) + }, resetImpl: {}) + completion(AnimationCacheItemResult(item: item, isFinal: true)) + } + }) else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + return + } + let fetchDisposable = fetch(AnimationCacheFetchOptions(size: size, writer: writer, firstFrameOnly: true)) + disposable.set(fetchDisposable) + } else { + completion(AnimationCacheItemResult(item: nil, isFinal: true)) + } + } + + return disposable + } + + // MARK: - Subcodec-specific API (used by renderer) + + public func getMbsPath(sourceId: String, size: CGSize) -> String { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" + return "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs" + } + + public func getMetaPath(sourceId: String, size: CGSize) -> String { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId), width: Int(size.width), height: Int(size.height)) + let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" + return "\(itemDirectoryPath)/\(sourceIdPath.fileName).mbs.meta" + } + + public func loadMetadata(sourceId: String, size: CGSize) -> MbsMetadata? { + let metaPath = getMetaPath(sourceId: sourceId, size: size) + return loadMbsMetadata(metaPath: metaPath) + } + + public func fetchIfNeeded(sourceId: String, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable, completion: @escaping (String?) -> Void) -> Disposable { + let mbsPath = getMbsPath(sourceId: sourceId, size: size) + if FileManager.default.fileExists(atPath: mbsPath) { + completion(mbsPath) + return EmptyDisposable + } + + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { [weak self] result in + guard let strongSelf = self else { + completion(nil) + return + } + if result.isFinal { + let path = strongSelf.getMbsPath(sourceId: sourceId, size: size) + if FileManager.default.fileExists(atPath: path) { + completion(path) + } else { + completion(nil) + } + } + })) + } + return disposable + } +} diff --git a/submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl/BUILD b/submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl/BUILD new file mode 100644 index 0000000000..a80393fc96 --- /dev/null +++ b/submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SubcodecMultiAnimationRendererImpl", + module_name = "SubcodecMultiAnimationRendererImpl", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/SubcodecAnimationCacheImpl:SubcodecAnimationCacheImpl", + "//third-party/subcodec:SubcodecObjC", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl/Sources/SubcodecMultiAnimationRendererImpl.swift b/submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl/Sources/SubcodecMultiAnimationRendererImpl.swift new file mode 100644 index 0000000000..d9bd6a3799 --- /dev/null +++ b/submodules/TelegramUI/Components/SubcodecMultiAnimationRendererImpl/Sources/SubcodecMultiAnimationRendererImpl.swift @@ -0,0 +1,653 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import AnimationCache +import MultiAnimationRenderer +import SubcodecObjC +import SubcodecAnimationCacheImpl +import Accelerate + +private let maxSlotsLimit: Int = 882 + +private func roundUp(_ numToRound: Int, multiple: Int) -> Int { + if multiple == 0 { return numToRound } + let remainder = numToRound % multiple + if remainder == 0 { return numToRound } + return numToRound + multiple - remainder +} + +private final class SpriteContext { + let mbsPath: String + let region: SCSpriteRegion + let targets = Bag>() + let metadata: MbsMetadata + + var currentFrameIndex: Int = 0 + var remainingDuration: Double = 0.0 + + var isPlaying: Bool { + for target in self.targets.copyItems() { + if let target = target.value, target.shouldBeAnimating { + return true + } + } + return false + } + + init(mbsPath: String, region: SCSpriteRegion, metadata: MbsMetadata) { + self.mbsPath = mbsPath + self.region = region + self.metadata = metadata + if !metadata.frameDurations.isEmpty { + self.remainingDuration = metadata.frameDurations[0] + } + } +} + +private final class SurfaceGroup { + let spriteWidth: Int + let spriteHeight: Int + + private(set) var surface: SCMuxSurface? + private(set) var decoder: SCVideoToolboxDecoder? + private(set) var spriteContexts: [Int: SpriteContext] = [:] + private var currentMaxSlots: Int = 64 + private var accumulatedNalData = Data() + private var lastDecodedFrame: SCDecodedFrame? + + init(spriteWidth: Int, spriteHeight: Int) { + self.spriteWidth = spriteWidth + self.spriteHeight = spriteHeight + } + + func ensureInitialized() -> Bool { + if self.surface != nil { + return true + } + let sink: (Data) -> Void = { [weak self] data in + self?.accumulatedNalData.append(data) + } + guard let surface = try? SCMuxSurface.create( + withSpriteWidth: Int32(self.spriteWidth), + spriteHeight: Int32(self.spriteHeight), + maxSlots: Int32(self.currentMaxSlots), + qp: 26, + sink: sink + ) else { + return false + } + self.surface = surface + + guard let decoder = try? SCVideoToolboxDecoder.createDecoder() else { + return false + } + self.decoder = decoder + + return true + } + + func addSprite(mbsPath: String, metadata: MbsMetadata) -> SpriteContext? { + guard self.ensureInitialized(), let surface = self.surface else { + return nil + } + + guard let region = try? surface.addSprite(atPath: mbsPath) else { + if !self.tryGrow() { + return nil + } + guard let region = try? surface.addSprite(atPath: mbsPath) else { + return nil + } + let context = SpriteContext(mbsPath: mbsPath, region: region, metadata: metadata) + self.spriteContexts[Int(region.slot)] = context + return context + } + + let context = SpriteContext(mbsPath: mbsPath, region: region, metadata: metadata) + self.spriteContexts[Int(region.slot)] = context + return context + } + + func removeSprite(slot: Int) { + self.surface?.removeSprite(atSlot: Int32(slot)) + self.spriteContexts.removeValue(forKey: slot) + } + + private func tryGrow() -> Bool { + guard let surface = self.surface, let decoder = self.decoder else { + return false + } + + let newMaxSlots: Int + if self.currentMaxSlots * 2 <= maxSlotsLimit { + newMaxSlots = self.currentMaxSlots * 2 + } else if self.currentMaxSlots < maxSlotsLimit { + newMaxSlots = maxSlotsLimit + } else { + return false + } + + if self.lastDecodedFrame == nil { + self.accumulatedNalData = Data() + guard let _ = try? surface.advanceFrame(sink: { [weak self] data in + self?.accumulatedNalData.append(data) + }) else { + return false + } + if !self.accumulatedNalData.isEmpty { + if let frames = try? decoder.decodeStream(self.accumulatedNalData) { + self.lastDecodedFrame = frames.last + } + } + } + + guard let decodedFrame = self.lastDecodedFrame else { + return false + } + + self.accumulatedNalData = Data() + let yData = decodedFrame.y + let cbData = decodedFrame.cb + let crData = decodedFrame.cr + let decodedWidth = Int(decodedFrame.width) + let decodedHeight = Int(decodedFrame.height) + let chromaWidth = decodedWidth / 2 + + guard let resizeResult = try? surface.resize( + toMaxSlots: Int32(newMaxSlots), + yPlane: yData, + cbPlane: cbData, + crPlane: crData, + decodedWidth: Int32(decodedWidth), + decodedHeight: Int32(decodedHeight), + strideY: Int32(decodedWidth), + strideCb: Int32(chromaWidth), + strideCr: Int32(chromaWidth), + withSink: { [weak self] data in + self?.accumulatedNalData.append(data) + } + ) else { + return false + } + + if !self.accumulatedNalData.isEmpty { + if let frames = try? decoder.decodeStream(self.accumulatedNalData) { + self.lastDecodedFrame = frames.last + } + } + + var updatedContexts: [Int: SpriteContext] = [:] + for newRegion in resizeResult.regions { + let newSlot = Int(newRegion.slot) + for (_, context) in self.spriteContexts { + if updatedContexts.values.contains(where: { $0 === context }) { + continue + } + let updated = SpriteContext(mbsPath: context.mbsPath, region: newRegion, metadata: context.metadata) + updated.currentFrameIndex = context.currentFrameIndex + updated.remainingDuration = context.remainingDuration + for target in context.targets.copyItems() { + if let target = target.value { + let _ = updated.targets.add(Weak(target)) + } + } + updatedContexts[newSlot] = updated + break + } + } + + self.spriteContexts = updatedContexts + self.currentMaxSlots = newMaxSlots + self.accumulatedNalData = Data() + return true + } + + func tick(advanceTimestamp: Double) { + guard let surface = self.surface, let decoder = self.decoder else { + return + } + + var anyAdvanced = false + for (slot, context) in self.spriteContexts { + guard context.isPlaying else { + continue + } + if advanceTimestamp > 0.0 { + context.remainingDuration -= advanceTimestamp + if context.remainingDuration <= 0.0 { + surface.advanceSprite(atSlot: Int32(slot)) + context.currentFrameIndex += 1 + if context.currentFrameIndex >= context.metadata.frameCount { + context.currentFrameIndex = 0 + } + if context.currentFrameIndex < context.metadata.frameDurations.count { + context.remainingDuration = context.metadata.frameDurations[context.currentFrameIndex] + } + anyAdvanced = true + } + } else { + surface.advanceSprite(atSlot: Int32(slot)) + anyAdvanced = true + } + } + + if !anyAdvanced { + return + } + + self.accumulatedNalData = Data() + guard let _ = try? surface.emitFrameIfNeeded(sink: { [weak self] data in + self?.accumulatedNalData.append(data) + }) else { + return + } + + guard !self.accumulatedNalData.isEmpty else { + return + } + + guard let frames = try? decoder.decodeStream(self.accumulatedNalData) else { + return + } + guard let decodedFrame = frames.last else { + return + } + + self.lastDecodedFrame = decodedFrame + + for (_, context) in self.spriteContexts { + let region = context.region + let colorRect = region.colorRect + let alphaRect = region.alphaRect + + let frameWidth = Int(colorRect.width) + let frameHeight = Int(colorRect.height) + guard frameWidth > 0 && frameHeight > 0 else { + continue + } + + let cgImage = extractCGImage( + decodedFrame: decodedFrame, + colorRect: colorRect, + alphaRect: alphaRect, + frameWidth: frameWidth, + frameHeight: frameHeight + ) + + let didLoop = context.currentFrameIndex == 0 && context.metadata.frameCount > 1 + + for targetRef in context.targets.copyItems() { + if let target = targetRef.value { + if let cgImage = cgImage { + target.transitionToContents(cgImage, didLoop: didLoop) + } + } + } + } + } + + var isEmpty: Bool { + return self.spriteContexts.isEmpty + } + + var hasPlayingSprites: Bool { + for (_, context) in self.spriteContexts { + if context.isPlaying { + return true + } + } + return false + } +} + +private func extractCGImage( + decodedFrame: SCDecodedFrame, + colorRect: CGRect, + alphaRect: CGRect, + frameWidth: Int, + frameHeight: Int +) -> CGImage? { + let decodedWidth = Int(decodedFrame.width) + let chromaWidth = decodedWidth / 2 + + let bytesPerRow = frameWidth * 4 + let bufferSize = frameHeight * bytesPerRow + guard let buffer = malloc(bufferSize) else { + return nil + } + let outPtr = buffer.assumingMemoryBound(to: UInt8.self) + + let colorX = Int(colorRect.origin.x) + let colorY = Int(colorRect.origin.y) + let alphaX = Int(alphaRect.origin.x) + let alphaY = Int(alphaRect.origin.y) + + decodedFrame.y.withUnsafeBytes { yBuf in + decodedFrame.cb.withUnsafeBytes { cbBuf in + decodedFrame.cr.withUnsafeBytes { crBuf in + let yPtr = yBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let cbPtr = cbBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + let crPtr = crBuf.baseAddress!.assumingMemoryBound(to: UInt8.self) + + for row in 0 ..< frameHeight { + for col in 0 ..< frameWidth { + let srcY = yPtr[(colorY + row) * decodedWidth + (colorX + col)] + let srcCb = cbPtr[((colorY + row) / 2) * chromaWidth + ((colorX + col) / 2)] + let srcCr = crPtr[((colorY + row) / 2) * chromaWidth + ((colorX + col) / 2)] + let srcA = yPtr[(alphaY + row) * decodedWidth + (alphaX + col)] + + let yVal = Int(srcY) - 16 + let cbVal = Int(srcCb) - 128 + let crVal = Int(srcCr) - 128 + + var r = (298 * yVal + 459 * crVal + 128) >> 8 + var g = (298 * yVal - 55 * cbVal - 136 * crVal + 128) >> 8 + var b = (298 * yVal + 541 * cbVal + 128) >> 8 + + r = max(0, min(255, r)) + g = max(0, min(255, g)) + b = max(0, min(255, b)) + + let a = Int(srcA) + let pr = (r * a + 127) / 255 + let pg = (g * a + 127) / 255 + let pb = (b * a + 127) / 255 + + let outOffset = (row * frameWidth + col) * 4 + outPtr[outOffset + 0] = UInt8(pb) + outPtr[outOffset + 1] = UInt8(pg) + outPtr[outOffset + 2] = UInt8(pr) + outPtr[outOffset + 3] = UInt8(a) + } + } + } + } + } + + guard let dataProvider = CGDataProvider(dataInfo: nil, data: buffer, size: bufferSize, releaseData: { _, data, _ in + free(UnsafeMutableRawPointer(mutating: data)) + }) else { + free(buffer) + return nil + } + + return CGImage( + width: frameWidth, + height: frameHeight, + bitsPerComponent: 8, + bitsPerPixel: 32, + bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue), + provider: dataProvider, + decode: nil, + shouldInterpolate: false, + intent: .defaultIntent + ) +} + +public final class SubcodecMultiAnimationRendererImpl: MultiAnimationRenderer { + private struct SizeKey: Hashable { + let width: Int + let height: Int + } + + private var surfaceGroups: [SizeKey: SurfaceGroup] = [:] + private var frameSkip: Int + private var displayTimer: Foundation.Timer? + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + if self.isPlaying { + if self.displayTimer == nil { + final class TimerTarget: NSObject { + private let f: () -> Void + init(_ f: @escaping () -> Void) { + self.f = f + } + @objc func timerEvent() { + self.f() + } + } + let frameInterval = Double(self.frameSkip) / 60.0 + let displayTimer = Foundation.Timer(timeInterval: frameInterval, target: TimerTarget { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.animationTick(frameInterval: frameInterval) + }, selector: #selector(TimerTarget.timerEvent), userInfo: nil, repeats: true) + self.displayTimer = displayTimer + RunLoop.main.add(displayTimer, forMode: .common) + } + } else { + if let displayTimer = self.displayTimer { + self.displayTimer = nil + displayTimer.invalidate() + } + } + } + } + } + + public init() { + if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.processorCount > 2 { + self.frameSkip = 1 + } else { + self.frameSkip = 2 + } + } + + public func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, unique: Bool, size: CGSize, fetch: @escaping (AnimationCacheFetchOptions) -> Disposable) -> Disposable { + guard let subcodecCache = cache as? SubcodecAnimationCacheImpl else { + return EmptyDisposable + } + + let spriteWidth = roundUp(Int(size.width), multiple: 16) + let spriteHeight = roundUp(Int(size.height), multiple: 16) + let sizeKey = SizeKey(width: spriteWidth, height: spriteHeight) + + let fetchDisposable = MetaDisposable() + + fetchDisposable.set(subcodecCache.fetchIfNeeded(sourceId: itemId, size: size, fetch: fetch, completion: { [weak self, weak target] mbsPath in + Queue.mainQueue().async { + guard let strongSelf = self, let target = target, let mbsPath = mbsPath else { + return + } + + guard let metadata = subcodecCache.loadMetadata(sourceId: itemId, size: size) else { + return + } + + let surfaceGroup: SurfaceGroup + if let existing = strongSelf.surfaceGroups[sizeKey] { + surfaceGroup = existing + } else { + surfaceGroup = SurfaceGroup(spriteWidth: spriteWidth, spriteHeight: spriteHeight) + strongSelf.surfaceGroups[sizeKey] = surfaceGroup + } + + guard let spriteContext = surfaceGroup.addSprite(mbsPath: mbsPath, metadata: metadata) else { + return + } + + let targetIndex = spriteContext.targets.add(Weak(target)) + target.numFrames = metadata.frameCount + + let slot = Int(spriteContext.region.slot) + + let deinitIndex = target.deinitCallbacks.add { [weak self, weak surfaceGroup] in + Queue.mainQueue().async { + guard let strongSelf = self, let surfaceGroup = surfaceGroup else { + return + } + spriteContext.targets.remove(targetIndex) + if spriteContext.targets.isEmpty { + surfaceGroup.removeSprite(slot: slot) + if surfaceGroup.isEmpty { + strongSelf.surfaceGroups.removeValue(forKey: sizeKey) + } + strongSelf.updateIsPlaying() + } + } + } + + let updateStateIndex = target.updateStateCallbacks.add { [weak self] in + self?.updateIsPlaying() + } + + fetchDisposable.set(ActionDisposable { [weak self, weak surfaceGroup, weak target] in + guard let strongSelf = self, let surfaceGroup = surfaceGroup else { + return + } + if let target = target { + target.deinitCallbacks.remove(deinitIndex) + target.updateStateCallbacks.remove(updateStateIndex) + } + spriteContext.targets.remove(targetIndex) + if spriteContext.targets.isEmpty { + surfaceGroup.removeSprite(slot: slot) + if surfaceGroup.isEmpty { + strongSelf.surfaceGroups.removeValue(forKey: sizeKey) + } + strongSelf.updateIsPlaying() + } + }) + + strongSelf.updateIsPlaying() + } + })) + + return ActionDisposable { + fetchDisposable.dispose() + }.strict() + } + + public func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) { + guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else { + return false + } + switch frame.frame.format { + case let .rgba(data, width, height, bytesPerRow): + guard let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) else { + return false + } + data.withUnsafeBytes { bytes -> Void in + memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) + } + guard let image = context.generateImage() else { + return false + } + target.contents = image.cgImage + target.numFrames = item.numFrames + return true + default: + return false + } + } + return false + } + + public func loadFirstFrame(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (Bool, Bool) -> Void) -> Disposable { + return cache.getFirstFrame(queue: .mainQueue(), sourceId: itemId, size: size, fetch: fetch, completion: { [weak target] result in + guard let item = result.item else { + Queue.mainQueue().async { + completion(false, result.isFinal) + } + return + } + + let loaded: Bool + if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { + switch frame.frame.format { + case let .rgba(data, width, height, bytesPerRow): + Queue.mainQueue().async { + guard let target = target else { + completion(false, true) + return + } + target.numFrames = item.numFrames + if let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) { + data.withUnsafeBytes { bytes -> Void in + memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) + } + if let image = context.generateImage() { + target.contents = image.cgImage + completion(true, true) + return + } + } + completion(false, true) + } + return + default: + loaded = false + } + } else { + loaded = false + } + + Queue.mainQueue().async { + completion(loaded, true) + } + }).strict() + } + + public func loadFirstFrameAsImage(cache: AnimationCache, itemId: String, size: CGSize, fetch: ((AnimationCacheFetchOptions) -> Disposable)?, completion: @escaping (CGImage?) -> Void) -> Disposable { + return cache.getFirstFrame(queue: .mainQueue(), sourceId: itemId, size: size, fetch: fetch, completion: { result in + guard let item = result.item else { + Queue.mainQueue().async { + completion(nil) + } + return + } + + if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { + switch frame.frame.format { + case let .rgba(data, width, height, bytesPerRow): + Queue.mainQueue().async { + if let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) { + data.withUnsafeBytes { bytes -> Void in + memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) + } + completion(context.generateImage()?.cgImage) + } else { + completion(nil) + } + } + return + default: + break + } + } + + Queue.mainQueue().async { + completion(nil) + } + }).strict() + } + + public func setFrameIndex(itemId: String, size: CGSize, frameIndex: Int, placeholder: UIImage) { + } + + private func updateIsPlaying() { + var isPlaying = false + for (_, group) in self.surfaceGroups { + if group.hasPlayingSprites { + isPlaying = true + break + } + } + self.isPlaying = isPlaying + } + + private func animationTick(frameInterval: Double) { + for (_, group) in self.surfaceGroups { + if group.hasPlayingSprites { + group.tick(advanceTimestamp: frameInterval) + } + } + } +} diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/BUILD b/submodules/TelegramUI/Components/TextProcessingScreen/BUILD index 547c1c3acc..01ac090ee0 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/BUILD +++ b/submodules/TelegramUI/Components/TextProcessingScreen/BUILD @@ -38,6 +38,9 @@ swift_library( "//submodules/TelegramUI/Components/ToastComponent", "//submodules/TelegramUI/Components/InteractiveTextComponent", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent", + "//submodules/TelegramUI/Components/TextFieldComponent", + "//submodules/TelegramUI/Components/EntityKeyboard", "//submodules/TelegramNotices", "//submodules/Markdown", "//submodules/TelegramUIPreferences", @@ -45,6 +48,7 @@ swift_library( "//submodules/TextSelectionNode", "//submodules/Pasteboard", "//submodules/Speak", + "//submodules/ContextUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingLanguageSelectionComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingLanguageSelectionComponent.swift index 24243a125e..76e631c9b8 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingLanguageSelectionComponent.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingLanguageSelectionComponent.swift @@ -32,7 +32,7 @@ final class TextProcessingLanguageSelectionComponent: Component { let selectedLanguageCode: String let currentStyle: TelegramComposeAIMessageMode.StyleId let displayStyles: [TextProcessingScreen.Style]? - let completion: (String, TelegramComposeAIMessageMode.StyleId) -> Void + let completion: (String, TelegramComposeAIMessageMode.StyleReference) -> Void let dismissed: () -> Void let inputHeight: CGFloat @@ -45,7 +45,7 @@ final class TextProcessingLanguageSelectionComponent: Component { selectedLanguageCode: String, currentStyle: TelegramComposeAIMessageMode.StyleId, displayStyles: [TextProcessingScreen.Style]?, - completion: @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void, + completion: @escaping (String, TelegramComposeAIMessageMode.StyleReference) -> Void, dismissed: @escaping () -> Void, inputHeight: CGFloat ) { @@ -271,7 +271,18 @@ final class TextProcessingLanguageSelectionComponent: Component { } if case .ended = recognizer.state { if component.displayStyles != nil, let updatedStyle = self.updatedStyle { - component.completion(component.selectedLanguageCode, updatedStyle) + let mappedStyle: TelegramComposeAIMessageMode.StyleReference + switch updatedStyle { + case .neutral: + mappedStyle = .neutral + case let .style(style): + if let styleValue = component.displayStyles?.first(where: { $0.reference.id == style }) { + mappedStyle = .style(styleValue.reference) + } else { + return + } + } + component.completion(component.selectedLanguageCode, mappedStyle) } self.animateOut() } @@ -298,7 +309,19 @@ final class TextProcessingLanguageSelectionComponent: Component { return } if self.updatedLanguage != nil || self.updatedStyle != nil { - component.completion(self.updatedLanguage ?? component.selectedLanguageCode, self.updatedStyle ?? component.currentStyle) + let styleId = self.updatedStyle ?? component.currentStyle + let mappedStyle: TelegramComposeAIMessageMode.StyleReference + switch styleId { + case .neutral: + mappedStyle = .neutral + case let .style(style): + if let styleValue = component.displayStyles?.first(where: { $0.reference.id == style }) { + mappedStyle = .style(styleValue.reference) + } else { + return + } + } + component.completion(self.updatedLanguage ?? component.selectedLanguageCode, mappedStyle) } self.animateOut() } @@ -469,7 +492,7 @@ final class TextProcessingLanguageSelectionComponent: Component { var styleData: [(id: TelegramComposeAIMessageMode.StyleId, iconFileId: Int64?, iconFile: TelegramMediaFile?, title: String)] = [] styleData.append((.neutral, nil, nil, component.strings.TextProcessingStyle_Neutral)) for item in displayStyles { - styleData.append((item.id, item.emojiFileId, item.emojiFile, item.title)) + styleData.append((.style(item.reference.id), item.emojiFileId, item.emojiFile, item.title)) } let stylesItemSize = CGSize(width: 82.0, height: 60.0) diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift index c078d33c57..dc9d2a985c 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingScreen.swift @@ -24,6 +24,8 @@ import TelegramNotices import Markdown import TelegramUIPreferences import ChatSendMessageActionUI +import ContextUI +import EmojiStatusComponent final class TextProcessingContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -40,6 +42,7 @@ final class TextProcessingContentComponent: Component { let externalState: ExternalState let context: AccountContext let mode: TextProcessingScreen.Mode + let previewIconFile: TelegramMediaFile? let styles: [TextProcessingScreen.Style] let inputText: TextWithEntities let initialEditState: TextProcessingScreen.EditState? @@ -47,12 +50,18 @@ final class TextProcessingContentComponent: Component { let shouldDisplayStyleNotice: Bool let copyCurrentResult: (() -> Void)? let translateChat: ((String) -> Void)? - let displayLanguageSelectionMenu: (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void) -> Void + let displayLanguageSelectionMenu: (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleReference) -> Void) -> Void + let newStyleAdded: (TelegramComposeAIMessageMode.CloudStyle) -> Void + let styleUpdated: (TelegramComposeAIMessageMode.CloudStyle) -> Void + let styleDeleted: (TelegramComposeAIMessageMode.StyleId) -> Void + let displayToast: (String) -> Void + let dismiss: (@escaping () -> Void) -> Void init( externalState: ExternalState, context: AccountContext, mode: TextProcessingScreen.Mode, + previewIconFile: TelegramMediaFile?, styles: [TextProcessingScreen.Style], inputText: TextWithEntities, initialEditState: TextProcessingScreen.EditState?, @@ -60,12 +69,18 @@ final class TextProcessingContentComponent: Component { shouldDisplayStyleNotice: Bool, copyCurrentResult: (() -> Void)?, translateChat: ((String) -> Void)?, - displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void) -> Void + displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleReference) -> Void) -> Void, + newStyleAdded: @escaping (TelegramComposeAIMessageMode.CloudStyle) -> Void, + styleUpdated: @escaping (TelegramComposeAIMessageMode.CloudStyle) -> Void, + styleDeleted: @escaping (TelegramComposeAIMessageMode.StyleId) -> Void, + displayToast: @escaping (String) -> Void, + dismiss: @escaping (@escaping () -> Void) -> Void ) { self.externalState = externalState self.styles = styles self.context = context self.mode = mode + self.previewIconFile = previewIconFile self.inputText = inputText self.initialEditState = initialEditState self.ignoredTranslationLanguages = ignoredTranslationLanguages @@ -73,9 +88,17 @@ final class TextProcessingContentComponent: Component { self.copyCurrentResult = copyCurrentResult self.translateChat = translateChat self.displayLanguageSelectionMenu = displayLanguageSelectionMenu + self.newStyleAdded = newStyleAdded + self.styleUpdated = styleUpdated + self.styleDeleted = styleDeleted + self.displayToast = displayToast + self.dismiss = dismiss } static func ==(lhs: TextProcessingContentComponent, rhs: TextProcessingContentComponent) -> Bool { + if lhs.styles != rhs.styles { + return false + } return true } @@ -94,8 +117,14 @@ final class TextProcessingContentComponent: Component { private let modeTabs = ComponentView() private let actionsSection = ComponentView() + private var previewIcon: ComponentView? + private var previewTitle: ComponentView? + private var previewDescription: ComponentView? + private let currentContentBackground: UIImageView private let currentContentContainer: UIView + private var currentContentHeader: (id: AnyHashable, view: ComponentView)? + private var currentContentFooter: (id: AnyHashable, view: ComponentView)? private let translateState = TextProcessingTranslateContentComponent.ExternalState() private let stylizeState = TextProcessingTranslateContentComponent.ExternalState() @@ -105,6 +134,10 @@ final class TextProcessingContentComponent: Component { private var currentMode: Mode = .translate + private var isRequestingStylePreview: Bool = false + private var currentStylePreview: AIMessageStylePreview? + private var currentStylePreviewDisposable: Disposable? + override init(frame: CGRect) { self.currentContentBackground = UIImageView() self.currentContentContainer = UIView() @@ -148,6 +181,10 @@ final class TextProcessingContentComponent: Component { preconditionFailure() } + deinit { + self.currentStylePreviewDisposable?.dispose() + } + private func externalStatesUpdated() { guard let component = self.component else { return @@ -195,6 +232,235 @@ final class TextProcessingContentComponent: Component { }) } } + + private func requestShareStyle(id: TelegramComposeAIMessageMode.StyleReference) { + guard let component = self.component else { + return + } + guard let style = component.styles.first(where: { .style($0.reference.id) == id.id }) else { + return + } + guard let slug = style.slug else { + return + } + let shareController = component.context.sharedContext.makeShareController(context: component.context, params: ShareControllerParams( + subject: .url("https://t.me/addstyle/\(slug)"), + completed: { [weak self] peerIds in + Task { @MainActor in + guard let self, let component = self.component, let environment = self.environment, let peerId = peerIds.first else { + return + } + let text: String + if peerId == component.context.account.peerId { + text = environment.strings.WebBrowser_LinkForwardTooltip_SavedMessages_One + } else { + guard let peer = await component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)).get() else { + return + } + text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string + } + component.displayToast(text) + } + } + )) + self.environment?.controller()?.present(shareController, in: .window(.root)) + } + + private func requestEditStyle(id: TelegramComposeAIMessageMode.StyleReference) { + Task { @MainActor [weak self] in + guard let self else { + return + } + guard let component = self.component, let environment = self.environment else { + return + } + guard let style = component.styles.first(where: { .style($0.reference.id) == id.id }) else { + return + } + environment.controller()?.push(await TextStyleEditScreen( + context: component.context, + mode: .edit(style.cloudStyle), + completion: { [weak self] style in + guard let self, let component = self.component else { + return + } + component.styleUpdated(style) + }, + styleDeleted: { [weak self] in + guard let self, let component = self.component else { + return + } + component.styleDeleted(style.id.id) + } + )) + } + } + + private func requestDeleteStyle(id: TelegramComposeAIMessageMode.StyleReference) { + guard let component = self.component, let environment = self.environment else { + return + } + guard let style = component.styles.first(where: { .style($0.reference.id) == id.id }) else { + return + } + guard case let .custom(style) = style.cloudStyle.content else { + return + } + + if style.isCreator { + environment.controller()?.push(textAlertController( + context: component.context, + title: environment.strings.TextProcessing_AlertCreatorDeleteStyle_Title, + text: environment.strings.TextProcessing_AlertCreatorDeleteStyle_Text, + actions: [ + TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: environment.strings.Common_Delete, action: { [weak self] in + guard let self, let component = self.component else { + return + } + let _ = component.context.engine.messages.deleteAITextStyle(id: style.id, accessHash: style.accessHash).startStandalone() + component.styleDeleted(id.id) + }), + ] + )) + } else { + environment.controller()?.push(textAlertController( + context: component.context, + title: environment.strings.TextProcessing_AlertDeleteStyle_Title, + text: environment.strings.TextProcessing_AlertDeleteStyle_Text, + actions: [ + TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: environment.strings.Common_Delete, action: { [weak self] in + guard let self, let component = self.component else { + return + } + let _ = component.context.engine.messages.unsaveAITextStyle(id: style.id, accessHash: style.accessHash).startStandalone() + component.styleDeleted(id.id) + }), + ] + )) + } + } + + private func openStyleContextMenu(id: TelegramComposeAIMessageMode.StyleReference, gesture: ContextGesture, sourceView: ContextExtractedContentContainingView) { + guard let component = self.component, let environment = self.environment else { + return + } + + guard let style = component.styles.first(where: { .style($0.reference.id) == id.id }) else { + return + } + + var items: [ContextMenuItem] = [] + if style.isAuthor { + items.append(.action(ContextMenuActionItem( + text: environment.strings.TextProcessing_StyleMenu_Edit, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) + }, + action: { [weak self] c, _ in + c?.dismiss(completion: { [weak self] in + guard let self else { + return + } + self.requestEditStyle(id: id) + }) + }) + )) + } + items.append(.action(ContextMenuActionItem( + text: environment.strings.TextProcessing_StyleMenu_Share, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) + }, + action: { [weak self] c, _ in + c?.dismiss(completion: { [weak self] in + guard let self else { + return + } + self.requestShareStyle(id: id) + }) + }) + )) + items.append(.action(ContextMenuActionItem( + text: environment.strings.TextProcessing_StyleMenu_Delete, + textColor: .destructive, + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) + }, + action: { [weak self] c, _ in + c?.dismiss(completion: { [weak self] in + guard let self else { + return + } + self.requestDeleteStyle(id: id) + }) + }) + )) + + final class ContextExtractedContentSourceImpl: ContextExtractedContentSource { + let keepInPlace: Bool = false + let ignoreContentTouches: Bool = false + let blurBackground: Bool = false + let actionsHorizontalAlignment: ContextActionsHorizontalAlignment = .center + + private let contentView: ContextExtractedContentContainingView + + init(contentView: ContextExtractedContentContainingView) { + self.contentView = contentView + } + + func takeView() -> ContextControllerTakeViewInfo? { + return ContextControllerTakeViewInfo(containingItem: .view(self.contentView), contentAreaInScreenSpace: UIScreen.main.bounds) + } + + func putBack() -> ContextControllerPutBackViewInfo? { + return ContextControllerPutBackViewInfo(contentAreaInScreenSpace: UIScreen.main.bounds) + } + } + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + let controller = makeContextController( + presentationData: presentationData, + source: .extracted(ContextExtractedContentSourceImpl(contentView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture + ) + environment.controller()?.presentInGlobalOverlay(controller) + } + + private func requestStylePreview() { + guard let component = self.component else { + return + } + guard case let .preview(style, _, _, _, _) = component.mode else { + return + } + + var index = 0 + if let currentStylePreview = self.currentStylePreview, let currentIndex = currentStylePreview.index { + var maxPreviewCount = 3 + if let data = component.context.currentAppConfiguration.with({ $0 }).data, let value = data["aicompose_tone_examples_num"] as? Double { + maxPreviewCount = max(1, Int(value)) + } + index = (currentIndex + 1) % maxPreviewCount + } + + self.isRequestingStylePreview = true + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + + self.currentStylePreviewDisposable?.dispose() + self.currentStylePreviewDisposable = (component.context.engine.messages.requestAIMessageStylePreview(reference: TelegramComposeAIMessageMode.CloudStyle(content: .custom(style)).reference, index: index) |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let self, let result else { + return + } + self.isRequestingStylePreview = false + self.currentStylePreview = result + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + }) + } func update(component: TextProcessingContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true @@ -206,24 +472,132 @@ final class TextProcessingContentComponent: Component { let environment = environment[ViewControllerComponentContainer.Environment.self].value - if self.component == nil { - self.stylizeState.displayStyleTooltip = component.shouldDisplayStyleNotice - switch component.mode { - case .edit: - self.currentMode = .stylize - case .translate: - self.currentMode = .translate - } - } + let isFirstTime = self.component == nil self.component = component self.environment = environment self.state = state + if isFirstTime { + self.stylizeState.displayStyleTooltip = component.shouldDisplayStyleNotice + switch component.mode { + case .edit: + self.currentMode = .stylize + case let .preview(_, _, initialPreview, _, _): + self.currentMode = .stylize + self.currentStylePreview = initialPreview + self.requestStylePreview() + case .translate: + self.currentMode = .translate + } + } + let sideInset: CGFloat = 16.0 var contentHeight: CGFloat = 0.0 - contentHeight += 82.0 + + if case let .preview(style, _, _, _, _) = component.mode { + contentHeight += 40.0 + if let previewIconFile = component.previewIconFile { + let previewIcon: ComponentView + if let current = self.previewIcon { + previewIcon = current + } else { + previewIcon = ComponentView() + self.previewIcon = previewIcon + } + let previewIconSize = CGSize(width: 60.0, height: 60.0) + let _ = previewIcon.update( + transition: transition, + component: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: .animation( + content: .file(file: previewIconFile), + size: previewIconSize, + placeholderColor: environment.theme.list.mediaPlaceholderColor, + themeColor: environment.theme.list.itemPrimaryTextColor, + loopMode: .count(1) + ), + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: previewIconSize + ) + let previewIconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - previewIconSize.width) * 0.5), y: contentHeight), size: previewIconSize) + if let previewIconView = previewIcon.view { + if previewIconView.superview == nil { + self.addSubview(previewIconView) + previewIconView.isUserInteractionEnabled = false + } + transition.setFrame(view: previewIconView, frame: previewIconFrame) + } + contentHeight += previewIconSize.height + contentHeight += 10.0 + } + + let previewTitle: ComponentView + if let current = self.previewTitle { + previewTitle = current + } else { + previewTitle = ComponentView() + self.previewTitle = previewTitle + } + + let previewDescription: ComponentView + if let current = self.previewDescription { + previewDescription = current + } else { + previewDescription = ComponentView() + self.previewDescription = previewDescription + } + + let titleSize = previewTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: style.title, font: Font.bold(30.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize) + if let previewTitleView = previewTitle.view { + if previewTitleView.superview == nil { + previewTitleView.isUserInteractionEnabled = false + self.addSubview(previewTitleView) + } + transition.setPosition(view: previewTitleView, position: titleFrame.center) + previewTitleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + contentHeight += titleSize.height + 5.0 + + let descriptionSize = previewDescription.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.TextProcessing_StylePreview_Subtitle, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.12 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let descriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionSize.width) * 0.5), y: contentHeight), size: descriptionSize) + if let previewDescriptionView = previewDescription.view { + if previewDescriptionView.superview == nil { + previewDescriptionView.isUserInteractionEnabled = false + self.addSubview(previewDescriptionView) + } + transition.setPosition(view: previewDescriptionView, position: descriptionFrame.center) + previewDescriptionView.bounds = CGRect(origin: CGPoint(), size: descriptionFrame.size) + } + contentHeight += descriptionSize.height + contentHeight += 30.0 + } else { + contentHeight += 82.0 + } switch component.mode { case .edit: @@ -331,7 +705,7 @@ final class TextProcessingContentComponent: Component { } contentHeight += modeTabsSize.height contentHeight += 24.0 - case .translate: + case .translate, .preview: break } @@ -344,9 +718,11 @@ final class TextProcessingContentComponent: Component { self.currentContent = nil } + let contentExternalState: TextProcessingTranslateContentComponent.ExternalState let contentComponent: AnyComponent switch self.currentMode { case .translate: + contentExternalState = self.translateState contentComponent = AnyComponent(TextProcessingTranslateContentComponent( context: component.context, theme: environment.theme, @@ -357,32 +733,163 @@ final class TextProcessingContentComponent: Component { mode: .translate(ignoredLanguages: component.ignoredTranslationLanguages), copyAction: component.copyCurrentResult, displayLanguageSelectionMenu: component.displayLanguageSelectionMenu, + createStyle: { + }, + openStyleContextMenu: { _, _, _ in + }, present: { [weak self] c, a in self?.environment?.controller()?.present(c, in: .window(.root), with: a) }, rootViewForTextSelection: { [weak self] in return self?.environment?.controller()?.view + }, + openPeer: { _ in + }, + requestAnotherPreviewExample: { } )) case .stylize: + var inputText = component.inputText + var isPreview = false + var fromText: TextWithEntities? + var toText: TextWithEntities? + var isRequestingPreview: Bool = false + var authorPeer: EnginePeer? + var userCount: Int = 0 + if case let .preview(style, authorPeerValue, _, _, _) = component.mode { + isPreview = true + inputText = TextWithEntities(text: "", entities: []) + authorPeer = authorPeerValue + userCount = style.userCount ?? 0 + isRequestingPreview = self.isRequestingStylePreview + if let currentStylePreview = self.currentStylePreview { + fromText = currentStylePreview.from + toText = currentStylePreview.to + } + } + + contentExternalState = self.stylizeState contentComponent = AnyComponent(TextProcessingTranslateContentComponent( context: component.context, theme: environment.theme, strings: environment.strings, styles: component.styles, externalState: self.stylizeState, - inputText: component.inputText, - mode: .stylize, + inputText: inputText, + mode: isPreview ? .preview(from: fromText, to: toText, authorPeer: authorPeer, userCount: userCount, isRequesting: isRequestingPreview) : .stylize, copyAction: component.copyCurrentResult, displayLanguageSelectionMenu: component.displayLanguageSelectionMenu, + createStyle: { [weak self] in + Task { @MainActor in + guard let self else { + return + } + guard let component = self.component, let environment = self.environment else { + return + } + + let hasPremium = await (component.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId) + ) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + }).get() + let userLimits = await component.context.engine.data.get( + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: hasPremium) + ).get() + + let maxStyles = Int(userLimits.maxOwnedAITextStyles) + + if component.styles.filter({ $0.isAuthor }).count >= maxStyles { + if !hasPremium { + let context = component.context + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .aiTools, forceDark: false, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesStealthMode, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }, dismissed: nil) + replaceImpl = { [weak self, weak controller] c in + controller?.dismiss(animated: true, completion: { + guard let self else { + return + } + self.environment?.controller()?.push(c) + }) + } + environment.controller()?.push(controller) + } else { + environment.controller()?.push(textAlertController( + context: component.context, + title: environment.strings.TextProcessing_AlertTooManyStyles_Title, + text: environment.strings.TextProcessing_AlertTooManyStyles_Text, + actions: [ + TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {}), + ] + )) + } + return + } + + environment.controller()?.push(await TextStyleEditScreen( + context: component.context, + mode: .create, + completion: { [weak self] style in + guard let self, let component = self.component else { + return + } + + if let contentComponentView = self.currentContent?.view.view as? TextProcessingTranslateContentComponent.View { + contentComponentView.scrollStylesToStart() + } + + component.newStyleAdded(style) + }, + styleDeleted: { + } + )) + } + }, + openStyleContextMenu: { [weak self] styleId, gesture, sourceView in + guard let self else { + return + } + self.openStyleContextMenu(id: styleId, gesture: gesture, sourceView: sourceView) + }, present: { [weak self] c, a in self?.environment?.controller()?.present(c, in: .window(.root), with: a) }, rootViewForTextSelection: { [weak self] in return self?.environment?.controller()?.view + }, + openPeer: { [weak self] peer in + guard let self, let component = self.component, let environment = self.environment else { + return + } + guard let navigationController = environment.controller()?.navigationController as? NavigationController else { + return + } + let context = component.context + component.dismiss({ [weak context, weak navigationController] in + guard let context, let navigationController else { + return + } + if let peerInfoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + navigationController.pushViewController(peerInfoController) + } + }) + }, + requestAnotherPreviewExample: { [weak self] in + guard let self else { + return + } + self.requestStylePreview() } )) case .fix: + contentExternalState = self.fixState contentComponent = AnyComponent(TextProcessingTranslateContentComponent( context: component.context, theme: environment.theme, @@ -393,15 +900,72 @@ final class TextProcessingContentComponent: Component { mode: .fix, copyAction: component.copyCurrentResult, displayLanguageSelectionMenu: component.displayLanguageSelectionMenu, + createStyle: { + }, + openStyleContextMenu: { _, _, _ in + }, present: { [weak self] c, a in self?.environment?.controller()?.present(c, in: .window(.root), with: a) }, rootViewForTextSelection: { [weak self] in return self?.environment?.controller()?.view + }, + openPeer: { _ in + }, + requestAnotherPreviewExample: { } )) } + if let sectionHeader = contentExternalState.sectionHeader { + let headerView: ComponentView + var headerTransition = transition + if let current = self.currentContentHeader, current.id == sectionHeader.id { + headerView = current.view + } else { + headerTransition = headerTransition.withAnimation(.none) + + if let currentContentHeader = self.currentContentHeader { + self.currentContentHeader = nil + if let componentView = currentContentHeader.view.view { + alphaTransition.setAlpha(view: componentView, alpha: 0.0, completion: { [weak componentView] _ in + componentView?.removeFromSuperview() + }) + } + } + + headerView = ComponentView() + self.currentContentHeader = (sectionHeader.id, headerView) + } + let headerSideInset: CGFloat = sideInset + 16.0 + let headerSize = headerView.update( + transition: .immediate, + component: sectionHeader.component, + environment: {}, + containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: 10000.0) + ) + let headerFrame = CGRect(origin: CGPoint(x: headerSideInset, y: contentHeight), size: headerSize) + if let headerComponentView = headerView.view { + if headerComponentView.superview == nil { + headerComponentView.layer.anchorPoint = CGPoint() + self.addSubview(headerComponentView) + alphaTransition.animateAlpha(view: headerComponentView, from: 0.0, to: 1.0) + } + headerTransition.setPosition(view: headerComponentView, position: headerFrame.origin) + headerComponentView.bounds = CGRect(origin: CGPoint(), size: headerFrame.size) + } + contentHeight += headerSize.height + 7.0 + } else { + if let currentContentHeader = self.currentContentHeader { + self.currentContentHeader = nil + if let componentView = currentContentHeader.view.view { + alphaTransition.setAlpha(view: componentView, alpha: 0.0, completion: { [weak componentView] _ in + componentView?.removeFromSuperview() + }) + } + } + } + let content: ComponentView var contentTransition = transition if let current = self.currentContent { @@ -435,9 +999,60 @@ final class TextProcessingContentComponent: Component { } self.currentContentBackground.tintColor = environment.theme.list.itemBlocksBackgroundColor transition.setFrame(view: self.currentContentBackground, frame: contentFrame) - contentHeight += contentSize.height + if let sectionFooter = contentExternalState.sectionFooter { + let footerView: ComponentView + var footerTransition = transition + if let current = self.currentContentFooter, current.id == sectionFooter.id { + footerView = current.view + } else { + footerTransition = footerTransition.withAnimation(.none) + + if let currentContentFooter = self.currentContentFooter { + self.currentContentFooter = nil + if let componentView = currentContentFooter.view.view { + alphaTransition.setAlpha(view: componentView, alpha: 0.0, completion: { [weak componentView] _ in + componentView?.removeFromSuperview() + }) + } + } + + footerView = ComponentView() + self.currentContentFooter = (sectionFooter.id, footerView) + } + let headerSideInset: CGFloat = sideInset + 16.0 + let footerSize = footerView.update( + transition: .immediate, + component: sectionFooter.component, + environment: {}, + containerSize: CGSize(width: availableSize.width - headerSideInset * 2.0, height: 10000.0) + ) + if contentHeight != 0.0 { + contentHeight += 8.0 - UIScreenPixel + } + let footerFrame = CGRect(origin: CGPoint(x: headerSideInset, y: contentHeight), size: footerSize) + if let footerComponentView = footerView.view { + if footerComponentView.superview == nil { + footerComponentView.layer.anchorPoint = CGPoint() + self.addSubview(footerComponentView) + alphaTransition.animateAlpha(view: footerComponentView, from: 0.0, to: 1.0) + } + footerTransition.setPosition(view: footerComponentView, position: footerFrame.origin) + footerComponentView.bounds = CGRect(origin: CGPoint(), size: footerFrame.size) + } + contentHeight += footerSize.height + } else { + if let currentContentFooter = self.currentContentFooter { + self.currentContentFooter = nil + if let componentView = currentContentFooter.view.view { + alphaTransition.setAlpha(view: componentView, alpha: 0.0, completion: { [weak componentView] _ in + componentView?.removeFromSuperview() + }) + } + } + } + var actionsSectionItems: [AnyComponentWithIdentity] = [] if case .translate = component.mode { if let copyTranslation = component.copyCurrentResult { @@ -531,10 +1146,11 @@ private final class TextProcessingSheetComponent: Component { let context: AccountContext let mode: TextProcessingScreen.Mode let ignoredTranslationLanguages: [String] - let styles: [TextProcessingScreen.Style] + let initialStyles: [TextProcessingScreen.Style] let inputText: TextWithEntities let initialEditState: TextProcessingScreen.EditState? let shouldDisplayStyleNotice: Bool + let previewIconFile: TelegramMediaFile? let copyCurrentResult: ((TextWithEntities) -> Void)? let translateChat: ((String) -> Void)? @@ -542,20 +1158,22 @@ private final class TextProcessingSheetComponent: Component { context: AccountContext, mode: TextProcessingScreen.Mode, ignoredTranslationLanguages: [String], - styles: [TextProcessingScreen.Style], + initialStyles: [TextProcessingScreen.Style], inputText: TextWithEntities, initialEditState: TextProcessingScreen.EditState?, shouldDisplayStyleNotice: Bool, + previewIconFile: TelegramMediaFile?, copyCurrentResult: ((TextWithEntities) -> Void)?, translateChat: ((String) -> Void)? ) { self.context = context self.mode = mode self.ignoredTranslationLanguages = ignoredTranslationLanguages - self.styles = styles + self.initialStyles = initialStyles self.inputText = inputText self.initialEditState = initialEditState self.shouldDisplayStyleNotice = shouldDisplayStyleNotice + self.previewIconFile = previewIconFile self.copyCurrentResult = copyCurrentResult self.translateChat = translateChat } @@ -567,18 +1185,19 @@ private final class TextProcessingSheetComponent: Component { final class View: UIView { private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>() private var toast: ComponentView? - private var languageSelectionMenu: ComponentView? private let animateOut = ActionSlot>() private let contentExternalState = TextProcessingContentComponent.ExternalState() + + private var styles: [TextProcessingScreen.Style] = [] private final class LanguageSelectionMenuData { let sourceView: UIView let currentLanguage: String let currentStyle: TelegramComposeAIMessageMode.StyleId let displayStyle: Bool - let completion: (String, TelegramComposeAIMessageMode.StyleId) -> Void + let completion: (String, TelegramComposeAIMessageMode.StyleReference) -> Void - init(sourceView: UIView, currentLanguage: String, currentStyle: TelegramComposeAIMessageMode.StyleId, displayStyle: Bool, completion: @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void) { + init(sourceView: UIView, currentLanguage: String, currentStyle: TelegramComposeAIMessageMode.StyleId, displayStyle: Bool, completion: @escaping (String, TelegramComposeAIMessageMode.StyleReference) -> Void) { self.sourceView = sourceView self.currentLanguage = currentLanguage self.currentStyle = currentStyle @@ -587,10 +1206,21 @@ private final class TextProcessingSheetComponent: Component { } } private var languageSelectionMenuData: LanguageSelectionMenuData? + private var languageSelectionMenu: ComponentView? + + private var styleCreatedToastData: (timer: Foundation.Timer, emojiFile: TelegramMediaFile, style: TelegramComposeAIMessageMode.CloudStyle.Custom)? + private var styleCreatedToast: ComponentView? + + private var customToastData: (timer: Foundation.Timer, text: String)? + private var customToast: ComponentView? private var component: TextProcessingSheetComponent? private var environment: ViewControllerComponentContainer.Environment? private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + private var actionDisposable: Disposable? + private var isPerformingMainAction: Bool = false override init(frame: CGRect) { super.init(frame: frame) @@ -600,6 +1230,12 @@ private final class TextProcessingSheetComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.actionDisposable?.dispose() + self.styleCreatedToastData?.timer.invalidate() + self.customToastData?.timer.invalidate() + } + private func displayLongPressSendMenu(sourceSendButton: UIView) { Task { @MainActor [weak self, weak sourceSendButton] in guard let self, let sourceSendButton, let component = self.component, case let .edit(_, _, _, sendContextActions) = component.mode, let sendContextActions else { @@ -665,7 +1301,7 @@ private final class TextProcessingSheetComponent: Component { currentPrice: nil, hasTimers: false, sendPaidMessageStars: nil, - isMonoforum: peer._asPeer().isMonoForum + isMonoforum: peer.isMonoForum )), hasEntityKeyboard: false, gesture: nil, @@ -717,6 +1353,15 @@ private final class TextProcessingSheetComponent: Component { } func update(component: TextProcessingSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + self.styles = component.initialStyles + } + self.component = component self.state = state @@ -788,6 +1433,36 @@ private final class TextProcessingSheetComponent: Component { performMainAction = { dismiss(true) } + case let .preview(style, _, _, isAlreadyAdded, added): + actionButtonTitle = isAlreadyAdded ? environmentValue.strings.TextProcessing_StyleMenu_ButtonClose : environmentValue.strings.TextProcessing_StyleMenu_ButtonAdd + isMainActionEnabled = !self.isPerformingMainAction + performMainAction = { [weak self] in + guard let self, let component = self.component else { + return + } + self.isPerformingMainAction = true + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + + if isAlreadyAdded { + dismiss(true) + } else { + self.actionDisposable?.dispose() + self.actionDisposable = (component.context.engine.messages.installAIMessageStyle(style: style) |> deliverOnMainQueue).startStrict(error: { [weak self] _ in + guard let self else { + return + } + self.isPerformingMainAction = false + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + }, completed: { + dismiss(true) + added() + }) + } + } } } let copyCurrentResult = component.copyCurrentResult @@ -801,13 +1476,18 @@ private final class TextProcessingSheetComponent: Component { } } + var displayInfoButton = true let titleString: String switch component.mode { case .edit: titleString = environmentValue.strings.TextProcessing_TitleEdit case .translate: titleString = environmentValue.strings.TextProcessing_TitleTranslate + case .preview: + titleString = "" + displayInfoButton = false } + let sheetSize = self.sheet.update( transition: transition, @@ -816,7 +1496,8 @@ private final class TextProcessingSheetComponent: Component { externalState: self.contentExternalState, context: component.context, mode: component.mode, - styles: component.styles, + previewIconFile: component.previewIconFile, + styles: self.styles, inputText: component.inputText, initialEditState: component.initialEditState, ignoredTranslationLanguages: component.ignoredTranslationLanguages, @@ -831,12 +1512,93 @@ private final class TextProcessingSheetComponent: Component { } }, displayLanguageSelectionMenu: { [weak self] sourceView, currentLanguage, currentStyle, displayStyle, completion in - guard let self else { return } + guard let self else { + return + } self.languageSelectionMenuData = LanguageSelectionMenuData(sourceView: sourceView, currentLanguage: currentLanguage, currentStyle: currentStyle, displayStyle: displayStyle, completion: completion) self.state?.updated(transition: .immediate) + }, + newStyleAdded: { [weak self] style in + Task { @MainActor in + guard let self, let component = self.component else { + return + } + var authorPeer: EnginePeer? + if case let .custom(style) = style.content, let authorId = style.authorId { + authorPeer = await component.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: authorId) + ).get() + } + + if case let .custom(style) = style.content, let emojiFileId = style.emojiFileId { + let emojiFile = await component.context.engine.stickers.resolveInlineStickersLocal(fileIds: [emojiFileId]).get().first?.value + + if let emojiFile { + self.styleCreatedToastData = (Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + self.styleCreatedToastData = nil + self.state?.updated(transition: .spring(duration: 0.4)) + }), emojiFile, style) + } + } + + self.styles.insert(TextProcessingScreen.Style(cloudStyle: style, authorPeer: authorPeer), at: 0) + self.state?.updated(transition: .spring(duration: 0.4)) + } + }, + styleUpdated: { [weak self] style in + Task { @MainActor in + guard let self, let component = self.component else { + return + } + guard let index = self.styles.firstIndex(where: { $0.id.id == style.id }) else { + return + } + var authorPeer: EnginePeer? + if case let .custom(style) = style.content, let authorId = style.authorId { + authorPeer = await component.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: authorId) + ).get() + } + self.styles[index] = TextProcessingScreen.Style(cloudStyle: style, authorPeer: authorPeer) + self.state?.updated(transition: .immediate) + } + }, + styleDeleted: { [weak self] id in + guard let self else { + return + } + guard let index = self.styles.firstIndex(where: { $0.id.id == id }) else { + return + } + self.styles.remove(at: index) + self.state?.updated(transition: .spring(duration: 0.4)) + }, + displayToast: { [weak self] text in + guard let self else { + return + } + self.customToastData = (Foundation.Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false, block: { [weak self] _ in + guard let self else { + return + } + self.customToastData = nil + self.state?.updated(transition: .spring(duration: 0.4)) + }), text) + self.state?.updated(transition: .spring(duration: 0.4)) + }, + dismiss: { [weak self] completion in + self?.animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + completion() + }) } )), - titleItem: AnyComponent(TitleComponent( + titleItem: titleString.isEmpty ? nil : AnyComponent(TitleComponent( theme: theme, title: titleString, isProcessing: self.contentExternalState.isProcessing @@ -858,7 +1620,7 @@ private final class TextProcessingSheetComponent: Component { } ) ), - rightItem: AnyComponent( + rightItem: displayInfoButton ? AnyComponent( GlassBarButtonComponent( size: CGSize(width: 44.0, height: 44.0), backgroundColor: nil, @@ -877,7 +1639,8 @@ private final class TextProcessingSheetComponent: Component { environment.controller()?.push(component.context.sharedContext.makeCocoonInfoScreen(context: component.context)) } ) - ), + ) : nil, + hasTopEdgeEffect: displayInfoButton, bottomItem: AnyComponent( ActionButtonsComponent( theme: theme, @@ -997,6 +1760,130 @@ private final class TextProcessingSheetComponent: Component { } } } + + if let styleCreatedToastData = self.styleCreatedToastData, !self.contentExternalState.nonPremiumFloodTriggered { + let sideInset: CGFloat = 8.0 + + let styleCreatedToast: ComponentView + var styleCreatedToastTransition = transition + if let current = self.styleCreatedToast { + styleCreatedToast = current + } else { + styleCreatedToastTransition = styleCreatedToastTransition.withAnimation(.none) + styleCreatedToast = ComponentView() + self.styleCreatedToast = styleCreatedToast + } + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let styleCreatedToastSize = styleCreatedToast.update( + transition: styleCreatedToastTransition, + component: AnyComponent(ToastContentComponent( + icon: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: .animation( + content: .file(file: styleCreatedToastData.emojiFile), + size: CGSize(width: 32.0, height: 32.0), + placeholderColor: environmentValue.theme.list.mediaPlaceholderColor, + themeColor: environmentValue.theme.list.itemPrimaryTextColor, + loopMode: .count(1) + ), + size: CGSize(width: 32.0, height: 32.0), + isVisibleForAnimations: true, + action: nil + )), + content: AnyComponent(VStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environmentValue.strings.TextProcessing_ToastStyleCreated_Title(styleCreatedToastData.style.title).string, font: Font.semibold(14.0), textColor: .white)), + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .markdown(text: environmentValue.strings.TextProcessing_ToastStyleCreated_Text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })), + maximumNumberOfLines: 0 + ))) + ], alignment: .left, spacing: 6.0)), + insets: UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0), + iconSpacing: 12.0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + if let styleCreatedToastView = styleCreatedToast.view { + if styleCreatedToastView.superview == nil, let sheetView = self.sheet.view as? ResizableSheetComponent.View { + sheetView.containerView.addSubview(styleCreatedToastView) + if !transition.animation.isImmediate { + styleCreatedToastView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + styleCreatedToastTransition.setFrame(view: styleCreatedToastView, frame: CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - 94.0 - styleCreatedToastSize.height), size: styleCreatedToastSize)) + } + } else { + if let styleCreatedToast = self.styleCreatedToast { + self.styleCreatedToast = nil + if let styleCreatedToastView = styleCreatedToast.view { + styleCreatedToastView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak styleCreatedToastView] _ in + styleCreatedToastView?.removeFromSuperview() + }) + } + } + } + + if let customToastData = self.customToastData, !self.contentExternalState.nonPremiumFloodTriggered, self.styleCreatedToastData == nil { + let sideInset: CGFloat = 8.0 + + let customToast: ComponentView + var customToastTransition = transition + if let current = self.customToast { + customToast = current + } else { + customToastTransition = customToastTransition.withAnimation(.none) + customToast = ComponentView() + self.customToast = customToast + } + let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) + let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) + let playOnce = ActionSlot() + let customToastSize = customToast.update( + transition: customToastTransition, + component: AnyComponent(ToastContentComponent( + icon: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "anim_infotip"), + startingPosition: .begin, + size: CGSize(width: 32.0, height: 32.0), + playOnce: playOnce + )), + content: AnyComponent(VStack([ + AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .markdown(text: customToastData.text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })), + maximumNumberOfLines: 0 + ))) + ], alignment: .left, spacing: 6.0)), + insets: UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0), + iconSpacing: 12.0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + if let customToastView = customToast.view { + if customToastView.superview == nil, let sheetView = self.sheet.view as? ResizableSheetComponent.View { + sheetView.containerView.addSubview(customToastView) + if !transition.animation.isImmediate { + customToastView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + playOnce.invoke(()) + } + customToastTransition.setFrame(view: customToastView, frame: CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - 94.0 - customToastSize.height), size: customToastSize)) + } + } else { + if let customToast = self.customToast { + self.customToast = nil + if let customToastView = customToast.view { + customToastView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak customToastView] _ in + customToastView?.removeFromSuperview() + }) + } + } + } if let languageSelectionMenuDataValue = self.languageSelectionMenuData { let languageSelectionMenu: ComponentView @@ -1017,10 +1904,12 @@ private final class TextProcessingSheetComponent: Component { topLanguages: [], selectedLanguageCode: languageSelectionMenuDataValue.currentLanguage, currentStyle: languageSelectionMenuDataValue.currentStyle, - displayStyles: languageSelectionMenuDataValue.displayStyle ? component.styles : nil, + displayStyles: languageSelectionMenuDataValue.displayStyle ? self.styles : nil, completion: languageSelectionMenuDataValue.completion, dismissed: { [weak self] in - guard let self else { return } + guard let self else { + return + } self.languageSelectionMenuData = nil self.state?.updated(transition: .immediate) }, @@ -1067,21 +1956,75 @@ public class TextProcessingScreen: ViewControllerComponentContainer { } } - public struct Style: Equatable { - public let type: String + public final class Style: Equatable { + public let reference: TelegramComposeAIMessageMode.CloudStyle.Reference public let title: String public let emojiFileId: Int64? public let emojiFile: TelegramMediaFile? + public let isAuthor: Bool + public let slug: String? + public let cloudStyle: TelegramComposeAIMessageMode.CloudStyle + public let authorPeer: EnginePeer? - public var id: TelegramComposeAIMessageMode.StyleId { - return .style(self.type) + public var id: TelegramComposeAIMessageMode.StyleReference { + return .style(self.reference) } - public init(type: String, title: String, emojiFileId: Int64?, emojiFile: TelegramMediaFile?) { - self.type = type + public init(reference: TelegramComposeAIMessageMode.CloudStyle.Reference, title: String, emojiFileId: Int64?, emojiFile: TelegramMediaFile?, isAuthor: Bool, slug: String?, cloudStyle: TelegramComposeAIMessageMode.CloudStyle, authorPeer: EnginePeer?) { + self.reference = reference self.title = title self.emojiFileId = emojiFileId self.emojiFile = emojiFile + self.isAuthor = isAuthor + self.slug = slug + self.cloudStyle = cloudStyle + self.authorPeer = authorPeer + } + + convenience init(cloudStyle: TelegramComposeAIMessageMode.CloudStyle, authorPeer: EnginePeer?) { + let title: String + let emojiFileId: Int64? + var isAuthor = false + var slug: String? + switch cloudStyle.content { + case let .standard(standard): + title = standard.title + emojiFileId = standard.emojiFileId + case let .custom(custom): + title = custom.title + emojiFileId = custom.emojiFileId + isAuthor = custom.isCreator + slug = custom.slug + } + self.init( + reference: cloudStyle.reference, + title: title, + emojiFileId: emojiFileId, + emojiFile: nil, + isAuthor: isAuthor, + slug: slug, + cloudStyle: cloudStyle, + authorPeer: authorPeer + ) + } + + public static func ==(lhs: Style, rhs: Style) -> Bool { + if lhs.reference != rhs.reference { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.emojiFileId != rhs.emojiFileId { + return false + } + if lhs.emojiFile != rhs.emojiFile { + return false + } + if lhs.cloudStyle != rhs.cloudStyle { + return false + } + return true } } @@ -1099,17 +2042,56 @@ public class TextProcessingScreen: ViewControllerComponentContainer { let rawStyles = await context.engine.messages.composeAIMessageStyles().get() var styles: [Style] = [] - let resolvedEmojiFiles: [Int64: TelegramMediaFile] = await context.engine.stickers.resolveInlineStickersLocal(fileIds: Array(Set(rawStyles.compactMap({ $0.emojiFileId })))).get() + let resolvedEmojiFiles: [Int64: TelegramMediaFile] = await context.engine.stickers.resolveInlineStickersLocal(fileIds: Array(Set(rawStyles.compactMap({ style in + switch style.content { + case let .standard(standard): + return standard.emojiFileId + case let .custom(custom): + return custom.emojiFileId + } + })))).get() for value in rawStyles { + let title: String + let emojiFileId: Int64? + var isAuthor = false + var slug: String? + var authorPeer: EnginePeer? + switch value.content { + case let .standard(standard): + title = standard.title + emojiFileId = standard.emojiFileId + case let .custom(custom): + title = custom.title + emojiFileId = custom.emojiFileId + isAuthor = custom.isCreator + slug = custom.slug + if let authorId = custom.authorId { + authorPeer = await context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: authorId) + ).get() + } + } styles.append(Style( - type: value.type, - title: value.title, - emojiFileId: value.emojiFileId, - emojiFile: value.emojiFileId.flatMap { resolvedEmojiFiles[$0] } + reference: value.reference, + title: title, + emojiFileId: emojiFileId, + emojiFile: emojiFileId.flatMap { resolvedEmojiFiles[$0] }, + isAuthor: isAuthor, + slug: slug, + cloudStyle: value, + authorPeer: authorPeer )) } - let shouldDisplayStyleNotice = await ApplicationSpecificNotice.getAITextProcessingStyleSelection(accountManager: context.sharedContext.accountManager).get() < 3 + var shouldDisplayStyleNotice = false + var previewIconFile: TelegramMediaFile? + if case let .preview(style, _, _, _, _) = mode { + if let emojiFileId = style.emojiFileId { + previewIconFile = await context.engine.stickers.resolveInlineStickersLocal(fileIds: [emojiFileId]).get().first?.value + } + } else { + shouldDisplayStyleNotice = await ApplicationSpecificNotice.getAITextProcessingStyleSelection(accountManager: context.sharedContext.accountManager).get() < 3 + } var initialEditState: EditState? if case let .edit(saveRestoreStateId, _, _, _) = mode, let saveRestoreStateId { @@ -1132,10 +2114,11 @@ public class TextProcessingScreen: ViewControllerComponentContainer { context: context, mode: mode, ignoredTranslationLanguages: translationSettings.ignoredLanguages ?? [], - styles: styles, + initialStyles: styles, inputText: inputText, initialEditState: initialEditState, shouldDisplayStyleNotice: shouldDisplayStyleNotice, + previewIconFile: previewIconFile, copyCurrentResult: copyResult, translateChat: translateChat ), diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift index 7cdaccce1b..e265719ceb 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingStyleSelectionComponent.swift @@ -7,6 +7,9 @@ import MultilineTextComponent import TelegramCore import EmojiStatusComponent import AccountContext +import BundleIconComponent +import GlassBackgroundComponent +import ComponentDisplayAdapters final class TextProcessingStyleSelectionComponent: Component { let context: AccountContext @@ -14,7 +17,9 @@ final class TextProcessingStyleSelectionComponent: Component { let strings: PresentationStrings let styles: [TextProcessingScreen.Style] let selectedStyle: TelegramComposeAIMessageMode.StyleId - let updateStyle: (TelegramComposeAIMessageMode.StyleId) -> Void + let updateStyle: (TelegramComposeAIMessageMode.StyleReference) -> Void + let createStyle: () -> Void + let openStyleContextMenu: (TelegramComposeAIMessageMode.StyleReference, ContextGesture, ContextExtractedContentContainingView) -> Void init( context: AccountContext, @@ -22,7 +27,9 @@ final class TextProcessingStyleSelectionComponent: Component { strings: PresentationStrings, styles: [TextProcessingScreen.Style], selectedStyle: TelegramComposeAIMessageMode.StyleId, - updateStyle: @escaping (TelegramComposeAIMessageMode.StyleId) -> Void + updateStyle: @escaping (TelegramComposeAIMessageMode.StyleReference) -> Void, + createStyle: @escaping () -> Void, + openStyleContextMenu: @escaping (TelegramComposeAIMessageMode.StyleReference, ContextGesture, ContextExtractedContentContainingView) -> Void ) { self.context = context self.theme = theme @@ -30,6 +37,8 @@ final class TextProcessingStyleSelectionComponent: Component { self.styles = styles self.selectedStyle = selectedStyle self.updateStyle = updateStyle + self.createStyle = createStyle + self.openStyleContextMenu = openStyleContextMenu } static func ==(lhs: TextProcessingStyleSelectionComponent, rhs: TextProcessingStyleSelectionComponent) -> Bool { @@ -59,11 +68,19 @@ final class TextProcessingStyleSelectionComponent: Component { private weak var state: EmptyComponentState? private var isUpdating: Bool = false + private let contextGestureContainerView: ContextControllerSourceView private let scrollView: ScrollView private var itemViews: [TelegramComposeAIMessageMode.StyleId: ComponentView] = [:] + private let createStyleItemView = ComponentView() private let selectedBackgroundView: UIImageView + + private var itemWithActiveContextGesture: TelegramComposeAIMessageMode.StyleId? override init(frame: CGRect) { + self.contextGestureContainerView = ContextControllerSourceView() + self.contextGestureContainerView.isGestureEnabled = true + self.contextGestureContainerView.useSublayerTransformForActivation = true + self.scrollView = ScrollView() self.selectedBackgroundView = UIImageView() self.selectedBackgroundView.isHidden = true @@ -81,16 +98,82 @@ final class TextProcessingStyleSelectionComponent: Component { self.scrollView.alwaysBounceVertical = false self.scrollView.scrollsToTop = false self.scrollView.clipsToBounds = false - self.addSubview(self.scrollView) - + self.addSubview(self.contextGestureContainerView) + self.scrollView.addSubview(self.selectedBackgroundView) + self.contextGestureContainerView.addSubview(self.scrollView) self.scrollView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:)))) + + self.contextGestureContainerView.shouldBegin = { [weak self] point in + guard let self, let component = self.component else { + return false + } + guard let (itemId, itemView) = self.item(at: point) else { + return false + } + guard let item = component.styles.first(where: { .style($0.reference.id) == itemId }) else { + return false + } + guard case .custom = item.reference else { + return false + } + guard let itemComponentView = itemView.view as? ItemComponent.View else { + return false + } + + self.itemWithActiveContextGesture = itemId + self.contextGestureContainerView.targetLayerForActivationProgress = itemComponentView.containerView.layer + + let startPoint = point + self.contextGestureContainerView.contextGesture?.externalUpdated = { [weak self] _, point in + guard let self else { + return + } + + let dist = sqrt(pow(startPoint.x - point.x, 2.0) + pow(startPoint.y - point.y, 2.0)) + if dist > 10.0 { + self.contextGestureContainerView.contextGesture?.cancel() + } + } + + return true + } + self.contextGestureContainerView.activated = { [weak self] gesture, _ in + guard let self, let component = self.component else { + return + } + guard let itemWithActiveContextGesture = self.itemWithActiveContextGesture else { + return + } + + var itemView: ItemComponent.View? + itemView = self.itemViews[itemWithActiveContextGesture]?.view as? ItemComponent.View + + guard let itemView else { + return + } + guard let item = component.styles.first(where: { .style($0.reference.id) == itemWithActiveContextGesture }) else { + return + } + component.openStyleContextMenu(item.id, gesture, itemView.contextContainerView) + } } required init?(coder: NSCoder) { preconditionFailure() } + + private func item(at point: CGPoint) -> (id: TelegramComposeAIMessageMode.StyleId, itemView: ComponentView)? { + for (id, itemView) in self.itemViews { + if let itemComponentView = itemView.view { + if itemComponentView.bounds.contains(self.scrollView.convert(self.convert(point, to: self.scrollView), to: itemComponentView)) { + return (id, itemView) + } + } + } + return nil + } @objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) { guard let component = self.component else { @@ -103,16 +186,28 @@ final class TextProcessingStyleSelectionComponent: Component { if component.selectedStyle == id { component.updateStyle(.neutral) } else { - component.updateStyle(id) + if let style = component.styles.first(where: { .style($0.reference.id) == id }) { + component.updateStyle(.style(style.reference)) + } } self.scrollView.scrollRectToVisible(itemComponentView.frame.insetBy(dx: -100.0, dy: 0.0), animated: true) break } } } + if let itemComponentView = self.createStyleItemView.view { + if itemComponentView.bounds.contains(self.scrollView.convert(recognizer.location(in: self.scrollView), to: itemComponentView)) { + component.createStyle() + } + } } } + func scrollToStart() { + let transition: ComponentTransition = .spring(duration: 0.4) + transition.setBounds(view: self.scrollView, bounds: CGRect(origin: CGPoint(), size: self.scrollView.bounds.size)) + } + func update(component: TextProcessingStyleSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -135,12 +230,12 @@ final class TextProcessingStyleSelectionComponent: Component { let style = component.styles[i] let itemView: ComponentView var itemTransition = transition - if let current = self.itemViews[style.id] { + if let current = self.itemViews[.style(style.reference.id)] { itemView = current } else { itemTransition = itemTransition.withAnimation(.none) itemView = ComponentView() - self.itemViews[style.id] = itemView + self.itemViews[.style(style.reference.id)] = itemView } let measuredSize = itemView.update( transition: itemTransition, @@ -154,18 +249,30 @@ final class TextProcessingStyleSelectionComponent: Component { environment: {}, containerSize: CGSize(width: maxItemWidth, height: availableSize.height) ) - itemSizes[style.id] = measuredSize + itemSizes[.style(style.reference.id)] = measuredSize } + + let createStyleItemSize: CGSize = self.createStyleItemView.update( + transition: transition, + component: AnyComponent(CreateItemComponent( + theme: component.theme, + strings: component.strings + )), + environment: {}, + containerSize: CGSize(width: maxItemWidth, height: availableSize.height) + ) // Compute uniform slot width from largest item var largestItemWidth: CGFloat = 0.0 for (_, size) in itemSizes { largestItemWidth = max(largestItemWidth, size.width) } + largestItemWidth = max(largestItemWidth, createStyleItemSize.width) + let contentBasedWidth = min(maxSlotWidth, max(minSlotWidth, largestItemWidth + itemPadding)) let slotWidth: CGFloat - if CGFloat(component.styles.count) * contentBasedWidth <= availableSize.width { - slotWidth = floor(availableSize.width / CGFloat(component.styles.count)) + if CGFloat(component.styles.count + 1) * contentBasedWidth <= availableSize.width { + slotWidth = floor(availableSize.width / CGFloat(component.styles.count + 1)) } else { var resolved: CGFloat = contentBasedWidth var targetVisible: CGFloat = min(7.5, floor((availableSize.width + 16.0) / (contentBasedWidth + 10.0)) + 0.5) @@ -179,39 +286,62 @@ final class TextProcessingStyleSelectionComponent: Component { } slotWidth = resolved } - let contentWidth = slotWidth * CGFloat(component.styles.count) + let contentWidth = slotWidth * CGFloat(component.styles.count + 1) self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) self.scrollView.contentSize = CGSize(width: contentWidth, height: availableSize.height) self.scrollView.alwaysBounceHorizontal = contentWidth > availableSize.width + self.contextGestureContainerView.frame = CGRect(origin: CGPoint(), size: availableSize) // Second pass: position items centered in their slots var selectedItemFrame: CGRect? for i in 0 ..< component.styles.count { let style = component.styles[i] - guard let itemView = self.itemViews[style.id], - let naturalSize = itemSizes[style.id] else { + guard let itemView = self.itemViews[.style(style.reference.id)], + let naturalSize = itemSizes[.style(style.reference.id)] else { continue } - let slotOriginX = CGFloat(i) * slotWidth + let itemSize = CGSize(width: slotWidth, height: naturalSize.height) + let itemFrame = CGRect(origin: CGPoint(x: CGFloat(i) * slotWidth, y: 0.0), size: itemSize) + if let itemComponentView = itemView.view as? ItemComponent.View { + var itemTransition = transition + if itemComponentView.superview == nil { + self.scrollView.addSubview(itemComponentView) + itemTransition = itemTransition.withAnimation(.none) + transition.animateScale(view: itemComponentView, from: 0.001, to: 1.0) + transition.animateAlpha(view: itemComponentView, from: 0.0, to: 1.0) + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + itemComponentView.applySize(measuredSize: naturalSize, size: itemSize, transition: itemTransition) + } + if .style(style.reference.id) == component.selectedStyle { + selectedItemFrame = CGRect(origin: CGPoint(x: itemFrame.minX, y: -5.0), size: CGSize(width: slotWidth, height: availableSize.height + 5.0 + 3.0)) + } + } + + do { + let naturalSize = createStyleItemSize + let slotOriginX = CGFloat(component.styles.count) * slotWidth let itemX = slotOriginX + floor((slotWidth - naturalSize.width) * 0.5) let itemFrame = CGRect(origin: CGPoint(x: itemX, y: 0.0), size: naturalSize) - if let itemComponentView = itemView.view { + if let itemComponentView = self.createStyleItemView.view { if itemComponentView.superview == nil { self.scrollView.addSubview(itemComponentView) } transition.setFrame(view: itemComponentView, frame: itemFrame) } - if style.id == component.selectedStyle { - selectedItemFrame = CGRect(origin: CGPoint(x: slotOriginX, y: -5.0), size: CGSize(width: slotWidth, height: availableSize.height + 5.0 + 3.0)) - } } var removedIds: [TelegramComposeAIMessageMode.StyleId] = [] for (id, itemView) in self.itemViews { - if !component.styles.contains(where: { $0.id == id }) { + if !component.styles.contains(where: { .style($0.reference.id) == id }) { removedIds.append(id) - itemView.view?.removeFromSuperview() + if let itemComponentView = itemView.view { + transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in + itemComponentView?.removeFromSuperview() + }) + transition.setScale(view: itemComponentView, scale: 0.001) + } } } for id in removedIds { @@ -293,6 +423,11 @@ private final class ItemComponent: Component { } final class View: UIView { + let contextContainerView: ContextExtractedContentContainingView + let containerView: UIView + private let backgroundContainer: GlassBackgroundContainerView + private let backgroundView: GlassBackgroundView + private var imageIcon: ComponentView? private let title = ComponentView() @@ -300,13 +435,55 @@ private final class ItemComponent: Component { private weak var state: EmptyComponentState? override init(frame: CGRect) { + self.contextContainerView = ContextExtractedContentContainingView() + self.containerView = UIView() + + self.backgroundContainer = GlassBackgroundContainerView() + self.backgroundContainer.alpha = 0.0 + self.backgroundView = GlassBackgroundView() + self.backgroundContainer.contentView.addSubview(self.backgroundView) + super.init(frame: frame) + + self.addSubview(self.contextContainerView) + self.contextContainerView.contentView.addSubview(self.backgroundContainer) + self.contextContainerView.contentView.addSubview(self.containerView) + + self.contextContainerView.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let self else { + return + } + let transition: ComponentTransition = transition.isAnimated ? .easeInOut(duration: 0.25) : .immediate + if isExtracted { + transition.setAlpha(view: self.backgroundContainer, alpha: 1.0) + } else { + transition.setAlpha(view: self.backgroundContainer, alpha: 0.001) + } + } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + func applySize(measuredSize: CGSize, size: CGSize, transition: ComponentTransition) { + guard let component = self.component else { + return + } + + let containerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - measuredSize.width) * 0.5), y: floor((measuredSize.height - size.height) * 0.5)), size: measuredSize) + let contentRect = CGRect(origin: CGPoint(x: 0.0, y: -5.0 - 4.0), size: CGSize(width: size.width + 0.0, height: size.height + 5.0 + 3.0 + 6.0)) + transition.setFrame(view: self.backgroundContainer, frame: contentRect) + self.backgroundContainer.update(size: contentRect.size, isDark: component.theme.overallDarkAppearance, transition: transition) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: contentRect.size)) + self.backgroundView.update(size: contentRect.size, cornerRadius: 20.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: transition) + + transition.setFrame(view: self.containerView, frame: containerFrame) + transition.setFrame(view: self.contextContainerView, frame: CGRect(origin: CGPoint(), size: size)) + transition.setFrame(view: self.contextContainerView.contentView, frame: CGRect(origin: CGPoint(), size: size)) + self.contextContainerView.contentRect = contentRect.insetBy(dx: -2.0, dy: -2.0) + } + func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component @@ -381,11 +558,105 @@ private final class ItemComponent: Component { let iconFrame = CGRect(origin: CGPoint(x: floor((contentWidth - iconSize.width) * 0.5), y: -3.0), size: iconSize) if let imageIconView = imageIcon.view { if imageIconView.superview == nil { - self.addSubview(imageIconView) + self.containerView.addSubview(imageIconView) } iconTransition.setFrame(view: imageIconView, frame: iconFrame) } + let titleFrame = CGRect(origin: CGPoint(x: floor((contentWidth - titleSize.width) * 0.5), y: availableSize.height - 5.0 - titleSize.height), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.containerView.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let size = CGSize(width: contentWidth, height: availableSize.height) + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class CreateItemComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + + init( + theme: PresentationTheme, + strings: PresentationStrings + ) { + self.theme = theme + self.strings = strings + } + + static func ==(lhs: CreateItemComponent, rhs: CreateItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + return true + } + + final class View: UIView { + private let icon = ComponentView() + private let title = ComponentView() + + private var component: CreateItemComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: CreateItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let iconTintColor = component.theme.list.itemPrimaryTextColor + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: "TextProcessing/NewStyle", + tintColor: iconTintColor + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.strings.TextProcessing_StyleList_Add, font: Font.medium(10.0), textColor: iconTintColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + + let contentWidth = max(iconSize.width, titleSize.width) + + let iconFrame = CGRect(origin: CGPoint(x: floor((contentWidth - iconSize.width) * 0.5), y: -3.0), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + transition.setFrame(view: iconView, frame: iconFrame) + } + let titleFrame = CGRect(origin: CGPoint(x: floor((contentWidth - titleSize.width) * 0.5), y: availableSize.height - 5.0 - titleSize.height), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { 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/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift index 845b487e72..186e69904f 100644 --- a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextProcessingTranslateContentComponent.swift @@ -11,6 +11,8 @@ import BundleIconComponent import TelegramCore import TranslateUI import TooltipComponent +import Markdown +import PlainButtonComponent private let languageRecognizer = NLLanguageRecognizer() @@ -54,10 +56,13 @@ final class TextProcessingTranslateContentComponent: Component { case translate(ignoredLanguages: [String]) case stylize case fix + case preview(from: TextWithEntities?, to: TextWithEntities?, authorPeer: EnginePeer?, userCount: Int, isRequesting: Bool) } final class ExternalState { fileprivate(set) var sourceLanguage: String? + fileprivate(set) var sectionHeader: AnyComponentWithIdentity? + fileprivate(set) var sectionFooter: AnyComponentWithIdentity? fileprivate(set) var result: (language: String, text: TextWithEntities?, textCorrectionRanges: [Range])? = nil { didSet { @@ -70,7 +75,7 @@ final class TextProcessingTranslateContentComponent: Component { fileprivate(set) var emojify: Bool = false fileprivate(set) var isSourceTextExpanded: Bool = false - fileprivate(set) var style: TelegramComposeAIMessageMode.StyleId = .neutral + fileprivate(set) var style: TelegramComposeAIMessageMode.StyleReference = .neutral var displayStyleTooltip: Bool = false fileprivate(set) var isProcessing: Bool = false { @@ -103,9 +108,13 @@ final class TextProcessingTranslateContentComponent: Component { let externalState: ExternalState let mode: Mode let copyAction: (() -> Void)? - let displayLanguageSelectionMenu: (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void) -> Void + let displayLanguageSelectionMenu: (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleReference) -> Void) -> Void + let createStyle: () -> Void + let openStyleContextMenu: (TelegramComposeAIMessageMode.StyleReference, ContextGesture, ContextExtractedContentContainingView) -> Void let present: (ViewController, Any?) -> Void let rootViewForTextSelection: () -> UIView? + let openPeer: (EnginePeer) -> Void + let requestAnotherPreviewExample: () -> Void init( context: AccountContext, @@ -116,9 +125,13 @@ final class TextProcessingTranslateContentComponent: Component { inputText: TextWithEntities, mode: Mode, copyAction: (() -> Void)?, - displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleId) -> Void) -> Void, + displayLanguageSelectionMenu: @escaping (UIView, String, TelegramComposeAIMessageMode.StyleId, Bool, @escaping (String, TelegramComposeAIMessageMode.StyleReference) -> Void) -> Void, + createStyle: @escaping () -> Void, + openStyleContextMenu: @escaping (TelegramComposeAIMessageMode.StyleReference, ContextGesture, ContextExtractedContentContainingView) -> Void, present: @escaping (ViewController, Any?) -> Void, - rootViewForTextSelection: @escaping () -> UIView? + rootViewForTextSelection: @escaping () -> UIView?, + openPeer: @escaping (EnginePeer) -> Void, + requestAnotherPreviewExample: @escaping () -> Void ) { self.context = context self.theme = theme @@ -129,8 +142,12 @@ final class TextProcessingTranslateContentComponent: Component { self.mode = mode self.copyAction = copyAction self.displayLanguageSelectionMenu = displayLanguageSelectionMenu + self.createStyle = createStyle + self.openStyleContextMenu = openStyleContextMenu self.present = present self.rootViewForTextSelection = rootViewForTextSelection + self.openPeer = openPeer + self.requestAnotherPreviewExample = requestAnotherPreviewExample } static func ==(lhs: TextProcessingTranslateContentComponent, rhs: TextProcessingTranslateContentComponent) -> Bool { @@ -222,6 +239,8 @@ final class TextProcessingTranslateContentComponent: Component { } else { mappedMode = .stylize(emojify: component.externalState.emojify, style: component.externalState.style) } + case .preview: + mappedMode = nil case .fix: mappedMode = .proofread } @@ -255,6 +274,12 @@ final class TextProcessingTranslateContentComponent: Component { } } } + + func scrollStylesToStart() { + if let styleSelectionView = self.styleSelection.view as? TextProcessingStyleSelectionComponent.View { + styleSelectionView.scrollToStart() + } + } func update(component: TextProcessingTranslateContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true @@ -318,6 +343,8 @@ final class TextProcessingTranslateContentComponent: Component { case .fix: component.externalState.result = ("", nil, []) self.beginTranslationIfNecessary(reset: false) + case .preview: + component.externalState.result = ("", component.inputText, []) } } @@ -354,6 +381,138 @@ final class TextProcessingTranslateContentComponent: Component { toFormat = component.strings.TextProcessing_ResultBadge } toTitle = "" + case .preview: + fromFormat = component.strings.TextProcessing_StylePreview_Before + toFormat = component.strings.TextProcessing_StylePreview_After + toTitle = "" + } + + var fromText: TextWithEntities? = component.inputText + var fromTextMeasurementString: String? + var toTextMeasurementString: String? + var toText: TextWithEntities? = component.externalState.result?.text + var isPreview = false + if case let .preview(from, to, _, _, isRequesting) = component.mode { + isPreview = true + if isRequesting { + fromText = nil + toText = nil + } else { + fromText = from + toText = to + } + fromTextMeasurementString = from?.text + toTextMeasurementString = to?.text + } + + if case .stylize = component.mode { + if case .style = component.externalState.style, let style = component.styles.first(where: { $0.id == component.externalState.style }), let authorPeer = style.authorPeer { + let footerText: String + if let addressName = authorPeer.addressName { + footerText = component.strings.TextProcessing_StyleFooterAuthor("@" + addressName).string + } else { + footerText = component.strings.TextProcessing_StyleFooterAuthor(authorPeer.displayTitle(strings: component.strings, displayOrder: .firstLast)).string + } + component.externalState.sectionFooter = AnyComponentWithIdentity(id: "style_by_\(authorPeer.id.toInt64())", component: AnyComponent(MultilineTextComponent( + text: .markdown(text: footerText, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: component.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + )), + highlightColor: component.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { [weak self] attributes, _ in + guard let self, let component = self.component else { + return + } + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] as? String { + component.openPeer(authorPeer) + } + } + ))) + } else { + component.externalState.sectionFooter = nil + } + } else if case let .preview(_, _, authorPeer, userCount, _) = component.mode { + component.externalState.sectionHeader = AnyComponentWithIdentity(id: "preview", component: AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .markdown(text: component.strings.TextProcessing_StylePreview_ExampleHeader, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: component.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + )) + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(PlainButtonComponent( + content: AnyComponent(HStack([ + AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + name: "Settings/Refresh", + tintColor: component.theme.list.itemAccentColor + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.strings.TextProcessing_StylePreview_ExampleHeaderRefresh, font: Font.regular(13.0), textColor: component.theme.list.itemAccentColor)) + ))) + ], spacing: 2.0)), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.requestAnotherPreviewExample() + }, + animateScale: false, + animateContents: false + ))), + ], spacing: 8.0, alignment: .alternatingLeftRight))) + + let userCountString = component.strings.TextProcessing_StyleFooterUserCount(Int32(userCount)) + + let footerText: String + if let authorPeer { + if let addressName = authorPeer.addressName { + footerText = component.strings.TextProcessing_StyleFooterCreatedByFormat(userCountString, component.strings.TextProcessing_StyleFooterCreatedBy("@" + addressName).string).string + } else { + footerText = component.strings.TextProcessing_StyleFooterCreatedByFormat(userCountString, component.strings.TextProcessing_StyleFooterCreatedBy(authorPeer.displayTitle(strings: component.strings, displayOrder: .firstLast)).string).string + } + } else { + footerText = component.strings.TextProcessing_StyleFooterCreatedBySimpleFormat(userCountString).string + } + component.externalState.sectionFooter = AnyComponentWithIdentity(id: "style_by_\(authorPeer?.id.toInt64() ?? 0)", component: AnyComponent(MultilineTextComponent( + text: .markdown(text: footerText, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: component.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + )), + highlightColor: component.theme.list.itemAccentColor.withAlphaComponent(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { [weak self] attributes, _ in + guard let self, let component = self.component else { + return + } + if let authorPeer, let _ = attributes[NSAttributedString.Key(rawValue: "URL")] as? String { + component.openPeer(authorPeer) + } + } + ))) } contentHeight += topInset @@ -365,7 +524,7 @@ final class TextProcessingTranslateContentComponent: Component { theme: component.theme, strings: component.strings, styles: component.styles, - selectedStyle: component.externalState.style, + selectedStyle: component.externalState.style.id, updateStyle: { [weak self] style in guard let self, let component = self.component else { return @@ -381,6 +540,18 @@ final class TextProcessingTranslateContentComponent: Component { } } } + }, + createStyle: { [weak self] in + guard let self, let component = self.component else { + return + } + component.createStyle() + }, + openStyleContextMenu: { [weak self] styleId, gesture, sourceView in + guard let self, let component = self.component else { + return + } + component.openStyleContextMenu(styleId, gesture, sourceView) } )), environment: {}, @@ -407,7 +578,7 @@ final class TextProcessingTranslateContentComponent: Component { titleFormat: fromFormat, title: fromTitle, titleAction: nil, - isExpanded: ( + isExpanded: isPreview ? nil : ( component.externalState.isSourceTextExpanded, { [weak self] in guard let self, let component = self.component else { @@ -421,8 +592,8 @@ final class TextProcessingTranslateContentComponent: Component { ), copyAction: nil, emojify: nil, - text: component.inputText, - loadingStateMeasuringText: nil, + text: fromText, + loadingStateMeasuringText: fromTextMeasurementString, textCorrectionRanges: [], present: component.present, rootViewForTextSelection: component.rootViewForTextSelection @@ -462,7 +633,7 @@ final class TextProcessingTranslateContentComponent: Component { guard let self, let component = self.component, let result = component.externalState.result else { return } - component.displayLanguageSelectionMenu(sourceView, result.language, component.externalState.style, true, { [weak self] language, style in + component.displayLanguageSelectionMenu(sourceView, result.language, component.externalState.style.id, true, { [weak self] language, style in guard let self, let component = self.component else { return } @@ -499,8 +670,8 @@ final class TextProcessingTranslateContentComponent: Component { } } ) : nil, - text: component.externalState.result?.text, - loadingStateMeasuringText: component.inputText.text, + text: toText, + loadingStateMeasuringText: toTextMeasurementString ?? component.inputText.text, textCorrectionRanges: component.mode == .fix ? (component.externalState.result?.textCorrectionRanges ?? []) : [], present: component.present, rootViewForTextSelection: component.rootViewForTextSelection diff --git a/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift new file mode 100644 index 0000000000..ae218bcc95 --- /dev/null +++ b/submodules/TelegramUI/Components/TextProcessingScreen/Sources/TextStyleEditScreen.swift @@ -0,0 +1,1108 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import TelegramPresentationData +import ComponentFlow +import ComponentDisplayAdapters +import AccountContext +import ViewControllerComponent +import MultilineTextComponent +import ButtonComponent +import BundleIconComponent +import TelegramCore +import PresentationDataUtils +import ResizableSheetComponent +import GlassBarButtonComponent +import ListSectionComponent +import Markdown +import TelegramUIPreferences +import ListMultilineTextFieldItemComponent +import TextFieldComponent +import ListActionItemComponent +import CheckComponent +import PlainButtonComponent +import EntityKeyboard +import EmojiStatusComponent + +final class TextStyleEditContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + final class ExternalState { + let titleInputState = ListMultilineTextFieldItemComponent.ExternalState() + let textInputState = ListMultilineTextFieldItemComponent.ExternalState() + var isLinkToProfileEnabled: Bool = false + var emojiFile: TelegramMediaFile? + } + + let externalState: ExternalState + let context: AccountContext + let mode: TextStyleEditScreen.Mode + let styleDeleted: () -> Void + + init( + externalState: ExternalState, + context: AccountContext, + mode: TextStyleEditScreen.Mode, + styleDeleted: @escaping () -> Void + ) { + self.externalState = externalState + self.context = context + self.mode = mode + self.styleDeleted = styleDeleted + } + + static func ==(lhs: TextStyleEditContentComponent, rhs: TextStyleEditContentComponent) -> Bool { + return true + } + + private enum Mode { + case translate + case stylize + case fix + } + + final class View: UIView { + private var component: TextStyleEditContentComponent? + private var environment: ViewControllerComponentContainer.Environment? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + private var previousEnvironment: ViewControllerComponentContainer.Environment? + + private let iconBackground = ComponentView() + private let emptyIcon = ComponentView() + private var emojiIcon: ComponentView? + private let titleSection = ComponentView() + private let textSection = ComponentView() + private let deleteSection = ComponentView() + private let linkOption = ComponentView() + + private let titleFieldTag = ListMultilineTextFieldItemComponent.Tag() + private let textFieldTag = ListMultilineTextFieldItemComponent.Tag() + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + private func recenterCaret(hintView: UIView, transition: ComponentTransition) { + var fieldView: ListMultilineTextFieldItemComponent.View? + var ancestor: UIView? = hintView + while let current = ancestor { + if let candidate = current as? ListMultilineTextFieldItemComponent.View { + fieldView = candidate + break + } + ancestor = current.superview + } + guard let fieldView else { + return + } + if !(fieldView.matches(tag: self.titleFieldTag) || fieldView.matches(tag: self.textFieldTag)) { + return + } + guard let inputTextView = fieldView.textFieldView?.inputTextView else { + return + } + let caretPosition = inputTextView.selectedTextRange?.end ?? inputTextView.endOfDocument + let caretRect = inputTextView.caretRect(for: caretPosition) + if caretRect.isNull || caretRect.isInfinite { + return + } + + var scrollAncestor: UIView? = self.superview + var scrollView: UIScrollView? + while let current = scrollAncestor { + if let candidate = current as? UIScrollView { + scrollView = candidate + break + } + scrollAncestor = current.superview + } + guard let scrollView, let environment = self.environment else { + return + } + + let caretInScroll = inputTextView.convert(caretRect, to: scrollView) + + // ResizableSheetComponent bottom action button (52pt) + gap above it (8pt). + let bottomActionAreaHeight: CGFloat = 60.0 + let caretTopInset: CGFloat = 24.0 + let caretBottomInset: CGFloat = 24.0 + let visibleTop = scrollView.bounds.minY + caretTopInset + let visibleBottom = scrollView.bounds.maxY - environment.inputHeight - bottomActionAreaHeight - caretBottomInset + + let previousBounds = scrollView.bounds + var newBounds = previousBounds + if caretInScroll.maxY > visibleBottom { + newBounds.origin.y += (caretInScroll.maxY - visibleBottom) + } else if caretInScroll.minY < visibleTop { + newBounds.origin.y -= (visibleTop - caretInScroll.minY) + } + let maxOriginY = max(0.0, scrollView.contentSize.height - scrollView.bounds.height) + newBounds.origin.y = min(max(0.0, newBounds.origin.y), maxOriginY) + + if newBounds != previousBounds { + scrollView.bounds = newBounds + if !transition.animation.isImmediate { + let offsetY = previousBounds.origin.y - newBounds.origin.y + transition.animateBoundsOrigin(view: scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + } + + func activateEmojiSelection() { + guard let component = self.component else { + return + } + guard let iconBackgroundView = self.iconBackground.view else { + return + } + self.environment?.controller()?.present(component.context.sharedContext.makeEmojiStatusSelectionController( + context: component.context, + mode: .backgroundSelection(completion: { [weak self] file in + guard let self, let component = self.component else { + return + } + component.externalState.emojiFile = file + self.state?.updated(transition: .immediate) + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.state?.updated(transition: .immediate) + } + }), + sourceView: iconBackgroundView, + emojiContent: EmojiPagerContentComponent.emojiInputData( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + isStandalone: false, + subject: .emoji, + hasTrending: false, + topReactionItems: [], + areUnicodeEmojiEnabled: false, + areCustomEmojiEnabled: true, + chatPeerId: component.context.account.peerId, + selectedItems: Set() + ) |> map { $0 }, + currentSelection: nil, + color: nil, + destinationItemView: { [weak self] in + guard let self else { + return nil + } + return self.emojiIcon?.view + } + ), in: .window(.root)) + } + + func update(component: TextStyleEditContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.2) + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let previousEnvironment = self.previousEnvironment + self.previousEnvironment = environment + + var resetTitle: String? + var resetText: String? + if self.component == nil { + resetTitle = "" + if case let .edit(style) = component.mode, case let .custom(style) = style.content { + resetTitle = style.title + resetText = style.prompt ?? "" + component.externalState.isLinkToProfileEnabled = style.authorId != nil + } + } + + self.component = component + self.environment = environment + self.state = state + + let sideInset: CGFloat = 16.0 + let sectionSpacing: CGFloat = 24.0 + let iconSpacing: CGFloat = 24.0 + + var contentHeight: CGFloat = 0.0 + contentHeight += 70.0 + + let iconBackgroundSize = CGSize(width: 100.0, height: 100.0) + let _ = self.iconBackground.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(FilledRoundedRectangleComponent( + color: environment.theme.list.itemBlocksBackgroundColor, + cornerRadius: .minEdge, + smoothCorners: false + )), + action: { [weak self] in + guard let self else { + return + } + self.activateEmojiSelection() + }, + animateAlpha: false, + animateScale: false, + )), + environment: {}, + containerSize: iconBackgroundSize + ) + let iconBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconBackgroundSize.width) * 0.5), y: contentHeight), size: iconBackgroundSize) + if let iconBackgroundView = self.iconBackground.view { + if iconBackgroundView.superview == nil { + self.addSubview(iconBackgroundView) + } + transition.setFrame(view: iconBackgroundView, frame: iconBackgroundFrame) + } + + let emptyIconSize = self.emptyIcon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: "TextProcessing/EditEmojiPlaceholder", + tintColor: environment.theme.list.controlSecondaryColor + )), + environment: {}, + containerSize: iconBackgroundFrame.size + ) + let emptyIconFrame = emptyIconSize.centered(in: iconBackgroundFrame) + if let emptyIconView = self.emptyIcon.view { + if emptyIconView.superview == nil { + self.addSubview(emptyIconView) + emptyIconView.isUserInteractionEnabled = false + } + transition.setFrame(view: emptyIconView, frame: emptyIconFrame) + transition.setAlpha(view: emptyIconView, alpha: component.externalState.emojiFile == nil ? 1.0 : 0.0) + } + + if let emojiFile = component.externalState.emojiFile { + var emojiIconTransition = transition + let emojiIcon: ComponentView + if let current = self.emojiIcon { + emojiIcon = current + } else { + emojiIconTransition = emojiIconTransition.withAnimation(.none) + emojiIcon = ComponentView() + self.emojiIcon = emojiIcon + } + let emojiSize = CGSize(width: 70.0, height: 70.0) + let emojiIconSize = emojiIcon.update( + transition: emojiIconTransition, + component: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: .animation( + content: .file(file: emojiFile), + size: emojiSize, + placeholderColor: environment.theme.list.mediaPlaceholderColor, + themeColor: environment.theme.list.itemPrimaryTextColor, + loopMode: .count(1) + ), + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: emojiSize + ) + let emojiIconFrame = emojiIconSize.centered(in: iconBackgroundFrame) + if let emojiIconView = emojiIcon.view { + if emojiIconView.superview == nil { + self.addSubview(emojiIconView) + emojiIconView.isUserInteractionEnabled = false + } + emojiIconTransition.setFrame(view: emojiIconView, frame: emojiIconFrame) + } + } else { + if let emojiIcon = self.emojiIcon { + self.emojiIcon = nil + if let emojiIconView = emojiIcon.view { + transition.setAlpha(view: emojiIconView, alpha: 0.0, completion: { [weak emojiIconView] _ in + emojiIconView?.removeFromSuperview() + }) + transition.setScale(view: emojiIconView, scale: 0.001) + } + } + } + + contentHeight += iconBackgroundSize.height + iconSpacing + + var titleSectionItems: [AnyComponentWithIdentity] = [] + titleSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent( + externalState: component.externalState.titleInputState, + style: .glass, + context: component.context, + theme: environment.theme, + strings: environment.strings, + initialText: "", + resetText: resetTitle.flatMap { resetTitle in + return ListMultilineTextFieldItemComponent.ResetText(value: resetTitle) + }, + placeholder: environment.strings.TextProcessing_EditStyle_NamePlaceholder, + autocapitalizationType: .sentences, + autocorrectionType: .default, + characterLimit: 12, + emptyLineHandling: .notAllowed, + updated: nil, + textUpdateTransition: .spring(duration: 0.4), + tag: self.titleFieldTag + )))) + let titleSectionSize = self.titleSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + style: .glass, + header: nil, + footer: nil, + items: titleSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let titleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: titleSectionSize) + if let titleSectionView = self.titleSection.view { + if titleSectionView.superview == nil { + self.addSubview(titleSectionView) + self.titleSection.parentState = state + } + transition.setFrame(view: titleSectionView, frame: titleSectionFrame) + } + contentHeight += titleSectionSize.height + contentHeight += sectionSpacing + + var textSectionItems: [AnyComponentWithIdentity] = [] + textSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent( + externalState: component.externalState.textInputState, + style: .glass, + context: component.context, + theme: environment.theme, + strings: environment.strings, + initialText: "", + resetText: resetText.flatMap { resetText in + return ListMultilineTextFieldItemComponent.ResetText(value: resetText) + }, + placeholder: environment.strings.TextProcessing_EditStyle_TextPlaceholder, + placeholderDefinesMinHeight: true, + autocapitalizationType: .sentences, + autocorrectionType: .default, + characterLimit: 1024, + emptyLineHandling: .allowed, + updated: nil, + textUpdateTransition: .spring(duration: 0.4), + tag: self.textFieldTag + )))) + let textSectionSize = self.textSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + style: .glass, + header: nil, + footer: nil, + items: textSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let textSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: textSectionSize) + if let textSectionView = self.textSection.view { + if textSectionView.superview == nil { + self.addSubview(textSectionView) + self.textSection.parentState = state + } + transition.setFrame(view: textSectionView, frame: textSectionFrame) + } + contentHeight += textSectionSize.height + + if case let .edit(style) = component.mode, case let .custom(style) = style.content { + contentHeight += sectionSpacing + + let deleteSectionSize = self.deleteSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + style: .glass, + header: nil, + footer: nil, + items: [AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.TextProcessing_EditStyle_Delete, + font: Font.regular(17.0), + textColor: environment.theme.list.itemDestructiveColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .center, spacing: 2.0, fillWidth: true)), + accessory: nil, + action: { [weak self] _ in + guard let self, let component = self.component, let environment = self.environment else { + return + } + environment.controller()?.push(textAlertController( + context: component.context, + title: environment.strings.TextProcessing_AlertCreatorDeleteStyle_Title, + text: environment.strings.TextProcessing_AlertCreatorDeleteStyle_Text, + actions: [ + TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), + TextAlertAction(type: .destructiveAction, title: environment.strings.Common_Delete, action: { [weak self] in + guard let self, let component = self.component else { + return + } + let _ = component.context.engine.messages.deleteAITextStyle(id: style.id, accessHash: style.accessHash).startStandalone() + component.styleDeleted() + }), + ] + )) + } + )))] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let deleteSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: deleteSectionSize) + if let deleteSectionView = self.deleteSection.view { + if deleteSectionView.superview == nil { + self.addSubview(deleteSectionView) + self.deleteSection.parentState = state + } + transition.setFrame(view: deleteSectionView, frame: deleteSectionFrame) + } + contentHeight += deleteSectionSize.height + } + + contentHeight += 23.0 + + let checkTheme = CheckComponent.Theme( + backgroundColor: environment.theme.list.itemCheckColors.fillColor, + strokeColor: environment.theme.list.itemCheckColors.foregroundColor, + borderColor: environment.theme.list.itemCheckColors.strokeColor, + overlayBorder: false, + hasInset: false, + hasShadow: false + ) + let linkOptionSize = self.linkOption.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(HStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent( + theme: checkTheme, + size: CGSize(width: 18.0, height: 18.0), + selected: component.externalState.isLinkToProfileEnabled + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.TextProcessing_EditStyle_AddLink, font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor)) + ))) + ], spacing: 10.0)), + effectAlignment: .center, + action: { [weak self] in + guard let self else { + return + } + component.externalState.isLinkToProfileEnabled = !component.externalState.isLinkToProfileEnabled + + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + }, + animateAlpha: false, + animateScale: false + )), + environment: { + }, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let linkOptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - linkOptionSize.width) * 0.5), y: contentHeight), size: linkOptionSize) + if let linkOptionView = self.linkOption.view { + if linkOptionView.superview == nil { + self.addSubview(linkOptionView) + } + transition.setFrame(view: linkOptionView, frame: linkOptionFrame) + } + contentHeight += linkOptionSize.height + + contentHeight += 104.0 + + let _ = alphaTransition + + if let hint = transition.userData(TextFieldComponent.AnimationHint.self), let hintView = hint.view { + switch hint.kind { + case .textChanged: + self.recenterCaret(hintView: hintView, transition: transition) + default: + break + } + } + if let previousEnvironment { + var targetView: UIView? + if component.externalState.titleInputState.isEditing { + if let view = self.titleSection.findTaggedView(tag: self.titleFieldTag) as? ListMultilineTextFieldItemComponent.View { + targetView = view + } + } else if component.externalState.textInputState.isEditing { + if let view = self.textSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View { + targetView = view + } + } + + if let targetView { + if (environment.inputHeight == 0.0) != (previousEnvironment.inputHeight == 0.0) { + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.recenterCaret(hintView: targetView, transition: transition) + } + } + } + } + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class TextStyleEditSheetComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let mode: TextStyleEditScreen.Mode + let initialEmojiFile: TelegramMediaFile? + let completion: (TelegramComposeAIMessageMode.CloudStyle) -> Void + let styleDeleted: () -> Void + + init( + context: AccountContext, + mode: TextStyleEditScreen.Mode, + initialEmojiFile: TelegramMediaFile?, + completion: @escaping (TelegramComposeAIMessageMode.CloudStyle) -> Void, + styleDeleted: @escaping () -> Void + ) { + self.context = context + self.mode = mode + self.initialEmojiFile = initialEmojiFile + self.completion = completion + self.styleDeleted = styleDeleted + } + + static func ==(lhs: TextStyleEditSheetComponent, rhs: TextStyleEditSheetComponent) -> Bool { + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>() + private let animateOut = ActionSlot>() + + private var component: TextStyleEditSheetComponent? + private var environment: ViewControllerComponentContainer.Environment? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + + private let contentState = TextStyleEditContentComponent.ExternalState() + + private var isActionInProgress: Bool = false + private var createDisposable: Disposable? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.contentState.titleInputState.updated = { [weak self] in + DispatchQueue.main.async { + guard let self else { + return + } + self.state?.updated(transition: .spring(duration: 0.4)) + } + } + self.contentState.textInputState.updated = self.contentState.titleInputState.updated + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.createDisposable?.dispose() + } + + private func performCreateStyle() { + guard let component = self.component else { + return + } + if self.contentState.titleInputState.text.string.isEmpty || self.contentState.textInputState.text.string.isEmpty { + return + } + guard let emojiFile = self.contentState.emojiFile else { + if let sheetView = self.sheet.view as? ResizableSheetComponent.View { + if let contentView = sheetView.contentViewValue as? TextStyleEditContentComponent.View { + contentView.activateEmojiSelection() + } + } + return + } + + self.createDisposable?.dispose() + + switch component.mode { + case .create: + self.isActionInProgress = true + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + + self.createDisposable = (component.context.engine.messages.createAITextStyle( + displayAuthor: self.contentState.isLinkToProfileEnabled, + emojiFileId: emojiFile.fileId.id, + title: self.contentState.titleInputState.text.string, + prompt: self.contentState.textInputState.text.string + ) + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let self, let component = self.component, let environment = self.environment else { + return + } + let controller = environment.controller + + self.animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + + component.completion(result) + }, error: { [weak self] error in + guard let self else { + return + } + + self.isActionInProgress = false + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + }) + case let .edit(style): + guard case let .custom(style) = style.content else { + return + } + self.isActionInProgress = true + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + self.createDisposable = (component.context.engine.messages.editAITextStyle( + id: style.id, + accessHash: style.accessHash, + displayAuthor: self.contentState.isLinkToProfileEnabled, + emojiFileId: emojiFile.fileId.id, + title: self.contentState.titleInputState.text.string, + prompt: self.contentState.textInputState.text.string + ) + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let self, let component = self.component, let environment = self.environment else { + return + } + let controller = environment.controller + + self.animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + + component.completion(result) + }, error: { [weak self] error in + guard let self else { + return + } + + self.isActionInProgress = false + if !self.isUpdating { + self.state?.updated(transition: .spring(duration: 0.4)) + } + }) + } + } + + func update(component: TextStyleEditSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + if self.component == nil { + self.contentState.emojiFile = component.initialEmojiFile + } + + self.component = component + self.state = state + + let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environmentValue + let controller = environmentValue.controller + let theme = environmentValue.theme + + let dismiss: (Bool) -> Void = { [weak self] animated in + if animated { + self?.animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + + let performMainAction: () -> Void = { [weak self] in + guard let self else { + return + } + self.performCreateStyle() + } + let isMainActionEnabled = !self.contentState.titleInputState.text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !self.contentState.textInputState.text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !self.isActionInProgress + let actionButtonTitle: String + let titleString: String + switch component.mode { + case .create: + titleString = environmentValue.strings.TextProcessing_EditStyle_TitleCreate + actionButtonTitle = environmentValue.strings.TextProcessing_EditStyle_ActionCreate + case .edit: + titleString = environmentValue.strings.TextProcessing_EditStyle_TitleEdit + actionButtonTitle = environmentValue.strings.TextProcessing_EditStyle_ActionEdit + } + + let sheetSize = self.sheet.update( + transition: transition, + component: AnyComponent(ResizableSheetComponent( + content: AnyComponent(TextStyleEditContentComponent( + externalState: self.contentState, + context: component.context, + mode: component.mode, + styleDeleted: component.styleDeleted + )), + titleItem: AnyComponent(TitleComponent( + theme: theme, + title: titleString + )), + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) + ), + rightItem: nil, + bottomItem: AnyComponent( + ActionButtonsComponent( + theme: theme, + strings: environmentValue.strings, + actionTitle: actionButtonTitle, + displayProgress: self.isActionInProgress, + action: isMainActionEnabled ? performMainAction : nil + ) + ), + backgroundColor: .color(theme.list.blocksBackgroundColor), + animateOut: self.animateOut + )), + environment: { + environmentValue + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environmentValue.statusBarHeight, + safeInsets: environmentValue.safeInsets, + inputHeight: environmentValue.inputHeight, + metrics: environmentValue.metrics, + deviceMetrics: environmentValue.deviceMetrics, + isDisplaying: environmentValue.isVisible, + isCentered: environmentValue.metrics.widthClass == .regular, + screenSize: availableSize, + regularMetricsSize: nil, + dismiss: { animated in + dismiss(animated) + } + ) + }, + containerSize: availableSize + ) + self.sheet.parentState = state + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize)) + } + + 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) + } +} + +public class TextStyleEditScreen: ViewControllerComponentContainer { + public enum Mode { + case create + case edit(TelegramComposeAIMessageMode.CloudStyle) + } + + private let context: AccountContext + + public init( + context: AccountContext, + theme: PresentationTheme? = nil, + mode: Mode, + completion: @escaping (TelegramComposeAIMessageMode.CloudStyle) -> Void, + styleDeleted: @escaping () -> Void + ) async { + self.context = context + + var initialEmojiFile: TelegramMediaFile? + if case let .edit(style) = mode, case let .custom(style) = style.content, let emojiFileId = style.emojiFileId { + initialEmojiFile = await context.engine.stickers.resolveInlineStickersLocal(fileIds: [emojiFileId]).get()[emojiFileId] + } + + super.init( + context: context, + component: TextStyleEditSheetComponent( + context: context, + mode: mode, + initialEmojiFile: initialEmojiFile, + completion: completion, + styleDeleted: styleDeleted + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: theme.flatMap({ .custom($0) }) ?? .default + ) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() + } + } +} + +private final class TitleComponent: Component { + let theme: PresentationTheme + let title: String + + init( + theme: PresentationTheme, + title: String + ) { + self.theme = theme + self.title = title + } + + static func ==(lhs: TitleComponent, rhs: TitleComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + + private var component: TitleComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: TitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + return titleSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class ActionButtonsComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let actionTitle: String + let displayProgress: Bool + let action: (() -> Void)? + + init( + theme: PresentationTheme, + strings: PresentationStrings, + actionTitle: String, + displayProgress: Bool, + action: (() -> Void)? + ) { + self.theme = theme + self.strings = strings + self.actionTitle = actionTitle + self.displayProgress = displayProgress + self.action = action + } + + static func ==(lhs: ActionButtonsComponent, rhs: ActionButtonsComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.actionTitle != rhs.actionTitle { + return false + } + if lhs.displayProgress != rhs.displayProgress { + return false + } + if (lhs.action == nil) != (rhs.action == nil) { + return false + } + return true + } + + final class View: UIView { + private let actionButton = ComponentView() + + private var component: ActionButtonsComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ActionButtonsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let actionButtonWidth: CGFloat = availableSize.width + + var actionButtonContents: [AnyComponentWithIdentity] = [] + actionButtonContents.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.actionTitle, font: Font.semibold(17.0), textColor: component.theme.list.itemCheckColors.foregroundColor)) + )))) + + let actionButtonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: component.theme.list.itemCheckColors.fillColor, + foreground: component.theme.list.itemCheckColors.foregroundColor, + pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(HStack( + actionButtonContents, + spacing: 6.0 + )) + ), + isEnabled: component.action != nil, + displaysProgress: component.displayProgress, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.action?() + } + )), + environment: {}, + containerSize: CGSize(width: actionButtonWidth, height: availableSize.height) + ) + let actionButtonFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: actionButtonSize) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + return CGSize(width: availableSize.width, height: actionButtonSize.height) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/TranslateHeaderPanelComponent/Sources/TranslateHeaderPanelComponent.swift b/submodules/TelegramUI/Components/TranslateHeaderPanelComponent/Sources/TranslateHeaderPanelComponent.swift index a684a8b9ac..8a0cb46451 100644 --- a/submodules/TelegramUI/Components/TranslateHeaderPanelComponent/Sources/TranslateHeaderPanelComponent.swift +++ b/submodules/TelegramUI/Components/TranslateHeaderPanelComponent/Sources/TranslateHeaderPanelComponent.swift @@ -7,7 +7,6 @@ import ComponentDisplayAdapters import AccountContext import TelegramCore import SwiftSignalKit -import Postbox import PresentationDataUtils public final class TranslateHeaderPanelComponent: Component { diff --git a/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/BUILD b/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/BUILD index c56da66785..1fd642ae5a 100644 --- a/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/BUILD +++ b/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/BUILD @@ -15,7 +15,6 @@ swift_library( "//submodules/TelegramPresentationData", "//submodules/AccountContext", "//submodules/SSignalKit/SwiftSignalKit", - "//submodules/Postbox", "//submodules/TelegramCore", "//submodules/LocalizedPeerData", "//submodules/TelegramStringFormatting", diff --git a/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/Sources/TranslationLanguagesContextMenuContent.swift b/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/Sources/TranslationLanguagesContextMenuContent.swift index 020a6f1f6b..331f8b6f05 100644 --- a/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/Sources/TranslationLanguagesContextMenuContent.swift +++ b/submodules/TelegramUI/Components/TranslationLanguagesContextMenuContent/Sources/TranslationLanguagesContextMenuContent.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramPresentationData import TelegramStringFormatting diff --git a/submodules/TelegramUI/Components/Utils/AnimatableProperty/Sources/AnimatableProperty.swift b/submodules/TelegramUI/Components/Utils/AnimatableProperty/Sources/AnimatableProperty.swift index 0b735bed62..e65b81b067 100644 --- a/submodules/TelegramUI/Components/Utils/AnimatableProperty/Sources/AnimatableProperty.swift +++ b/submodules/TelegramUI/Components/Utils/AnimatableProperty/Sources/AnimatableProperty.swift @@ -74,6 +74,8 @@ public final class AnimatableProperty { break case .easeInOut: t = listViewAnimationCurveEaseInOut(t) + case .easeIn: + t = listViewAnimationCurveEaseIn(t) case .spring: t = lookupSpringValue(t) case let .custom(x1, y1, x2, y2): 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 { diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index 4cc16219bc..97e52bec43 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -441,7 +441,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { } }, error: { [weak self] _ in if let self, let controller = self.getController() { - controller.completion(nil, nil, nil) + controller.completion(nil, nil, nil, nil) } })) } @@ -536,7 +536,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { let availableSize = context.component.containerSize var sideInset: CGFloat = 8.0 - if component.safeInsets.bottom <= 32.0 { + if component.safeInsets.bottom <= 32.0 && environment.inputHeight == 0.0 { sideInset += 18.0 } @@ -575,14 +575,18 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { if !component.isPreviewing { if case .on = component.cameraState.flashMode, case .front = component.cameraState.position { + let frontFlashPosition = CGPoint(x: component.previewFrame.midX, y: component.previewFrame.midY) + let frontFlashSize = CGSize( + width: max(abs(frontFlashPosition.x), abs(context.availableSize.width - frontFlashPosition.x)) * 2.0, + height: max(abs(frontFlashPosition.y), abs(context.availableSize.height - frontFlashPosition.y)) * 2.0 + ) let frontFlash = frontFlash.update( - component: Image(image: state.image(.flashImage, theme: environment.theme), tintColor: component.cameraState.flashTint.color), - availableSize: context.availableSize, + component: Image(image: state.image(.flashImage, theme: environment.theme), tintColor: component.cameraState.flashTint.color, contentMode: .scaleAspectFill), + availableSize: frontFlashSize, transition: .easeInOut(duration: 0.2) ) - let frontFlashFrame = CGRect(origin: CGPoint(), size: context.availableSize) context.add(frontFlash - .position(frontFlashFrame.center) + .position(frontFlashPosition) .scale(1.5 - component.cameraState.flashTintSize * 0.5) .appear(.default(alpha: true)) .disappear(ComponentTransition.Disappear({ view, transition, completion in @@ -1641,7 +1645,7 @@ public class VideoMessageCameraScreen: ViewController { fileprivate var allowLiveUpload: Bool fileprivate var viewOnceAvailable: Bool - fileprivate let completion: (EnqueueMessage?, Bool?, Int32?) -> Void + fileprivate let completion: (EnqueueMessage?, Bool?, Int32?, Int32?) -> Void private var audioSessionDisposable: Disposable? @@ -1794,7 +1798,7 @@ public class VideoMessageCameraScreen: ViewController { viewOnceAvailable: Bool, inputPanelFrame: (CGRect, Bool), chatNode: ASDisplayNode?, - completion: @escaping (EnqueueMessage?, Bool?, Int32?) -> Void + completion: @escaping (EnqueueMessage?, Bool?, Int32?, Int32?) -> Void ) { self.context = context self.updatedPresentationData = updatedPresentationData @@ -1833,7 +1837,7 @@ public class VideoMessageCameraScreen: ViewController { fileprivate var didSend = false fileprivate var lastActionTimestamp: Double? fileprivate var isSendingImmediately = false - public func sendVideoRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, messageEffect: ChatSendMessageEffect? = nil) { + public func sendVideoRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, repeatPeriod: Int32? = nil, messageEffect: ChatSendMessageEffect? = nil) { guard !self.didSend else { return } @@ -1845,7 +1849,7 @@ public class VideoMessageCameraScreen: ViewController { } if case .none = self.cameraState.recording, self.node.results.isEmpty { - self.completion(nil, nil, nil) + self.completion(nil, nil, nil, nil) return } @@ -1859,7 +1863,7 @@ public class VideoMessageCameraScreen: ViewController { self.waitingForNextResult = true self.node.stopRecording.invoke(Void()) } else { - self.completion(nil, nil, nil) + self.completion(nil, nil, nil, nil) return } } @@ -1889,7 +1893,7 @@ public class VideoMessageCameraScreen: ViewController { } if duration < 1.0 { - self.completion(nil, nil, nil) + self.completion(nil, nil, nil, nil) return } @@ -1955,7 +1959,7 @@ public class VideoMessageCameraScreen: ViewController { } if !hasAdjustments, let liveUploadData, let data = try? Data(contentsOf: URL(fileURLWithPath: video.videoPath)) { resource = LocalFileMediaResource(fileId: liveUploadData.id) - self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) } else { resource = LocalFileVideoMediaResource(randomId: Int64.random(in: Int64.min ... Int64.max), paths: videoPaths, adjustments: resourceAdjustments) } @@ -1965,7 +1969,7 @@ public class VideoMessageCameraScreen: ViewController { let thumbnailResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) let thumbnailSize = video.dimensions.cgSize.aspectFitted(CGSize(width: 320.0, height: 320.0)) if let thumbnailData = scaleImageToPixelSize(image: thumbnailImage, size: thumbnailSize)?.jpegData(compressionQuality: 0.4) { - self.context.account.postbox.mediaBox.storeResourceData(thumbnailResource.id, data: thumbnailData) + self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(thumbnailResource.id), data: thumbnailData) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(thumbnailSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } @@ -1998,7 +2002,7 @@ public class VideoMessageCameraScreen: ViewController { localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [] - ), silentPosting, scheduleTime) + ), silentPosting, scheduleTime, repeatPeriod) }) }) } diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf b/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf deleted file mode 100644 index 6c97d03a30..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Avatar8.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@2x.png b/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@2x.png deleted file mode 100644 index 1d51eb1725..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@3x.png b/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@3x.png deleted file mode 100644 index 95e99b4db2..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/CallCancelIcon@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/Contents.json deleted file mode 100644 index 313c758607..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Call/CallCancelButton.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "CallCancelIcon@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "CallCancelIcon@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/CallRouteBluetooth@2x.png b/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/CallRouteBluetooth@2x.png deleted file mode 100644 index 55a0de9e65..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/CallRouteBluetooth@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/CallRouteBluetooth@3x.png b/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/CallRouteBluetooth@3x.png deleted file mode 100644 index 0f680c2fe6..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/CallRouteBluetooth@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/Contents.json deleted file mode 100644 index d221f407ce..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Call/CallRouteBluetooth.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "CallRouteBluetooth@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "CallRouteBluetooth@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallRouteSpeaker.imageset/ic_calls_speaker.pdf b/submodules/TelegramUI/Images.xcassets/Call/CallRouteSpeaker.imageset/ic_calls_speaker.pdf deleted file mode 100644 index 22af6e39c6..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Call/CallRouteSpeaker.imageset/ic_calls_speaker.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/Contents.json deleted file mode 100644 index 2ef9a3ee1a..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png deleted file mode 100644 index 9b5e566eb4..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@2x_Before_b542dc3fd7b381de80e554286a94ccd5b8d02154.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png b/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png deleted file mode 100644 index 0026e6063d..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Call/LegacyCallRouteSpeaker.imageset/submodules_TelegramUI_Images.xcassets_Call_CallRouteSpeaker.imageset_CallRouteSpeaker@3x_Before_c8c1c96f16a3977d2a6d1956a0aeb31768c6bb23.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/ic_voicevolumeoff.pdf b/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/ic_voicevolumeoff.pdf deleted file mode 100644 index bcc068ce30..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/ic_voicevolumeoff.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/ic_cam_flip.pdf b/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/ic_cam_flip.pdf deleted file mode 100644 index 5084218a29..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/ic_cam_flip.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/CallRouteSpeaker.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/CallIncomingIcon.imageset/Contents.json similarity index 74% rename from submodules/TelegramUI/Images.xcassets/Call/CallRouteSpeaker.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Chat List/CallIncomingIcon.imageset/Contents.json index d0d69abee5..da60b22b56 100644 --- a/submodules/TelegramUI/Images.xcassets/Call/CallRouteSpeaker.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat List/CallIncomingIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_calls_speaker.pdf", + "filename" : "incoming_call_20.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/CallIncomingIcon.imageset/incoming_call_20.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/CallIncomingIcon.imageset/incoming_call_20.pdf new file mode 100644 index 0000000000..af90dd7613 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/CallIncomingIcon.imageset/incoming_call_20.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/CallOutgoingIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/CallOutgoingIcon.imageset/Contents.json new file mode 100644 index 0000000000..9b101ab4da --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/CallOutgoingIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "outgoing_call_20.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/CallOutgoingIcon.imageset/outgoing_call_20.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/CallOutgoingIcon.imageset/outgoing_call_20.pdf new file mode 100644 index 0000000000..61af8fc2bb Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/CallOutgoingIcon.imageset/outgoing_call_20.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/CallVideoIncomingIcon.imageset/Contents.json similarity index 71% rename from submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Chat List/CallVideoIncomingIcon.imageset/Contents.json index 95ca49b9d3..c409ed977a 100644 --- a/submodules/TelegramUI/Images.xcassets/Call/StatusVolumeOff.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat List/CallVideoIncomingIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_voicevolumeoff.pdf", + "filename" : "incoming_video_call_20.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/CallVideoIncomingIcon.imageset/incoming_video_call_20.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/CallVideoIncomingIcon.imageset/incoming_video_call_20.pdf new file mode 100644 index 0000000000..979cb9e3af Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/CallVideoIncomingIcon.imageset/incoming_video_call_20.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/CallVideoOutgoingIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/CallVideoOutgoingIcon.imageset/Contents.json new file mode 100644 index 0000000000..01d2bc12c2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/CallVideoOutgoingIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "outgoing_video_call_20.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/CallVideoOutgoingIcon.imageset/outgoing_video_call_20.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/CallVideoOutgoingIcon.imageset/outgoing_video_call_20.pdf new file mode 100644 index 0000000000..bb7773265c Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/CallVideoOutgoingIcon.imageset/outgoing_video_call_20.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/GameIcon.imageset/Contents.json similarity index 76% rename from submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Chat List/GameIcon.imageset/Contents.json index fb10bc0705..7375ad0121 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat List/GameIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "shrink_24.pdf", + "filename" : "gamepad_20.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/GameIcon.imageset/gamepad_20.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/GameIcon.imageset/gamepad_20.pdf new file mode 100644 index 0000000000..7304fb98ad Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/GameIcon.imageset/gamepad_20.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/TodoIcon.imageset/Contents.json similarity index 75% rename from submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Chat List/TodoIcon.imageset/Contents.json index e6f1b93d3e..d2f8bb359c 100644 --- a/submodules/TelegramUI/Images.xcassets/Call/SwitchCameraIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat List/TodoIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "ic_cam_flip.pdf", + "filename" : "checklist_20.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/TodoIcon.imageset/checklist_20.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/TodoIcon.imageset/checklist_20.pdf new file mode 100644 index 0000000000..63fe6b59e9 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat List/TodoIcon.imageset/checklist_20.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/VoiceMessageIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/VoiceMessageIcon.imageset/Contents.json new file mode 100644 index 0000000000..75aa24b443 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/VoiceMessageIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "voice_message_20.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/giftcrash.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/VoiceMessageIcon.imageset/voice_message_20.pdf similarity index 56% rename from submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/giftcrash.pdf rename to submodules/TelegramUI/Images.xcassets/Chat List/VoiceMessageIcon.imageset/voice_message_20.pdf index 2bdf30a259..4d0f9fb5dc 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/giftcrash.pdf and b/submodules/TelegramUI/Images.xcassets/Chat List/VoiceMessageIcon.imageset/voice_message_20.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeMark.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddMusic.imageset/Contents.json similarity index 77% rename from submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeMark.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddMusic.imageset/Contents.json index eda08930c8..fb844031d6 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeMark.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddMusic.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "18on_24.pdf", + "filename" : "addmusic.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddMusic.imageset/addmusic.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddMusic.imageset/addmusic.pdf new file mode 100644 index 0000000000..6939e1fdab Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AddMusic.imageset/addmusic.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeMark.imageset/18on_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeMark.imageset/18on_24.pdf deleted file mode 100644 index 356e934807..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeMark.imageset/18on_24.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeUnmark.imageset/18off_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeUnmark.imageset/18off_24.pdf deleted file mode 100644 index 974496474d..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeUnmark.imageset/18off_24.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/Contents.json deleted file mode 100644 index 1caab208d5..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "hidecaption_24.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/hidecaption_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/hidecaption_24.pdf deleted file mode 100644 index 3d86f95ada..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionHide.imageset/hidecaption_24.pdf +++ /dev/null @@ -1,177 +0,0 @@ -%PDF-1.7 - -1 0 obj - << >> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 3.334961 3.205170 cm -0.000000 0.000000 0.000000 scn -1.135289 17.265064 m -0.875590 17.524763 0.454536 17.524763 0.194837 17.265064 c --0.064862 17.005365 -0.064862 16.584312 0.194837 16.324614 c -16.194838 0.324612 l -16.454536 0.064913 16.875591 0.064913 17.135290 0.324612 c -17.394989 0.584311 17.394989 1.005365 17.135290 1.265064 c -15.270563 3.129791 l -15.665111 3.129791 l -16.032377 3.129791 16.330109 3.427522 16.330109 3.794791 c -16.330109 4.162061 16.032377 4.459791 15.665111 4.459791 c -13.940563 4.459791 l -7.330003 11.070351 l -7.330003 12.694838 l -7.330004 12.718859 l -7.330019 12.978289 7.330032 13.211649 7.314171 13.405788 c -7.297186 13.613665 7.258753 13.834405 7.148529 14.050732 c -6.988900 14.364021 6.734188 14.618734 6.420897 14.778363 c -6.204570 14.888588 5.983830 14.927021 5.775954 14.944005 c -5.581833 14.959866 5.348500 14.959853 5.089100 14.959841 c -5.089095 14.959841 l -5.089057 14.959841 l -5.089017 14.959841 l -5.065003 14.959841 l -3.440514 14.959841 l -1.135289 17.265064 l -h -4.770516 13.629837 m -6.000003 12.400351 l -6.000003 12.694838 l -6.000003 12.985837 5.999486 13.164092 5.988588 13.297483 c -5.983510 13.359637 5.977127 13.397297 5.971728 13.420219 c -5.969160 13.431128 5.967000 13.437864 5.965689 13.441540 c -5.964900 13.443751 5.964303 13.445174 5.963926 13.446011 c -5.963701 13.446509 5.963554 13.446799 5.963490 13.446924 c -5.931373 13.509958 5.880125 13.561207 5.817090 13.593325 c -5.816755 13.593495 5.815236 13.594263 5.811705 13.595523 c -5.808031 13.596835 5.801293 13.598994 5.790386 13.601562 c -5.767462 13.606961 5.729803 13.613344 5.667649 13.618422 c -5.534258 13.629320 5.356003 13.629837 5.065003 13.629837 c -4.770516 13.629837 l -h -0.181478 14.050732 m -0.267737 14.220026 0.381761 14.372214 0.517790 14.501538 c -1.459741 13.559587 l -1.420951 13.529512 1.389032 13.491115 1.366516 13.446924 c -1.366346 13.446590 1.365578 13.445070 1.364318 13.441540 c -1.363006 13.437864 1.360847 13.431128 1.358278 13.420219 c -1.352880 13.397297 1.346497 13.359637 1.341419 13.297483 c -1.330521 13.164092 1.330003 12.985837 1.330003 12.694838 c -1.330003 9.894837 l -1.330003 9.603838 1.330521 9.425583 1.341419 9.292192 c -1.346497 9.230038 1.352880 9.192378 1.358278 9.169455 c -1.360847 9.158547 1.363006 9.151810 1.364318 9.148135 c -1.365578 9.144605 1.366346 9.143085 1.366516 9.142751 c -1.398634 9.079716 1.449882 9.028467 1.512917 8.996350 c -1.513251 8.996180 1.514771 8.995412 1.518302 8.994151 c -1.521976 8.992840 1.528713 8.990681 1.539621 8.988112 c -1.562544 8.982714 1.600204 8.976331 1.662358 8.971252 c -1.795749 8.960355 1.974004 8.959837 2.265003 8.959837 c -5.065003 8.959837 l -5.356003 8.959837 5.534258 8.960355 5.667649 8.971252 c -5.729803 8.976331 5.767462 8.982714 5.790386 8.988112 c -5.801293 8.990681 5.808031 8.992840 5.811705 8.994151 c -5.815236 8.995412 5.816755 8.996180 5.817090 8.996350 c -5.861280 9.018866 5.899678 9.050784 5.929753 9.089574 c -6.871704 8.147623 l -6.742380 8.011595 6.590191 7.897571 6.420897 7.811312 c -6.204570 7.701087 5.983830 7.662654 5.775953 7.645670 c -5.581820 7.629809 5.348469 7.629822 5.089050 7.629836 c -5.089025 7.629836 l -5.065003 7.629837 l -2.265003 7.629837 l -2.240981 7.629836 l -2.240957 7.629836 l -1.981538 7.629822 1.748187 7.629809 1.554053 7.645670 c -1.346177 7.662654 1.125436 7.701087 0.909109 7.811312 c -0.595819 7.970941 0.341107 8.225653 0.181478 8.538943 c -0.071253 8.755270 0.032820 8.976010 0.015836 9.183887 c --0.000024 9.378011 -0.000013 9.611347 0.000001 9.870750 c -0.000001 9.870787 l -0.000001 9.870823 l -0.000001 9.894837 l -0.000001 12.694838 l -0.000001 12.718851 l -0.000001 12.718889 l -0.000001 12.718924 l --0.000013 12.978327 -0.000024 13.211664 0.015836 13.405788 c -0.032820 13.613665 0.071253 13.834405 0.181478 14.050732 c -h -1.665110 4.459791 m -10.559536 4.459791 l -11.889536 3.129791 l -1.665110 3.129791 l -1.297841 3.129791 1.000110 3.427522 1.000110 3.794791 c -1.000110 4.162061 1.297841 4.459791 1.665110 4.459791 c -h -10.665111 14.459792 m -10.297841 14.459792 10.000111 14.162062 10.000111 13.794792 c -10.000111 13.427523 10.297841 13.129791 10.665111 13.129791 c -15.665111 13.129791 l -16.032377 13.129791 16.330109 13.427523 16.330109 13.794792 c -16.330109 14.162062 16.032377 14.459792 15.665111 14.459792 c -10.665111 14.459792 l -h -12.665111 9.459791 m -12.297841 9.459791 12.000111 9.162061 12.000111 8.794791 c -12.000111 8.427522 12.297841 8.129791 12.665111 8.129791 c -15.665111 8.129791 l -16.032377 8.129791 16.330109 8.427522 16.330109 8.794791 c -16.330109 9.162061 16.032377 9.459791 15.665111 9.459791 c -12.665111 9.459791 l -h -f* -n -Q - -endstream -endobj - -3 0 obj - 4618 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Pages 5 0 R - /Type /Catalog - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000004708 00000 n -0000004731 00000 n -0000004904 00000 n -0000004978 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -5037 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/Contents.json deleted file mode 100644 index 8b6179bcaf..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "showcaption_24.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/showcaption_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/showcaption_24.pdf deleted file mode 100644 index 174e22bccd..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/CaptionShow.imageset/showcaption_24.pdf +++ /dev/null @@ -1,169 +0,0 @@ -%PDF-1.7 - -1 0 obj - << >> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 3.334961 6.334900 cm -0.000000 0.000000 0.000000 scn -2.265003 11.830077 m -2.240989 11.830077 l -2.240949 11.830077 l -1.981532 11.830091 1.748184 11.830104 1.554052 11.814242 c -1.346176 11.797258 1.125435 11.758825 0.909108 11.648602 c -0.595819 11.488972 0.341106 11.234260 0.181477 10.920970 c -0.071253 10.704642 0.032820 10.483902 0.015835 10.276026 c --0.000026 10.081894 -0.000013 9.848544 0.000001 9.589127 c -0.000001 9.589089 l -0.000001 9.565076 l -0.000001 6.765075 l -0.000001 6.741061 l -0.000001 6.741024 l --0.000013 6.481606 -0.000026 6.248257 0.015835 6.054125 c -0.032820 5.846249 0.071253 5.625508 0.181477 5.409181 c -0.341106 5.095891 0.595819 4.841178 0.909108 4.681550 c -1.125435 4.571325 1.346176 4.532892 1.554052 4.515908 c -1.748192 4.500046 1.981551 4.500060 2.240980 4.500074 c -2.265002 4.500075 l -5.065003 4.500075 l -5.089025 4.500074 l -5.348454 4.500060 5.581814 4.500046 5.775953 4.515908 c -5.983829 4.532892 6.204570 4.571325 6.420897 4.681550 c -6.734187 4.841178 6.988900 5.095891 7.148529 5.409181 c -7.258753 5.625508 7.297186 5.846249 7.314170 6.054125 c -7.330032 6.248264 7.330019 6.481624 7.330004 6.741053 c -7.330003 6.765075 l -7.330003 9.565075 l -7.330004 9.589098 l -7.330019 9.848527 7.330032 10.081886 7.314170 10.276026 c -7.297186 10.483902 7.258753 10.704642 7.148529 10.920970 c -6.988900 11.234260 6.734187 11.488972 6.420897 11.648602 c -6.204570 11.758825 5.983829 11.797258 5.775953 11.814242 c -5.581822 11.830104 5.348474 11.830091 5.089057 11.830077 c -5.089017 11.830077 l -5.065003 11.830077 l -2.265003 11.830077 l -h -1.512916 10.463563 m -1.513250 10.463733 1.514770 10.464500 1.518301 10.465761 c -1.521975 10.467072 1.528712 10.469232 1.539620 10.471801 c -1.562544 10.477199 1.600203 10.483582 1.662357 10.488660 c -1.795748 10.499558 1.974003 10.500075 2.265003 10.500075 c -5.065003 10.500075 l -5.356002 10.500075 5.534257 10.499558 5.667649 10.488660 c -5.729803 10.483582 5.767462 10.477199 5.790385 10.471801 c -5.801293 10.469232 5.808030 10.467072 5.811704 10.465761 c -5.815235 10.464500 5.816755 10.463733 5.817090 10.463563 c -5.880124 10.431445 5.931373 10.380197 5.963490 10.317163 c -5.963660 10.316828 5.964428 10.315308 5.965689 10.311777 c -5.967000 10.308103 5.969159 10.301366 5.971728 10.290458 c -5.977127 10.267534 5.983509 10.229876 5.988587 10.167721 c -5.999485 10.034330 6.000003 9.856074 6.000003 9.565075 c -6.000003 6.765075 l -6.000003 6.474076 5.999485 6.295821 5.988587 6.162429 c -5.983509 6.100276 5.977127 6.062616 5.971728 6.039693 c -5.969159 6.028785 5.967000 6.022048 5.965689 6.018374 c -5.964428 6.014843 5.963660 6.013323 5.963490 6.012989 c -5.931373 5.949954 5.880124 5.898705 5.817090 5.866588 c -5.816755 5.866418 5.815235 5.865650 5.811705 5.864389 c -5.808030 5.863078 5.801293 5.860919 5.790385 5.858350 c -5.767462 5.852952 5.729803 5.846569 5.667649 5.841491 c -5.534257 5.830593 5.356002 5.830075 5.065003 5.830075 c -2.265002 5.830075 l -1.974003 5.830075 1.795748 5.830593 1.662357 5.841491 c -1.600203 5.846569 1.562544 5.852952 1.539620 5.858350 c -1.528712 5.860919 1.521975 5.863078 1.518301 5.864389 c -1.514770 5.865650 1.513250 5.866418 1.512916 5.866588 c -1.449882 5.898705 1.398633 5.949954 1.366516 6.012989 c -1.366345 6.013323 1.365577 6.014843 1.364317 6.018373 c -1.363005 6.022048 1.360846 6.028785 1.358277 6.039693 c -1.352879 6.062616 1.346496 6.100276 1.341418 6.162429 c -1.330520 6.295821 1.330003 6.474076 1.330003 6.765075 c -1.330003 9.565076 l -1.330003 9.856075 1.330520 10.034330 1.341418 10.167721 c -1.346496 10.229876 1.352879 10.267534 1.358277 10.290458 c -1.360846 10.301366 1.363005 10.308103 1.364317 10.311777 c -1.365577 10.315308 1.366345 10.316828 1.366516 10.317163 c -1.398633 10.380197 1.449882 10.431445 1.512916 10.463563 c -h -10.665110 11.330029 m -10.297840 11.330029 10.000110 11.032299 10.000110 10.665030 c -10.000110 10.297760 10.297840 10.000030 10.665110 10.000030 c -15.665110 10.000030 l -16.032377 10.000030 16.330109 10.297760 16.330109 10.665030 c -16.330109 11.032299 16.032377 11.330029 15.665110 11.330029 c -10.665110 11.330029 l -h -10.665110 6.330029 m -10.297840 6.330029 10.000110 6.032299 10.000110 5.665030 c -10.000110 5.297760 10.297840 5.000030 10.665110 5.000030 c -15.665110 5.000030 l -16.032377 5.000030 16.330109 5.297760 16.330109 5.665030 c -16.330109 6.032299 16.032377 6.330029 15.665110 6.330029 c -10.665110 6.330029 l -h -1.000109 0.665030 m -1.000109 1.032299 1.297840 1.330029 1.665109 1.330029 c -15.665110 1.330029 l -16.032377 1.330029 16.330109 1.032299 16.330109 0.665030 c -16.330109 0.297760 16.032377 0.000030 15.665110 0.000030 c -1.665109 0.000030 l -1.297840 0.000030 1.000109 0.297760 1.000109 0.665030 c -h -f* -n -Q - -endstream -endobj - -3 0 obj - 4706 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Pages 5 0 R - /Type /Catalog - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000004796 00000 n -0000004819 00000 n -0000004992 00000 n -0000005066 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -5125 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DarkMode.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DarkMode.imageset/Contents.json deleted file mode 100644 index 97b7ab0446..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DarkMode.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_lt_darkmode.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DarkMode.imageset/ic_lt_darkmode.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DarkMode.imageset/ic_lt_darkmode.pdf deleted file mode 100644 index 98a41e4986..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/DarkMode.imageset/ic_lt_darkmode.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/shrink_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/shrink_24.pdf deleted file mode 100644 index 0b4710ee5d..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ImageShrink.imageset/shrink_24.pdf +++ /dev/null @@ -1,169 +0,0 @@ -%PDF-1.7 - -1 0 obj - << >> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 3.835083 3.834961 cm -0.000000 0.000000 0.000000 scn -5.436178 16.330017 m -5.465000 16.330017 l -10.865000 16.330017 l -10.893822 16.330017 l -11.709461 16.330023 12.362126 16.330027 12.889546 16.286936 c -13.430926 16.242702 13.898637 16.149773 14.328876 15.930555 c -15.018489 15.579180 15.579163 15.018506 15.930539 14.328892 c -16.149757 13.898653 16.242687 13.430943 16.286919 12.889563 c -16.330009 12.362154 16.330006 11.709505 16.330000 10.893888 c -16.330000 10.893824 l -16.330000 10.865017 l -16.330000 5.465017 l -16.330000 5.436212 l -16.330000 5.436146 l -16.330006 4.620529 16.330009 3.967880 16.286919 3.440471 c -16.242687 2.899091 16.149757 2.431380 15.930539 2.001142 c -15.579163 1.311528 15.018489 0.750854 14.328876 0.399478 c -13.898637 0.180260 13.430927 0.087330 12.889546 0.043098 c -12.362138 0.000008 11.709489 0.000011 10.893871 0.000017 c -10.893806 0.000017 l -10.865000 0.000017 l -5.465000 0.000017 l -5.436194 0.000017 l -5.436130 0.000017 l -4.620512 0.000011 3.967863 0.000008 3.440454 0.043098 c -2.899074 0.087330 2.431364 0.180260 2.001125 0.399478 c -1.311511 0.750854 0.750837 1.311528 0.399461 2.001142 c -0.180244 2.431380 0.087314 2.899090 0.043081 3.440471 c --0.000010 3.967890 -0.000006 4.620555 0.000000 5.436195 c -0.000000 5.465017 l -0.000000 10.865017 l -0.000000 10.893839 l --0.000006 11.709478 -0.000010 12.362144 0.043081 12.889563 c -0.087314 13.430943 0.180244 13.898653 0.399461 14.328892 c -0.750837 15.018506 1.311511 15.579180 2.001125 15.930555 c -2.431364 16.149773 2.899074 16.242702 3.440454 16.286936 c -3.967873 16.330027 4.620538 16.330023 5.436178 16.330017 c -h -3.548759 14.961352 m -3.089627 14.923841 2.816429 14.853280 2.604933 14.745517 c -2.165574 14.521652 1.808364 14.164443 1.584500 13.725084 c -1.476737 13.513588 1.406177 13.240391 1.368664 12.781259 c -1.330518 12.314363 1.330000 11.716068 1.330000 10.865017 c -1.330000 5.465017 l -1.330000 4.613965 1.330518 4.015671 1.368664 3.548775 c -1.406177 3.089643 1.476737 2.816445 1.584500 2.604949 c -1.808364 2.165591 2.165574 1.808381 2.604933 1.584517 c -2.816429 1.476754 3.089627 1.406194 3.548759 1.368681 c -4.015654 1.330534 4.613948 1.330017 5.465000 1.330017 c -10.865000 1.330017 l -11.716052 1.330017 12.314346 1.330534 12.781242 1.368681 c -13.240374 1.406194 13.513572 1.476754 13.725068 1.584517 c -14.164426 1.808381 14.521636 2.165591 14.745501 2.604949 c -14.853263 2.816445 14.923823 3.089643 14.961336 3.548776 c -14.999483 4.015671 15.000000 4.613965 15.000000 5.465017 c -15.000000 10.865017 l -15.000000 11.716068 14.999483 12.314363 14.961336 12.781259 c -14.923823 13.240391 14.853263 13.513588 14.745501 13.725084 c -14.521636 14.164443 14.164426 14.521652 13.725068 14.745517 c -13.513572 14.853280 13.240374 14.923841 12.781241 14.961352 c -12.314346 14.999499 11.716052 15.000017 10.865000 15.000017 c -5.465000 15.000017 l -4.613948 15.000017 4.015654 14.999499 3.548759 14.961352 c -h -7.829878 12.164894 m -7.829878 12.532164 7.532147 12.829895 7.164878 12.829895 c -6.797609 12.829895 6.499878 12.532164 6.499878 12.164894 c -6.499878 10.770347 l -4.635104 12.635120 l -4.375406 12.894819 3.954351 12.894819 3.694653 12.635120 c -3.434954 12.375422 3.434954 11.954367 3.694653 11.694669 c -5.559426 9.829895 l -4.164878 9.829895 l -3.797609 9.829895 3.499878 9.532164 3.499878 9.164894 c -3.499878 8.797626 3.797609 8.499895 4.164878 8.499895 c -7.148878 8.499895 l -7.153147 8.499901 l -7.157188 8.499939 l -7.329931 8.497953 7.503299 8.562863 7.635104 8.694669 c -7.766910 8.826473 7.831820 8.999842 7.829834 9.172585 c -7.829872 9.176626 l -7.829878 9.180895 l -7.829878 12.164894 l -h -9.164833 3.499878 m -8.797565 3.499878 8.499834 3.797609 8.499834 4.164879 c -8.499834 7.148878 l -8.499840 7.153147 l -8.499878 7.157188 l -8.497892 7.329931 8.562802 7.503300 8.694608 7.635104 c -8.826412 7.766910 8.999781 7.831820 9.172523 7.829834 c -9.176565 7.829872 l -9.180834 7.829878 l -12.164833 7.829878 l -12.532103 7.829878 12.829834 7.532147 12.829834 7.164879 c -12.829834 6.797609 12.532103 6.499878 12.164833 6.499878 c -10.770286 6.499878 l -12.635059 4.635104 l -12.894758 4.375405 12.894758 3.954351 12.635059 3.694653 c -12.375360 3.434954 11.954307 3.434954 11.694608 3.694653 c -9.829834 5.559426 l -9.829834 4.164879 l -9.829834 3.797609 9.532103 3.499878 9.164833 3.499878 c -h -f* -n -Q - -endstream -endobj - -3 0 obj - 4327 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Pages 5 0 R - /Type /Catalog - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000004417 00000 n -0000004440 00000 n -0000004613 00000 n -0000004687 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -4746 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Mute2h.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Mute2h.imageset/Contents.json deleted file mode 100644 index 5b28aa91ea..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Mute2h.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ic_mute2hours.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Mute2h.imageset/ic_mute2hours.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Mute2h.imageset/ic_mute2hours.pdf deleted file mode 100644 index 25545ba34e..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Mute2h.imageset/ic_mute2hours.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Unstar.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Unstar.imageset/Contents.json deleted file mode 100644 index 96ba4365cd..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Unstar.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "Size=24px, Type=Crossed Out.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Unstar.imageset/Size=24px, Type=Crossed Out.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Unstar.imageset/Size=24px, Type=Crossed Out.pdf deleted file mode 100644 index 0b20e7ad20..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Unstar.imageset/Size=24px, Type=Crossed Out.pdf +++ /dev/null @@ -1,165 +0,0 @@ -%PDF-1.7 - -1 0 obj - << >> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 4.834961 4.361450 cm -0.000000 0.000000 0.000000 scn -0.294029 13.569108 m -0.148725 13.203500 0.079447 12.808213 0.043083 12.363134 c --0.000009 11.835724 -0.000005 11.183071 0.000001 10.367449 c -0.000001 10.367412 l -0.000001 10.338589 l -0.000001 2.887971 l -0.000001 2.855927 l -0.000001 2.855909 l --0.000013 2.361413 -0.000024 1.944829 0.029606 1.619491 c -0.058879 1.298060 0.124788 0.933573 0.357819 0.638192 c -0.669126 0.243593 1.142026 0.010736 1.644602 0.004579 c -2.020809 -0.000031 2.349897 0.169960 2.622518 0.342736 c -2.898462 0.517618 3.228661 0.771631 3.620616 1.073153 c -3.645996 1.092677 l -6.594922 3.361081 l -6.992292 3.666750 7.044882 3.691571 7.078142 3.700500 c -7.135041 3.715775 7.194962 3.715775 7.251861 3.700500 c -7.285121 3.691571 7.337711 3.666750 7.735081 3.361081 c -10.684008 1.092677 l -10.709353 1.073179 l -11.101323 0.771646 11.431534 0.517624 11.707485 0.342736 c -11.980106 0.169960 12.309195 -0.000031 12.685401 0.004579 c -13.008502 0.008537 13.319336 0.106193 13.582854 0.280283 c -12.355464 1.507673 l -12.146183 1.646584 11.878798 1.851580 11.494923 2.146868 c -8.545997 4.415272 l -8.477973 4.467776 l -8.477962 4.467785 l -8.199948 4.682729 7.921870 4.897722 7.596705 4.985017 c -7.313910 5.060937 7.016093 5.060937 6.733298 4.985017 c -6.408127 4.897721 6.130046 4.682723 5.852028 4.467772 c -5.784007 4.415271 l -2.835081 2.146868 l -2.410956 1.820618 2.129028 1.604588 1.910557 1.466129 c -1.735109 1.354937 1.664445 1.337146 1.654627 1.334675 c -1.654414 1.334620 l -1.557878 1.337669 1.467334 1.382254 1.406059 1.456912 c -1.405972 1.457113 l -1.401946 1.466402 1.372963 1.533260 1.354124 1.740117 c -1.330665 1.997703 1.330002 2.352881 1.330002 2.887971 c -1.330002 10.338589 l -1.330002 11.189641 1.330519 11.787935 1.368666 12.254830 c -1.375014 12.332534 1.382310 12.404911 1.390571 12.472566 c -0.294029 13.569108 l -h -12.999968 2.744075 m -12.999996 2.790545 13.000002 2.838489 13.000002 2.887970 c -13.000002 10.338589 l -13.000002 11.189640 12.999485 11.787935 12.961338 12.254830 c -12.923825 12.713963 12.853265 12.987160 12.745502 13.198656 c -12.521638 13.638015 12.164428 13.995224 11.725070 14.219089 c -11.513574 14.326852 11.240376 14.397411 10.781243 14.434924 c -10.314348 14.473071 9.716053 14.473588 8.865002 14.473588 c -5.465002 14.473588 l -4.613950 14.473588 4.015655 14.473071 3.548759 14.434924 c -3.089628 14.397411 2.816430 14.326852 2.604934 14.219089 c -2.372593 14.100705 2.163225 13.945032 1.984401 13.759640 c -1.043852 14.700190 l -1.320836 14.983611 1.643658 15.221989 2.001126 15.404127 c -2.431365 15.623345 2.899075 15.716275 3.440455 15.760508 c -3.967874 15.803599 4.620536 15.803595 5.436175 15.803589 c -5.436180 15.803589 l -5.465002 15.803589 l -8.865002 15.803589 l -8.893824 15.803589 l -9.709463 15.803595 10.362128 15.803599 10.889548 15.760508 c -11.430928 15.716275 11.898639 15.623345 12.328877 15.404127 c -13.018491 15.052752 13.579165 14.492078 13.930541 13.802464 c -14.149758 13.372225 14.242688 12.904515 14.286921 12.363134 c -14.330012 11.835714 14.330008 11.183048 14.330002 10.367406 c -14.330002 10.367395 l -14.330002 10.338589 l -14.330002 2.887970 l -14.330002 2.855918 l -14.330016 2.361417 14.330028 1.944830 14.300398 1.619491 c -14.295680 1.567688 14.290010 1.514769 14.282844 1.461199 c -12.999968 2.744075 l -h -f* -n -Q -q -1.000000 0.000000 -0.000000 1.000000 4.500000 3.540039 cm -0.000000 0.000000 0.000000 scn -0.470226 17.930187 m -0.210527 18.189886 -0.210527 18.189886 -0.470226 17.930187 c --0.729925 17.670488 -0.729925 17.249434 -0.470226 16.989735 c -0.470226 17.930187 l -h -15.529774 0.989735 m -15.789473 0.730036 16.210527 0.730036 16.470226 0.989735 c -16.729925 1.249434 16.729925 1.670488 16.470226 1.930187 c -15.529774 0.989735 l -h --0.470226 16.989735 m -15.529774 0.989735 l -16.470226 1.930187 l -0.470226 17.930187 l --0.470226 16.989735 l -h -f -n -Q - -endstream -endobj - -3 0 obj - 3871 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Pages 5 0 R - /Type /Catalog - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000003961 00000 n -0000003984 00000 n -0000004157 00000 n -0000004231 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -4290 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/Contents.json deleted file mode 100644 index ab416f72c8..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "hideforward_24.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/hideforward_24.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/hideforward_24.pdf deleted file mode 100644 index b05af4f661..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/UserCrossed.imageset/hideforward_24.pdf +++ /dev/null @@ -1,112 +0,0 @@ -%PDF-1.7 - -1 0 obj - << >> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 3.334961 1.270081 cm -0.000000 0.000000 0.000000 scn -6.330028 15.229959 m -6.330028 16.519543 7.375443 17.564959 8.665028 17.564959 c -9.954613 17.564959 11.000028 16.519543 11.000028 15.229959 c -11.000028 13.940373 9.954613 12.894958 8.665028 12.894958 c -7.375443 12.894958 6.330028 13.940373 6.330028 15.229959 c -h -8.665028 18.894958 m -6.640904 18.894958 5.000028 17.254082 5.000028 15.229959 c -5.000028 13.205835 6.640904 11.564959 8.665028 11.564959 c -10.689151 11.564959 12.330028 13.205835 12.330028 15.229959 c -12.330028 17.254082 10.689151 18.894958 8.665028 18.894958 c -h -2.847365 5.874736 m -3.182212 6.620716 3.727596 7.408447 4.622032 8.012360 c -5.052609 8.303081 5.574740 8.558434 6.209703 8.744807 c -5.164166 9.790345 l -4.686711 9.600591 4.259488 9.372349 3.877790 9.114632 c -2.736064 8.343752 2.047248 7.340033 1.633996 6.419378 c -1.238339 5.537922 1.434021 4.662781 1.966020 4.035870 c -2.481754 3.428125 3.295742 3.064959 4.165028 3.064959 c -11.889552 3.064959 l -10.559552 4.394958 l -4.165028 4.394958 l -3.653601 4.394958 3.222711 4.610523 2.980097 4.896420 c -2.753749 5.163151 2.677613 5.496559 2.847365 5.874736 c -h -14.796587 3.538785 m -17.135227 1.200146 l -17.394926 0.940447 17.394926 0.519392 17.135227 0.259693 c -16.875528 -0.000006 16.454473 -0.000006 16.194775 0.259693 c -0.194774 16.259693 l --0.064925 16.519392 -0.064925 16.940447 0.194774 17.200146 c -0.454473 17.459845 0.875527 17.459845 1.135226 17.200146 c -7.960464 10.374907 l -8.188262 10.388149 8.423051 10.394958 8.665028 10.394958 c -10.754458 10.394958 12.307954 9.887258 13.452266 9.114632 c -14.593992 8.343751 15.282808 7.340032 15.696060 6.419377 c -16.091717 5.537921 15.896035 4.662780 15.364036 4.035870 c -15.201860 3.844761 15.010192 3.677837 14.796587 3.538785 c -h -13.815245 4.520127 m -14.036389 4.610582 14.220131 4.743431 14.349958 4.896420 c -14.576306 5.163150 14.652442 5.496557 14.482691 5.874735 c -14.147844 6.620715 13.602460 7.408447 12.708024 8.012359 c -11.919037 8.545074 10.822649 8.959038 9.287930 9.047441 c -13.815245 4.520127 l -h -f* -n -Q - -endstream -endobj - -3 0 obj - 2105 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Pages 5 0 R - /Type /Catalog - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000002195 00000 n -0000002218 00000 n -0000002391 00000 n -0000002465 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -2524 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/PremiumRequiredIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/PremiumRequiredIcon.imageset/Contents.json deleted file mode 100644 index a2cead8511..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/PremiumRequiredIcon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "premiumlockedchat.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/PremiumRequiredIcon.imageset/premiumlockedchat.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/PremiumRequiredIcon.imageset/premiumlockedchat.pdf deleted file mode 100644 index 56f0ca020c..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Empty Chat/PremiumRequiredIcon.imageset/premiumlockedchat.pdf +++ /dev/null @@ -1,188 +0,0 @@ -%PDF-1.7 - -1 0 obj - << /Type /XObject - /Length 2 0 R - /Group << /Type /Group - /S /Transparency - >> - /Subtype /Form - /Resources << >> - /BBox [ 0.000000 0.000000 120.000000 120.000000 ] - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 26.666748 23.947998 cm -0.000000 0.000000 0.000000 scn -33.333332 67.118652 m -51.742825 67.118652 66.666664 53.551525 66.666664 36.815620 c -66.666664 20.079723 51.742825 6.512592 33.333332 6.512592 c -30.510870 6.512592 27.770340 6.831497 25.152962 7.431835 c -24.657856 7.545395 24.095694 7.026123 23.157236 6.159271 c -22.022297 5.110928 20.337004 3.554222 17.554379 1.993690 c -13.999221 -0.000084 9.073529 0.172890 8.239902 0.522552 c -7.440670 0.857796 8.055842 1.524185 9.139198 2.697731 c -9.888692 3.509621 10.862268 4.564251 11.746614 5.919903 c -13.909238 9.235085 13.015616 13.181854 12.045396 13.892178 c -4.458577 19.446659 0.000000 27.270615 0.000000 36.815620 c -0.000000 53.551525 14.923841 67.118652 33.333332 67.118652 c -h -25.627058 49.241592 m -26.466211 54.000668 31.004473 57.178391 35.763550 56.339237 c -40.522621 55.500084 43.700348 50.961823 42.861195 46.202747 c -41.652504 39.347935 l -42.190289 39.251141 42.644272 39.108437 43.057198 38.898041 c -44.186165 38.322800 45.104050 37.404915 45.679291 36.275944 c -46.333252 34.992474 46.333252 33.312317 46.333252 29.952000 c -46.333252 28.152000 l -46.333252 24.791687 46.333252 23.111526 45.679291 21.828056 c -45.104050 20.699085 44.186165 19.781200 43.057198 19.205959 c -41.773727 18.552002 40.093567 18.552002 36.733253 18.552002 c -29.933254 18.552002 l -26.572937 18.552002 24.892780 18.552002 23.609308 19.205959 c -22.480337 19.781200 21.562452 20.699085 20.987213 21.828056 c -20.333252 23.111526 20.333252 24.791687 20.333252 28.152000 c -20.333252 29.952000 l -20.333252 33.312317 20.333252 34.992474 20.987213 36.275944 c -21.562452 37.404915 22.480337 38.322800 23.609308 38.898041 c -24.892780 39.552002 26.572937 39.552002 29.933252 39.552002 c -36.733253 39.552002 l -37.045204 39.552002 37.342670 39.552002 37.626686 39.551476 c -38.921963 46.897339 l -39.377502 49.480839 37.652451 51.944466 35.068954 52.400005 c -32.485458 52.855545 30.021830 51.130497 29.566288 48.546997 c -29.436054 47.808395 l -29.244247 46.720604 28.206930 45.994267 27.119141 46.186073 c -26.031353 46.377880 25.305016 47.415199 25.496822 48.502983 c -25.627058 49.241592 l -h -f* -n -Q - -endstream -endobj - -2 0 obj - 2182 -endobj - -3 0 obj - << /Type /XObject - /Length 4 0 R - /Group << /Type /Group - /S /Transparency - >> - /Subtype /Form - /Resources << >> - /BBox [ 0.000000 0.000000 120.000000 120.000000 ] - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 10.000000 9.000000 cm -0.000000 0.000000 0.000000 scn -0.000000 62.666668 m -0.000000 75.734558 0.000000 82.268509 2.543181 87.259781 c -4.780226 91.650230 8.349772 95.219772 12.740221 97.456818 c -17.731495 100.000000 24.265440 100.000000 37.333332 100.000000 c -62.666668 100.000000 l -75.734558 100.000000 82.268509 100.000000 87.259781 97.456818 c -91.650230 95.219772 95.219772 91.650230 97.456818 87.259781 c -100.000000 82.268509 100.000000 75.734558 100.000000 62.666668 c -100.000000 37.333332 l -100.000000 24.265442 100.000000 17.731491 97.456818 12.740219 c -95.219772 8.349770 91.650230 4.780228 87.259781 2.543182 c -82.268509 0.000000 75.734558 0.000000 62.666668 0.000000 c -37.333332 0.000000 l -24.265440 0.000000 17.731495 0.000000 12.740221 2.543182 c -8.349772 4.780228 4.780226 8.349770 2.543181 12.740219 c -0.000000 17.731491 0.000000 24.265442 0.000000 37.333332 c -0.000000 62.666668 l -h -f -n -Q - -endstream -endobj - -4 0 obj - 969 -endobj - -5 0 obj - << /XObject << /X1 1 0 R >> - /ExtGState << /E1 << /SMask << /Type /Mask - /G 3 0 R - /S /Alpha - >> - /Type /ExtGState - >> >> - >> -endobj - -6 0 obj - << /Length 7 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -/E1 gs -/X1 Do -Q - -endstream -endobj - -7 0 obj - 46 -endobj - -8 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 120.000000 120.000000 ] - /Resources 5 0 R - /Contents 6 0 R - /Parent 9 0 R - >> -endobj - -9 0 obj - << /Kids [ 8 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -10 0 obj - << /Pages 9 0 R - /Type /Catalog - >> -endobj - -xref -0 11 -0000000000 65535 f -0000000010 00000 n -0000002442 00000 n -0000002465 00000 n -0000003684 00000 n -0000003706 00000 n -0000004004 00000 n -0000004106 00000 n -0000004127 00000 n -0000004302 00000 n -0000004376 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 10 0 R - /Size 11 ->> -startxref -4436 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/Contents.json deleted file mode 100644 index ee96d83980..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "search_24.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/search_24.svg b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/search_24.svg deleted file mode 100644 index aebf081900..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/search_24.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/AccessoryIconReaction.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/AccessoryIconReaction.imageset/Contents.json deleted file mode 100644 index dcbff79c27..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/AccessoryIconReaction.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "InputReactionIcon.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/AccessoryIconReaction.imageset/InputReactionIcon.svg b/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/AccessoryIconReaction.imageset/InputReactionIcon.svg deleted file mode 100644 index 0e358991b8..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Input/Text/AccessoryIconReaction.imageset/InputReactionIcon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpiredStoryPlaceholder.imageset/Slice 1.png b/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpiredStoryPlaceholder.imageset/Slice 1.png deleted file mode 100644 index 7546315a2c..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpiredStoryPlaceholder.imageset/Slice 1.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExternalLink.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Message/ExternalLink.imageset/Contents.json deleted file mode 100644 index 740a85579e..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExternalLink.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "addlink_16.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExternalLink.imageset/addlink_16.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Message/ExternalLink.imageset/addlink_16.pdf deleted file mode 100644 index 0eb367086f..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExternalLink.imageset/addlink_16.pdf +++ /dev/null @@ -1,236 +0,0 @@ -%PDF-1.7 - -1 0 obj - << >> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 8.500000 7.040154 cm -0.000000 0.000000 0.000000 scn --0.470226 1.930072 m --0.729925 1.670374 -0.729925 1.249319 -0.470226 0.989621 c --0.210527 0.729922 0.210527 0.729922 0.470226 0.989621 c --0.470226 1.930072 l -h -6.470226 6.989621 m -6.729925 7.249319 6.729925 7.670374 6.470226 7.930072 c -6.210527 8.189772 5.789473 8.189772 5.529774 7.930072 c -6.470226 6.989621 l -h -0.470226 0.989621 m -6.470226 6.989621 l -5.529774 7.930072 l --0.470226 1.930072 l -0.470226 0.989621 l -h -f -n -Q -q -1.000000 0.000000 -0.000000 1.000000 9.500000 8.170006 cm -0.000000 0.000000 0.000000 scn -0.000000 6.994994 m --0.367269 6.994994 -0.665000 6.697264 -0.665000 6.329994 c --0.665000 5.962725 -0.367269 5.664994 0.000000 5.664994 c -0.000000 6.994994 l -h -5.000000 6.329994 m -5.665000 6.329994 l -5.665000 6.697264 5.367270 6.994994 5.000000 6.994994 c -5.000000 6.329994 l -h -4.335000 1.329994 m -4.335000 0.962725 4.632730 0.664994 5.000000 0.664994 c -5.367270 0.664994 5.665000 0.962725 5.665000 1.329994 c -4.335000 1.329994 l -h -0.000000 5.664994 m -5.000000 5.664994 l -5.000000 6.994994 l -0.000000 6.994994 l -0.000000 5.664994 l -h -4.335000 6.329994 m -4.335000 1.329994 l -5.665000 1.329994 l -5.665000 6.329994 l -4.335000 6.329994 l -h -f -n -Q -q -1.000000 0.000000 -0.000000 1.000000 3.000000 1.668968 cm -0.000000 0.000000 0.000000 scn -4.000000 10.666032 m -4.367270 10.666032 4.665000 10.963762 4.665000 11.331032 c -4.665000 11.698301 4.367270 11.996032 4.000000 11.996032 c -4.000000 10.666032 l -h -10.665000 5.331032 m -10.665000 5.698301 10.367270 5.996032 10.000000 5.996032 c -9.632730 5.996032 9.335000 5.698301 9.335000 5.331032 c -10.665000 5.331032 l -h -8.907981 1.549019 m -9.209885 0.956499 l -8.907981 1.549019 l -h -9.782013 2.423051 m -10.374533 2.121147 l -9.782013 2.423051 l -h -1.092019 11.113045 m -0.790115 11.705564 l -1.092019 11.113045 l -h -0.217987 10.239013 m --0.374532 10.540916 l -0.217987 10.239013 l -h -4.000000 11.996032 m -3.200000 11.996032 l -3.200000 10.666032 l -4.000000 10.666032 l -4.000000 11.996032 l -h --0.665000 8.131032 m --0.665000 4.531032 l -0.665000 4.531032 l -0.665000 8.131032 l --0.665000 8.131032 l -h -3.200000 0.666032 m -6.800000 0.666032 l -6.800000 1.996032 l -3.200000 1.996032 l -3.200000 0.666032 l -h -10.665000 4.531032 m -10.665000 5.331032 l -9.335000 5.331032 l -9.335000 4.531032 l -10.665000 4.531032 l -h -6.800000 0.666032 m -7.349080 0.666032 7.800883 0.665515 8.167748 0.695489 c -8.542377 0.726097 8.886601 0.791779 9.209885 0.956499 c -8.606077 2.141538 l -8.501536 2.088272 8.351824 2.044960 8.059443 2.021071 c -7.759301 1.996549 7.371026 1.996032 6.800000 1.996032 c -6.800000 0.666032 l -h -9.335000 4.531032 m -9.335000 3.960006 9.334483 3.571731 9.309960 3.271588 c -9.286072 2.979208 9.242760 2.829495 9.189494 2.724955 c -10.374533 2.121147 l -10.539253 2.444430 10.604935 2.788655 10.635543 3.163283 c -10.665517 3.530149 10.665000 3.981952 10.665000 4.531032 c -9.335000 4.531032 l -h -9.209885 0.956499 m -9.711337 1.212002 10.119030 1.619695 10.374533 2.121147 c -9.189494 2.724955 l -9.061502 2.473758 8.857274 2.269529 8.606077 2.141538 c -9.209885 0.956499 l -h --0.665000 4.531032 m --0.665000 3.981952 -0.665517 3.530149 -0.635543 3.163283 c --0.604935 2.788655 -0.539253 2.444430 -0.374532 2.121147 c -0.810506 2.724955 l -0.757240 2.829495 0.713928 2.979208 0.690040 3.271588 c -0.665517 3.571731 0.665000 3.960006 0.665000 4.531032 c --0.665000 4.531032 l -h -3.200000 1.996032 m -2.628974 1.996032 2.240699 1.996549 1.940556 2.021071 c -1.648176 2.044960 1.498463 2.088272 1.393923 2.141538 c -0.790115 0.956499 l -1.113398 0.791779 1.457623 0.726097 1.832252 0.695489 c -2.199117 0.665515 2.650921 0.666032 3.200000 0.666032 c -3.200000 1.996032 l -h --0.374532 2.121147 m --0.119030 1.619695 0.288663 1.212002 0.790115 0.956499 c -1.393923 2.141538 l -1.142726 2.269529 0.938497 2.473758 0.810506 2.724955 c --0.374532 2.121147 l -h -3.200000 11.996032 m -2.650921 11.996032 2.199117 11.996549 1.832252 11.966575 c -1.457623 11.935966 1.113398 11.870285 0.790115 11.705564 c -1.393923 10.520525 l -1.498463 10.573792 1.648176 10.617104 1.940556 10.640992 c -2.240699 10.665515 2.628974 10.666032 3.200000 10.666032 c -3.200000 11.996032 l -h -0.665000 8.131032 m -0.665000 8.702057 0.665517 9.090332 0.690040 9.390476 c -0.713928 9.682856 0.757240 9.832568 0.810506 9.937109 c --0.374532 10.540916 l --0.539253 10.217633 -0.604935 9.873408 -0.635543 9.498780 c --0.665517 9.131915 -0.665000 8.680111 -0.665000 8.131032 c -0.665000 8.131032 l -h -0.790115 11.705564 m -0.288663 11.450062 -0.119030 11.042369 -0.374532 10.540916 c -0.810506 9.937109 l -0.938497 10.188306 1.142726 10.392534 1.393923 10.520525 c -0.790115 11.705564 l -h -f -n -Q - -endstream -endobj - -3 0 obj - 4659 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 16.000000 16.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Pages 5 0 R - /Type /Catalog - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000004749 00000 n -0000004772 00000 n -0000004945 00000 n -0000005019 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -5078 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/PeopleNearbyIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Contact List/PeopleNearbyIcon.imageset/Contents.json deleted file mode 100644 index 77dd2fca4e..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Contact List/PeopleNearbyIcon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_location.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/PeopleNearbyIcon.imageset/ic_location.pdf b/submodules/TelegramUI/Images.xcassets/Contact List/PeopleNearbyIcon.imageset/ic_location.pdf deleted file mode 100644 index 45afa9b5f7..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Contact List/PeopleNearbyIcon.imageset/ic_location.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/CloseSmall.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Instant View/CloseSmall.imageset/Contents.json deleted file mode 100644 index f6826555b5..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Instant View/CloseSmall.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ic_ivclosesmall.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/CloseSmall.imageset/ic_ivclosesmall.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/CloseSmall.imageset/ic_ivclosesmall.pdf deleted file mode 100644 index 04f8b15429..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Instant View/CloseSmall.imageset/ic_ivclosesmall.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpiredStoryPlaceholder.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/Icons/Cake.imageset/Contents.json similarity index 77% rename from submodules/TelegramUI/Images.xcassets/Chat/Message/ExpiredStoryPlaceholder.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Item List/Icons/Cake.imageset/Contents.json index 037cefa85b..2cc650de69 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Message/ExpiredStoryPlaceholder.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Item List/Icons/Cake.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Slice 1.png", + "filename" : "birthday.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Item List/Icons/Cake.imageset/birthday.pdf b/submodules/TelegramUI/Images.xcassets/Item List/Icons/Cake.imageset/birthday.pdf new file mode 100644 index 0000000000..c77d724ac1 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Item List/Icons/Cake.imageset/birthday.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Item List/Icons/Flag.imageset/Contents.json similarity index 78% rename from submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Item List/Icons/Flag.imageset/Contents.json index 3b6cd02302..43174ee790 100644 --- a/submodules/TelegramUI/Images.xcassets/Avatar/SampleAvatar1.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Item List/Icons/Flag.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Avatar8.pdf", + "filename" : "flag.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Item List/Icons/Flag.imageset/flag.pdf b/submodules/TelegramUI/Images.xcassets/Item List/Icons/Flag.imageset/flag.pdf new file mode 100644 index 0000000000..7b9163fe79 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Item List/Icons/Flag.imageset/flag.pdf differ 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 c8bc2dbfa6..c1125541bb 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Item List/Icons/Gradient.imageset/gradient.pdf and b/submodules/TelegramUI/Images.xcassets/Item List/Icons/Gradient.imageset/gradient.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/Contents.json deleted file mode 100644 index 3b8e5cbe3c..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LocationPinIcon@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LocationPinIcon@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@2x.png b/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@2x.png deleted file mode 100644 index ab6fd29ccb..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@3x.png b/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@3x.png deleted file mode 100644 index a57e0d4252..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/Contents.json deleted file mode 100644 index e2fa1abb90..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "videosettingsauto_30.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/videosettingsauto_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/videosettingsauto_30.pdf deleted file mode 100644 index d36b2d7686..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQAuto.imageset/videosettingsauto_30.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json deleted file mode 100644 index 1f7bfc2bb2..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "videosettingshd_30.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/videosettingshd_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/videosettingshd_30.pdf deleted file mode 100644 index 735db423a8..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQHD.imageset/videosettingshd_30.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/Contents.json deleted file mode 100644 index 8f71976b5f..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "videosettingssd_30.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/videosettingssd_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/videosettingssd_30.pdf deleted file mode 100644 index c77db5874e..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Media Gallery/NavigationSettingsQSD.imageset/videosettingssd_30.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Navigation/CreateGroup.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Navigation/CreateGroup.imageset/Contents.json new file mode 100644 index 0000000000..e30ddbbd0e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Navigation/CreateGroup.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "sharenewgroup.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Navigation/CreateGroup.imageset/sharenewgroup.pdf b/submodules/TelegramUI/Images.xcassets/Navigation/CreateGroup.imageset/sharenewgroup.pdf new file mode 100644 index 0000000000..6f8900ccef Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Navigation/CreateGroup.imageset/sharenewgroup.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Navigation/Share.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Navigation/Share.imageset/Contents.json new file mode 100644 index 0000000000..dc5cf9d4a0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Navigation/Share.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "shareshare.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Navigation/Share.imageset/shareshare.pdf b/submodules/TelegramUI/Images.xcassets/Navigation/Share.imageset/shareshare.pdf new file mode 100644 index 0000000000..81a1664bf5 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Navigation/Share.imageset/shareshare.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Open In/Maps.imageset/200x200bb-75.jpg b/submodules/TelegramUI/Images.xcassets/Open In/Maps.imageset/200x200bb-75.jpg new file mode 100644 index 0000000000..0249b126a8 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Open In/Maps.imageset/200x200bb-75.jpg differ diff --git a/submodules/TelegramUI/Images.xcassets/Open In/Maps.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Open In/Maps.imageset/Contents.json index d46b1751fe..0fcdd8c48e 100644 --- a/submodules/TelegramUI/Images.xcassets/Open In/Maps.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Open In/Maps.imageset/Contents.json @@ -6,17 +6,16 @@ }, { "idiom" : "universal", - "filename" : "Maps@2x.png", "scale" : "2x" }, { + "filename" : "200x200bb-75.jpg", "idiom" : "universal", - "filename" : "Maps@3x.png", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/submodules/TelegramUI/Images.xcassets/Open In/Maps.imageset/Maps@2x.png b/submodules/TelegramUI/Images.xcassets/Open In/Maps.imageset/Maps@2x.png deleted file mode 100644 index bfccc77337..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Open In/Maps.imageset/Maps@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Open In/Maps.imageset/Maps@3x.png b/submodules/TelegramUI/Images.xcassets/Open In/Maps.imageset/Maps@3x.png deleted file mode 100644 index 29e2387b08..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Open In/Maps.imageset/Maps@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/Contents.json deleted file mode 100644 index 6a062695f3..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "username.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/username.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/username.pdf deleted file mode 100644 index 548f72b27c..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/username.pdf +++ /dev/null @@ -1,100 +0,0 @@ -%PDF-1.7 - -1 0 obj - << >> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 19.254883 19.005127 cm -0.000000 0.000000 0.000000 scn -0.000000 25.994989 m -0.000000 40.351631 11.638357 51.989990 25.995001 51.989990 c -40.351643 51.989990 51.989998 40.351631 51.989998 25.994989 c -51.989998 19.994989 l -51.989998 15.579475 48.410515 11.999992 43.994999 11.999992 c -40.680130 11.999992 37.836452 14.017384 36.624741 16.891380 c -34.058002 13.897156 30.248056 11.999989 25.995001 11.999989 c -18.265776 11.999989 12.000000 18.265766 12.000000 25.994989 c -12.000000 33.724213 18.265776 39.989990 25.995001 39.989990 c -29.919403 39.989990 33.466534 38.374702 36.007809 35.772713 c -36.097725 36.791267 36.953087 37.589989 37.994999 37.589989 c -39.096806 37.589989 39.989998 36.696800 39.989998 35.594986 c -39.989998 26.005434 l -39.990002 25.994989 l -39.989998 25.984545 l -39.989998 19.994995 l -39.989998 17.783092 41.783100 15.989990 43.994999 15.989990 c -46.206898 15.989990 48.000000 17.783092 48.000000 19.994989 c -48.000000 25.994989 l -48.000000 38.148018 38.148026 47.999989 25.995001 47.999989 c -13.841973 47.999989 3.990000 38.148018 3.990000 25.994989 c -3.990000 25.793646 3.992699 25.592970 3.998061 25.392994 c -4.317001 13.518284 14.043282 3.989990 25.995001 3.989990 c -29.449270 3.989990 32.712318 4.784481 35.616467 6.198845 c -36.607044 6.681271 37.801151 6.269333 38.283577 5.278755 c -38.766006 4.288174 38.354069 3.094070 37.363487 2.611641 c -33.926983 0.938011 30.067709 -0.000008 25.995001 -0.000008 c -11.638357 -0.000008 0.000000 11.638348 0.000000 25.994989 c -h -25.995001 35.999992 m -31.520609 35.999992 35.999996 31.520597 35.999996 25.994989 c -35.999996 20.469381 31.520609 15.989994 25.995001 15.989994 c -20.469393 15.989994 15.990000 20.469381 15.990000 25.994989 c -15.990000 31.520597 20.469393 35.999992 25.995001 35.999992 c -h -f* -n -Q - -endstream -endobj - -3 0 obj - 1836 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 90.000000 90.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Pages 5 0 R - /Type /Catalog - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000001926 00000 n -0000001949 00000 n -0000002122 00000 n -0000002196 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -2255 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/Contents.json deleted file mode 100644 index dc0972e257..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "replacedboost_30.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/replacedboost_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/replacedboost_30.pdf deleted file mode 100644 index d6cbcced6a..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Premium/BoostReplaceIcon.imageset/replacedboost_30.pdf +++ /dev/null @@ -1,273 +0,0 @@ -%PDF-1.7 - -1 0 obj - << >> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 4.877197 15.157974 cm -0.000000 0.000000 0.000000 scn -5.552186 8.359201 m -5.243867 8.359201 5.008486 8.634665 5.056572 8.939211 c -5.727019 13.185373 l -5.810456 13.713807 5.119398 13.988482 4.817303 13.546960 c -0.088512 6.635651 l --0.139333 6.302646 0.099122 5.850563 0.502614 5.850563 c -2.581887 5.850563 l -2.890206 5.850563 3.125587 5.575099 3.077501 5.270554 c -2.407054 1.024391 l -2.323617 0.495958 3.014675 0.221282 3.316770 0.662805 c -8.045561 7.574114 l -8.273406 7.907119 8.034951 8.359201 7.631459 8.359201 c -5.552186 8.359201 l -h -f* -n -Q -q -1.000000 0.000000 -0.000000 1.000000 16.877197 1.157974 cm -0.000000 0.000000 0.000000 scn -5.552186 8.359201 m -5.243867 8.359201 5.008486 8.634665 5.056572 8.939211 c -5.727019 13.185373 l -5.810456 13.713807 5.119398 13.988482 4.817303 13.546960 c -0.088512 6.635651 l --0.139333 6.302646 0.099122 5.850563 0.502614 5.850563 c -2.581887 5.850563 l -2.890206 5.850563 3.125587 5.575099 3.077501 5.270554 c -2.407054 1.024391 l -2.323617 0.495958 3.014675 0.221282 3.316770 0.662805 c -8.045561 7.574114 l -8.273406 7.907119 8.034951 8.359201 7.631459 8.359201 c -5.552186 8.359201 l -h -f* -n -Q -q -1.000000 0.000000 -0.000000 1.000000 20.000000 15.177917 cm -0.000000 0.000000 0.000000 scn -6.586899 4.235184 m -6.911034 4.559319 6.911034 5.084846 6.586899 5.408981 c -6.262764 5.733116 5.737236 5.733116 5.413101 5.408981 c -6.586899 4.235184 l -h -3.000000 1.822083 m -2.413101 1.235184 l -2.737236 0.911049 3.262764 0.911049 3.586899 1.235184 c -3.000000 1.822083 l -h -0.586899 5.408981 m -0.262763 5.733116 -0.262763 5.733116 -0.586899 5.408981 c --0.911034 5.084846 -0.911034 4.559319 -0.586899 4.235184 c -0.586899 5.408981 l -h -5.413101 5.408981 m -2.413101 2.408981 l -3.586899 1.235184 l -6.586899 4.235184 l -5.413101 5.408981 l -h -3.586899 2.408981 m -0.586899 5.408981 l --0.586899 4.235184 l -2.413101 1.235184 l -3.586899 2.408981 l -h -f -n -Q -q -1.000000 0.000000 -0.000000 1.000000 16.000000 15.339355 cm -0.000000 0.000000 0.000000 scn -0.000000 10.490644 m --0.458396 10.490644 -0.830000 10.119040 -0.830000 9.660645 c --0.830000 9.202249 -0.458396 8.830645 0.000000 8.830645 c -0.000000 10.490644 l -h -6.170000 1.660645 m -6.170000 1.202249 6.541604 0.830645 7.000000 0.830645 c -7.458396 0.830645 7.830000 1.202249 7.830000 1.660645 c -6.170000 1.660645 l -h -6.727516 8.295621 m -7.467052 8.672433 l -6.727516 8.295621 l -h -0.000000 8.830645 m -3.000000 8.830645 l -3.000000 10.490644 l -0.000000 10.490644 l -0.000000 8.830645 l -h -6.170000 5.660645 m -6.170000 1.660645 l -7.830000 1.660645 l -7.830000 5.660645 l -6.170000 5.660645 l -h -3.000000 8.830645 m -3.713761 8.830645 4.199165 8.829999 4.574407 8.799340 c -4.939959 8.769474 5.127283 8.715313 5.258164 8.648625 c -6.011788 10.127696 l -5.607891 10.333492 5.177792 10.415573 4.709584 10.453828 c -4.251065 10.491290 3.686370 10.490644 3.000000 10.490644 c -3.000000 8.830645 l -h -7.830000 5.660645 m -7.830000 6.347014 7.830646 6.911709 7.793183 7.370228 c -7.754929 7.838436 7.672848 8.268535 7.467052 8.672433 c -5.987981 7.918809 l -6.054668 7.787928 6.108829 7.600603 6.138696 7.235051 c -6.169354 6.859809 6.170000 6.374406 6.170000 5.660645 c -7.830000 5.660645 l -h -5.258164 8.648625 m -5.572395 8.488517 5.827872 8.233040 5.987981 7.918809 c -7.467052 8.672433 l -7.147793 9.299013 6.638368 9.808438 6.011788 10.127696 c -5.258164 8.648625 l -h -f -n -Q -q --1.000000 -0.000000 -0.000000 -1.000000 10.000000 14.822083 cm -0.000000 0.000000 0.000000 scn -6.586899 4.235184 m -6.911034 4.559319 6.911034 5.084846 6.586899 5.408981 c -6.262764 5.733116 5.737236 5.733116 5.413101 5.408981 c -6.586899 4.235184 l -h -3.000000 1.822083 m -2.413101 1.235184 l -2.737236 0.911049 3.262764 0.911049 3.586899 1.235184 c -3.000000 1.822083 l -h -0.586899 5.408981 m -0.262763 5.733116 -0.262763 5.733116 -0.586899 5.408981 c --0.911034 5.084846 -0.911034 4.559319 -0.586899 4.235184 c -0.586899 5.408981 l -h -5.413101 5.408981 m -2.413101 2.408981 l -3.586899 1.235184 l -6.586899 4.235184 l -5.413101 5.408981 l -h -3.586899 2.408981 m -0.586899 5.408981 l --0.586899 4.235184 l -2.413101 1.235184 l -3.586899 2.408981 l -h -f -n -Q -q --1.000000 -0.000000 -0.000000 -1.000000 14.000000 13.660645 cm -0.000000 0.000000 0.000000 scn -0.000000 9.490644 m --0.458396 9.490644 -0.830000 9.119040 -0.830000 8.660645 c --0.830000 8.202249 -0.458396 7.830645 0.000000 7.830645 c -0.000000 9.490644 l -h -6.170000 1.660645 m -6.170000 1.202248 6.541604 0.830645 7.000000 0.830645 c -7.458396 0.830645 7.830000 1.202248 7.830000 1.660645 c -6.170000 1.660645 l -h -6.727516 7.295621 m -7.467052 7.672433 l -6.727516 7.295621 l -h -0.000000 7.830645 m -3.000000 7.830645 l -3.000000 9.490644 l -0.000000 9.490644 l -0.000000 7.830645 l -h -6.170000 4.660645 m -6.170000 1.660645 l -7.830000 1.660645 l -7.830000 4.660645 l -6.170000 4.660645 l -h -3.000000 7.830645 m -3.713761 7.830645 4.199165 7.829999 4.574407 7.799341 c -4.939959 7.769474 5.127283 7.715313 5.258164 7.648625 c -6.011788 9.127696 l -5.607891 9.333492 5.177792 9.415573 4.709584 9.453828 c -4.251065 9.491290 3.686370 9.490644 3.000000 9.490644 c -3.000000 7.830645 l -h -7.830000 4.660645 m -7.830000 5.347014 7.830646 5.911709 7.793183 6.370228 c -7.754929 6.838436 7.672848 7.268535 7.467052 7.672433 c -5.987981 6.918809 l -6.054668 6.787928 6.108829 6.600603 6.138696 6.235051 c -6.169354 5.859809 6.170000 5.374406 6.170000 4.660645 c -7.830000 4.660645 l -h -5.258164 7.648625 m -5.572395 7.488517 5.827872 7.233039 5.987981 6.918809 c -7.467052 7.672433 l -7.147793 8.299013 6.638368 8.808438 6.011788 9.127696 c -5.258164 7.648625 l -h -f -n -Q - -endstream -endobj - -3 0 obj - 5530 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Pages 5 0 R - /Type /Catalog - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000005620 00000 n -0000005643 00000 n -0000005816 00000 n -0000005890 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -5949 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/ForegroundIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/ForegroundIcon.imageset/Contents.json deleted file mode 100644 index c60ea81e07..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Premium/ForegroundIcon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "addreactions2_32.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/ForegroundIcon.imageset/addreactions2_32.pdf b/submodules/TelegramUI/Images.xcassets/Premium/ForegroundIcon.imageset/addreactions2_32.pdf deleted file mode 100644 index a10889d1e3..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Premium/ForegroundIcon.imageset/addreactions2_32.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/Contents.json deleted file mode 100644 index 27e29557d3..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Premium/GiftCrash.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "giftcrash.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/PrivacyReadTime.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/PrivacyReadTime.imageset/Contents.json deleted file mode 100644 index 5898847ff8..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Premium/PrivacyReadTime.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "ReadTime.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/PrivacyReadTime.imageset/ReadTime.pdf b/submodules/TelegramUI/Images.xcassets/Premium/PrivacyReadTime.imageset/ReadTime.pdf deleted file mode 100644 index 25f40eeac6..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Premium/PrivacyReadTime.imageset/ReadTime.pdf +++ /dev/null @@ -1,277 +0,0 @@ -%PDF-1.7 - -1 0 obj - << /Type /XObject - /Length 2 0 R - /Group << /Type /Group - /S /Transparency - >> - /Subtype /Form - /Resources << >> - /BBox [ 0.000000 0.000000 90.000000 90.000000 ] - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -0.707107 0.707107 -0.707107 0.707107 67.034660 -18.373894 cm -0.000000 0.000000 0.000000 scn -10.453167 41.350018 m -9.512874 42.238548 8.030319 42.196587 7.141789 41.256294 c -6.253258 40.316002 6.295220 38.833447 7.235513 37.944916 c -10.453167 41.350018 l -h -0.095610 63.883827 m --2.239701 64.066391 l -0.095610 63.883827 l -h --2.342436 54.749527 m --2.342436 53.455833 -1.293692 52.407089 0.000000 52.407089 c -1.293692 52.407089 2.342436 53.455833 2.342436 54.749527 c --2.342436 54.749527 l -h -7.235513 37.944916 m -13.076076 32.425873 20.978649 29.037750 29.669981 29.037750 c -29.669981 33.722622 l -22.218493 33.722622 15.455904 36.622681 10.453167 41.350018 c -7.235513 37.944916 l -h -29.669981 29.037750 m -47.645660 29.037750 62.242207 43.525253 62.242207 61.426483 c -57.557335 61.426483 l -57.557335 46.139488 45.085209 33.722622 29.669981 33.722622 c -29.669981 29.037750 l -h -62.242207 61.426483 m -62.242207 79.327713 47.645660 93.815216 29.669981 93.815216 c -29.669981 89.130341 l -45.085209 89.130341 57.557335 76.713478 57.557335 61.426483 c -62.242207 61.426483 l -h -29.669981 93.815216 m -12.540254 93.815216 -0.940433 80.686134 -2.239701 64.066391 c -2.430921 63.701260 l -3.546472 77.970970 15.073609 89.130341 29.669981 89.130341 c -29.669981 93.815216 l -h --2.239701 64.066391 m --2.312374 63.136772 -2.342436 55.500645 -2.342436 54.749527 c -2.342436 54.749527 l -2.342436 55.154526 2.350449 57.228703 2.366412 59.305004 c -2.374389 60.342529 2.384313 61.375381 2.396135 62.197342 c -2.402054 62.608826 2.408379 62.962509 2.415047 63.235344 c -2.418387 63.371964 2.421699 63.483559 2.424903 63.569450 c -2.426500 63.612274 2.427957 63.645615 2.429212 63.670444 c -2.430558 63.697090 2.431254 63.705528 2.430921 63.701260 c --2.239701 64.066391 l -h -f -n -Q -q -0.707107 0.707107 -0.707107 0.707107 35.092972 1.912642 cm -0.000000 0.000000 0.000000 scn -9.400195 9.052296 m -16.491901 18.012711 l -17.065374 18.737297 16.942873 19.789585 16.218287 20.363058 c -15.921938 20.597603 15.554913 20.724905 15.176980 20.724241 c -1.113485 20.699522 l -0.497442 20.698439 -0.001081 20.198158 0.000002 19.582117 c -0.000426 19.340685 0.079173 19.105904 0.224414 18.913044 c -7.634510 9.073509 l -8.005109 8.581406 8.704469 8.482906 9.196571 8.853506 c -9.272655 8.910804 9.341086 8.977612 9.400195 9.052296 c -h -f* -n -Q -q -1.000000 0.000000 -0.000000 1.000000 37.394165 30.785156 cm -0.000000 0.000000 0.000000 scn -25.237368 22.854145 m -26.030727 23.803909 25.903934 25.216991 24.954170 26.010351 c -24.004406 26.803711 22.591324 26.676918 21.797964 25.727154 c -25.237368 22.854145 l -h -7.466430 5.075024 m -9.186129 3.638515 l -9.186131 3.638519 l -7.466430 5.075024 l -h -7.322968 5.062153 m -5.886413 3.342493 l -5.886472 3.342443 l -7.322968 5.062153 l -h -7.306202 5.079990 m -5.500865 3.752708 l -5.500870 3.752703 l -7.306202 5.079990 l -h -1.805337 16.344992 m -1.072299 17.342052 -0.330222 17.556084 -1.327282 16.823048 c --2.324342 16.090010 -2.538375 14.687489 -1.805337 13.690428 c -1.805337 16.344992 l -h -21.797964 25.727154 m -5.746728 6.511530 l -9.186131 3.638519 l -25.237368 22.854145 l -21.797964 25.727154 l -h -5.746732 6.511532 m -6.504047 7.418142 7.852895 7.539131 8.759465 6.781860 c -5.886472 3.342443 l -6.879384 2.513050 8.356690 2.645563 9.186129 3.638515 c -5.746732 6.511532 l -h -8.759524 6.781811 m -8.891400 6.671646 9.009609 6.545914 9.111534 6.407280 c -5.500870 3.752703 l -5.612496 3.600872 5.741967 3.463160 5.886413 3.342493 c -8.759524 6.781811 l -h -9.111539 6.407272 m -1.805337 16.344992 l --1.805337 13.690428 l -5.500865 3.752708 l -9.111539 6.407272 l -h -f -n -Q -q -1.000000 0.000000 -0.000000 1.000000 29.174744 29.887207 cm -0.000000 0.000000 0.000000 scn -5.315279 3.728403 m -6.056771 2.737616 7.461063 2.535522 8.451851 3.277015 c -9.442638 4.018508 9.644732 5.422799 8.903239 6.413588 c -5.315279 3.728403 l -h -1.793980 15.913027 m -1.052487 16.903814 -0.351804 17.105907 -1.342592 16.364414 c --2.333380 15.622922 -2.535474 14.218631 -1.793980 13.227842 c -1.793980 15.913027 l -h -8.903239 6.413588 m -1.793980 15.913027 l --1.793980 13.227842 l -5.315279 3.728403 l -8.903239 6.413588 l -h -f -n -Q - -endstream -endobj - -2 0 obj - 4078 -endobj - -3 0 obj - << /Type /XObject - /Length 4 0 R - /Group << /Type /Group - /S /Transparency - >> - /Subtype /Form - /Resources << >> - /BBox [ 0.000000 0.000000 90.000000 90.000000 ] - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 5.000000 5.000000 cm -0.000000 0.000000 0.000000 scn -0.000000 80.000000 m -80.000000 80.000000 l -80.000000 0.000000 l -0.000000 0.000000 l -0.000000 80.000000 l -h -f -n -Q - -endstream -endobj - -4 0 obj - 232 -endobj - -5 0 obj - << /XObject << /X1 1 0 R >> - /ExtGState << /E1 << /SMask << /Type /Mask - /G 3 0 R - /S /Alpha - >> - /Type /ExtGState - >> >> - >> -endobj - -6 0 obj - << /Length 7 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -/E1 gs -/X1 Do -Q - -endstream -endobj - -7 0 obj - 46 -endobj - -8 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 90.000000 90.000000 ] - /Resources 5 0 R - /Contents 6 0 R - /Parent 9 0 R - >> -endobj - -9 0 obj - << /Kids [ 8 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -10 0 obj - << /Pages 9 0 R - /Type /Catalog - >> -endobj - -xref -0 11 -0000000000 65535 f -0000000010 00000 n -0000004336 00000 n -0000004359 00000 n -0000004839 00000 n -0000004861 00000 n -0000005159 00000 n -0000005261 00000 n -0000005282 00000 n -0000005455 00000 n -0000005529 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 10 0 R - /Size 11 ->> -startxref -5589 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/Contents.json deleted file mode 100644 index 2c2f33f0da..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "clearpasscode@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "clearpasscode@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/clearpasscode@2x.png b/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/clearpasscode@2x.png deleted file mode 100644 index 4fd57e8830..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/clearpasscode@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/clearpasscode@3x.png b/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/clearpasscode@3x.png deleted file mode 100644 index fc6549cc0c..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Settings/PasscodeClearIcon.imageset/clearpasscode@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/Contents.json deleted file mode 100644 index 3dd535cbed..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "PasswordPlaceholderIcon@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "PasswordPlaceholderIcon@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/PasswordPlaceholderIcon@2x.png b/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/PasswordPlaceholderIcon@2x.png deleted file mode 100644 index c9cd47a84c..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/PasswordPlaceholderIcon@2x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/PasswordPlaceholderIcon@3x.png b/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/PasswordPlaceholderIcon@3x.png deleted file mode 100644 index 8ce614090f..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Settings/PasswordIntroIcon.imageset/PasswordPlaceholderIcon@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/Contents.json deleted file mode 100644 index 6e965652df..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "provides-namespace" : true - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconBackgroundTime.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconBackgroundTime.imageset/Contents.json deleted file mode 100644 index f42e8f2075..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconBackgroundTime.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "PowerIconBackgroundTime.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconBackgroundTime.imageset/PowerIconBackgroundTime.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconBackgroundTime.imageset/PowerIconBackgroundTime.svg deleted file mode 100644 index d4e4739bc1..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconBackgroundTime.imageset/PowerIconBackgroundTime.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEffects.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEffects.imageset/Contents.json deleted file mode 100644 index 82b6ec3e70..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEffects.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "PowerIconEffects.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEffects.imageset/PowerIconEffects.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEffects.imageset/PowerIconEffects.svg deleted file mode 100644 index 417e451cca..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEffects.imageset/PowerIconEffects.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEmoji.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEmoji.imageset/Contents.json deleted file mode 100644 index fd88972363..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEmoji.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "PowerIconEmoji.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEmoji.imageset/PowerIconEmoji.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEmoji.imageset/PowerIconEmoji.svg deleted file mode 100644 index c8cc8922a1..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconEmoji.imageset/PowerIconEmoji.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconGif.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconGif.imageset/Contents.json deleted file mode 100644 index c3d709d9e8..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconGif.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "PowerIconGif.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconGif.imageset/PowerIconGif.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconGif.imageset/PowerIconGif.svg deleted file mode 100644 index adaed14d56..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconGif.imageset/PowerIconGif.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconMedia.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconMedia.imageset/Contents.json deleted file mode 100644 index 6e83cc8223..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconMedia.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "PowerIconMedia.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconMedia.imageset/PowerIconMedia.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconMedia.imageset/PowerIconMedia.svg deleted file mode 100644 index 4189aec595..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconMedia.imageset/PowerIconMedia.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconStickers.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconStickers.imageset/Contents.json deleted file mode 100644 index 8e14ecc776..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconStickers.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "PowerIconStickers.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconStickers.imageset/PowerIconStickers.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconStickers.imageset/PowerIconStickers.svg deleted file mode 100644 index d825813fc7..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconStickers.imageset/PowerIconStickers.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconVideo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconVideo.imageset/Contents.json deleted file mode 100644 index d47d2a6972..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconVideo.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "PowerIconVideo.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconVideo.imageset/PowerIconVideo.svg b/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconVideo.imageset/PowerIconVideo.svg deleted file mode 100644 index 0a3bb1baba..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Settings/Power/PowerIconVideo.imageset/PowerIconVideo.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeUnmark.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/Contents.json similarity index 77% rename from submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeUnmark.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/Contents.json index b48dcd4e15..9d000d735b 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/AgeUnmark.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "18off_24.pdf", + "filename" : "setemoji.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/setemoji.pdf b/submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/setemoji.pdf new file mode 100644 index 0000000000..92143fac3b Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/TextProcessing/EditEmojiPlaceholder.imageset/setemoji.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/Contents.json new file mode 100644 index 0000000000..d313636cad --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "addstyle.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/addstyle.pdf b/submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/addstyle.pdf new file mode 100644 index 0000000000..3565e78849 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/TextProcessing/NewStyle.imageset/addstyle.pdf differ diff --git a/submodules/TelegramUI/Resources/Animations/BotEmoji.tgs b/submodules/TelegramUI/Resources/Animations/BotEmoji.tgs deleted file mode 100644 index 0076344f4c..0000000000 Binary files a/submodules/TelegramUI/Resources/Animations/BotEmoji.tgs and /dev/null differ diff --git a/submodules/TelegramUI/Resources/Animations/ChatAutomation.tgs b/submodules/TelegramUI/Resources/Animations/ChatAutomation.tgs new file mode 100644 index 0000000000..3e386fd63e Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/ChatAutomation.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/NavigationMuteOff.tgs b/submodules/TelegramUI/Resources/Animations/NavigationMuteOff.tgs new file mode 100644 index 0000000000..0fd0c192a5 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/NavigationMuteOff.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/NavigationMuteOn.tgs b/submodules/TelegramUI/Resources/Animations/NavigationMuteOn.tgs new file mode 100644 index 0000000000..7d3d18b9fe Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/NavigationMuteOn.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/anim_hand5.json b/submodules/TelegramUI/Resources/Animations/anim_hand5.json deleted file mode 100644 index e90dc525a4..0000000000 --- a/submodules/TelegramUI/Resources/Animations/anim_hand5.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.7.6","fr":60,"ip":0,"op":240,"w":100,"h":100,"nm":"hand_5 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL CONTROL","parent":6,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":30,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":36,"s":[5.537,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":43,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":52,"s":[5.537,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":68,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":87,"s":[5.537,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.13,"y":1},"o":{"x":0.174,"y":0},"t":115,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.34,"y":1},"o":{"x":0.54,"y":0},"t":157,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.54,"y":0},"t":168,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":172,"s":[1.942,-42.622,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":178,"s":[13.542,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":184,"s":[9.142,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0},"t":190,"s":[9.542,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"t":199,"s":[6.742,-39.422,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"head","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.781],"y":[1]},"o":{"x":[0.412],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":11,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":30,"s":[21]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":54,"s":[21]},{"i":{"x":[0.13],"y":[1]},"o":{"x":[0.811],"y":[0]},"t":71,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.54],"y":[0]},"t":157,"s":[0.516]},{"i":{"x":[0.34],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":162,"s":[61.167]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.54],"y":[0]},"t":169,"s":[0.516]},{"i":{"x":[0.637],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":177,"s":[-21.833]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":183,"s":[0]},{"t":218,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.396,"y":1},"o":{"x":0.683,"y":0},"t":1,"s":[81.289,125.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":11,"s":[81.289,176.385,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":30,"s":[115.289,114.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.487,"y":0},"t":54,"s":[115.289,148.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":71,"s":[115.289,141.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":97,"s":[133.289,230.475,0],"to":[0,0,0],"ti":[0.667,1.167,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.167,"y":0.167},"t":110,"s":[129.289,223.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.13,"y":1},"o":{"x":0.181,"y":0},"t":117,"s":[129.289,223.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.34,"y":1},"o":{"x":0.54,"y":0},"t":157,"s":[145.289,244.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.54,"y":0},"t":169,"s":[153.289,246.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":183,"s":[70.289,86.475,0],"to":[0,0,0],"ti":[0,0,0]},{"t":218,"s":[81.289,125.475,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":1,"s":[500,500,100]},{"i":{"x":[0.425,0.425,0.425],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":11,"s":[500,500,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":16,"s":[520,484,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":30,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":54,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.7,0.7,0.7],"y":[0,0,0]},"t":71,"s":[500,500,100]},{"i":{"x":[0.13,0.13,0.13],"y":[1,1,1]},"o":{"x":[0.176,0.176,0.176],"y":[0,0,0]},"t":97,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.54,0.54,0.54],"y":[0,0,0]},"t":157,"s":[500,500,100]},{"i":{"x":[0.34,0.34,0.34],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":162,"s":[477,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.54,0.54,0.54],"y":[0,0,0]},"t":169,"s":[500,500,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":175,"s":[469,533,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":183,"s":[485,515,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":198,"s":[527,484,100]},{"t":218,"s":[500,500,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":0,"s":[{"i":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],"o":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],"v":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],"c":true}]},{"i":{"x":0.13,"y":1},"o":{"x":0.167,"y":0},"t":117,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]},{"i":{"x":0.34,"y":1},"o":{"x":0.54,"y":0},"t":157,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]},{"i":{"x":0.2,"y":1},"o":{"x":0.54,"y":0},"t":169,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":183,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]},{"t":218,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"hands","parent":6,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":28,"s":[8]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.487],"y":[0]},"t":49,"s":[8]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":66,"s":[8]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":95,"s":[15]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":172,"s":[15]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":183,"s":[0]},{"t":215,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.538,"y":1},"o":{"x":0.525,"y":0},"t":0,"s":[-2.341,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[-2.341,-12.256,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":0.3},"o":{"x":0.167,"y":0.167},"t":28,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.487,"y":0},"t":49,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.5,"y":0},"t":66,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.5,"y":0.5},"t":95,"s":[1.459,-10.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.6,"y":0},"t":172,"s":[1.459,-10.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":183,"s":[-2.341,-24.177,0],"to":[0,0,0],"ti":[0,0,0]},{"t":215,"s":[-2.341,-17.777,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"hands 3","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[0,0],[-1.37,-5.996],[-12.799,0.954],[-5.623,-0.366]],"o":[[-2.582,5.43],[1.226,5.005],[4.037,-0.259],[1.184,0.071]],"v":[[-21.167,-23.136],[-27.242,-4.081],[-7.372,3.34],[9.979,3.037]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":24,"s":[{"i":[[0,0],[-0.633,-3.903],[-5.394,-1.449],[-8.926,0.862]],"o":[[1.012,6.936],[0.998,6.158],[5.589,1.532],[1.18,-0.114]],"v":[[-18.065,-33.976],[-19.359,-12.251],[-7.484,2.834],[8.267,2.627]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":30,"s":[{"i":[[0,0],[-1.834,-3.522],[-4.71,-2.732],[-8.926,0.862]],"o":[[4.406,7.624],[2.894,5.558],[5.028,2.917],[1.18,-0.114]],"v":[[-29.396,-35.058],[-19.39,-14.698],[-8.387,2.178],[8.267,2.627]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":37,"s":[{"i":[[0,0],[-0.633,-3.903],[-5.394,-1.449],[-8.926,0.862]],"o":[[1.012,6.936],[0.998,6.158],[5.589,1.532],[1.18,-0.114]],"v":[[-18.065,-33.976],[-19.359,-12.251],[-7.484,2.834],[8.267,2.627]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":46,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.984,-1.753],[-8.926,0.862]],"o":[[1.597,6.857],[1.517,6.08],[5.484,1.929],[1.18,-0.114]],"v":[[-23.166,-35.108],[-20.844,-12.632],[-7.906,2.713],[8.267,2.627]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":62,"s":[{"i":[[0,0],[-0.633,-3.903],[-5.394,-1.449],[-8.926,0.862]],"o":[[1.012,6.936],[0.998,6.158],[5.589,1.532],[1.18,-0.114]],"v":[[-18.065,-33.976],[-19.359,-12.251],[-7.484,2.834],[8.371,3.018]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":83,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.906,-1.962],[-7.467,0.538]],"o":[[1.597,6.857],[1.517,6.08],[4.479,1.791],[1.183,-0.085]],"v":[[-23.166,-35.108],[-20.844,-12.632],[-7.906,2.713],[8.484,3.913]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":110,"s":[{"i":[[0,0],[-0.26,-3.59],[-3.384,-0.79],[-7.192,0.386]],"o":[[0.455,6.368],[0.41,5.665],[5.588,1.254],[1.184,-0.062]],"v":[[-19.285,-31.287],[-20.744,-10.988],[-7.332,2.936],[7.831,4.696]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":159,"s":[{"i":[[0,0],[-0.365,-3.581],[-5.898,-2.25],[-7.859,-0.922]],"o":[[0.643,6.352],[0.576,5.651],[5.898,2.25],[1.177,0.138]],"v":[[-19.77,-28.6],[-20.63,-8.268],[-7.332,2.936],[8.229,6.66]],"c":false}]},{"i":{"x":0.33,"y":1},"o":{"x":0.167,"y":0},"t":170,"s":[{"i":[[0,0],[-0.365,-3.581],[-5.898,-2.25],[-7.859,-0.922]],"o":[[0.643,6.352],[0.576,5.651],[5.898,2.25],[1.177,0.138]],"v":[[-17.58,-28.152],[-18.943,-8.927],[-7.087,3.077],[8.229,6.66]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":172,"s":[{"i":[[0,0],[-0.365,-3.581],[-5.898,-2.25],[-7.859,-0.922]],"o":[[0.643,6.352],[0.576,5.651],[5.898,2.25],[1.177,0.138]],"v":[[-17.58,-28.152],[-18.943,-8.927],[-7.087,3.077],[8.229,6.66]],"c":false}]},{"i":{"x":0.33,"y":1},"o":{"x":0.167,"y":0.167},"t":177,"s":[{"i":[[0,0],[-0.54,-4.745],[-7.23,-2.167],[-5.398,-0.759]],"o":[[-4.605,4.425],[0.657,5.889],[5.467,1.861],[1.174,0.151]],"v":[[-13.923,-29.232],[-18.91,-12.686],[-7.227,2.305],[7.477,5.457]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":183,"s":[{"i":[[0,0],[-0.757,-6.182],[-8.875,-2.065],[-4.049,-0.033]],"o":[[1.716,5.76],[0.757,6.182],[5.804,0.802],[1.183,0.01]],"v":[[-20.544,-37.522],[-16.784,-18.145],[-6.731,2.836],[6.768,3.871]],"c":false}]},{"i":{"x":0.22,"y":1},"o":{"x":0.167,"y":0.167},"t":197,"s":[{"i":[[0,0],[-1.231,-6.629],[-10.082,-1.99],[-3.033,-0.134]],"o":[[-2.133,4.972],[1.242,6.689],[4.542,1.029],[1.183,0.052]],"v":[[-21.494,-33.181],[-22.796,-14.58],[-6.487,2.795],[6.539,3.624]],"c":false}]},{"t":230,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"hands 2","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[-2.886,-0.073],[-2.347,-0.053],[-2.059,-3.86],[-0.222,-4.947]],"o":[[2.847,0.072],[11.634,0.272],[2.05,3.678],[0,0]],"v":[[2.805,2.87],[11.318,3.002],[28.486,9.829],[31.187,24.214]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":29,"s":[{"i":[[-2.805,0.395],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.23,-0.595],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.696,3.339],[12.911,2.962],[23.56,19.096],[31.538,32.046]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.4,"y":0},"t":49,"s":[{"i":[[-2.805,0.396],[-2.168,-0.779],[-1.2,-3.73],[-2.259,-5.223]],"o":[[3.592,-0.507],[6.085,2.162],[1.511,4.741],[0,0]],"v":[[2.591,3.352],[12.843,2.769],[22.316,18.867],[27.587,32.67]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":56,"s":[{"i":[[-2.809,0.363],[-2.164,-0.79],[-1.265,-3.731],[-2.492,-5.262]],"o":[[3.71,-0.472],[5.954,2.15],[1.585,4.718],[0,0]],"v":[[2.587,3.473],[12.901,3.159],[22.53,18.907],[28.267,32.563]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":71,"s":[{"i":[[-2.826,0.202],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.274,-0.305],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.755,3.732],[13.24,3.821],[23.56,19.096],[31.538,32.046]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":77,"s":[{"i":[[-3.178,0.215],[-2.041,-0.845],[-2.499,-1.941],[-4.429,-3.642]],"o":[[4.522,-0.369],[5.066,2.097],[2.711,2.446],[0,0]],"v":[[2.574,4.136],[12.86,4.148],[22.716,18.561],[31.303,23.734]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":83,"s":[{"i":[[-3.695,-0.034],[-1.897,-0.845],[-3.783,0.554],[-5.561,-1.128]],"o":[[4.929,-0.055],[4.71,2.097],[3.783,-0.554],[0,0]],"v":[[2.262,4.258],[12.224,3.929],[21.543,17.816],[30.975,12.182]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":89,"s":[{"i":[[-3.987,-0.175],[-1.816,-0.845],[-2.558,0.538],[-4.267,2.804]],"o":[[4.392,0.151],[4.508,2.097],[4.029,-0.892],[0,0]],"v":[[0.773,4.323],[12.105,4.679],[20.881,17.396],[23.392,2.697]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":97,"s":[{"i":[[-4.068,-0.214],[-1.793,-0.845],[-2.221,0.534],[-3.91,3.887]],"o":[[3.583,0.188],[4.452,2.097],[4.097,-0.985],[0,0]],"v":[[1.673,4.518],[12.072,4.886],[20.698,17.28],[21.304,0.085]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":110,"s":[{"i":[[-4.068,-0.214],[-1.793,-0.845],[-2.221,0.534],[-1.866,5.72]],"o":[[3.583,0.188],[4.452,2.097],[4.097,-0.985],[0,0]],"v":[[1.647,4.421],[11.286,4.268],[20.698,17.28],[20.124,-0.841]],"c":false}]},{"i":{"x":0.13,"y":1},"o":{"x":0.167,"y":0},"t":117,"s":[{"i":[[-4.068,-0.214],[-1.793,-0.845],[-2.221,0.534],[-1.866,5.72]],"o":[[3.583,0.188],[4.452,2.097],[4.097,-0.985],[0,0]],"v":[[1.647,4.421],[11.286,4.268],[20.698,17.28],[20.124,-0.841]],"c":false}]},{"i":{"x":0.47,"y":1},"o":{"x":0.54,"y":0},"t":159,"s":[{"i":[[-3.882,-1.235],[-1.793,-0.845],[-2.236,0.469],[-6.732,3.4]],"o":[[6.962,2.214],[4.452,2.097],[6.913,-1.449],[0,0]],"v":[[-2.326,4.761],[11.274,7.998],[20.698,17.28],[22.603,3.774]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.53,"y":0},"t":170,"s":[{"i":[[-3.882,-1.235],[-1.793,-0.845],[-3.475,0.534],[-2.888,6.054]],"o":[[6.962,2.214],[4.452,2.097],[6.411,-0.985],[0,0]],"v":[[-2.326,4.761],[12.099,7.984],[24.802,16.49],[25.028,3.271]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[{"i":[[-3.711,-1.045],[-1.877,-0.707],[-3.424,-0.312],[-3.136,3.726]],"o":[[6.283,1.865],[4.802,1.755],[5.881,-0.065],[0,0]],"v":[[-1.494,4.593],[11.879,7.297],[24.69,16.195],[35.961,7.352]],"c":false}]},{"i":{"x":0.33,"y":1},"o":{"x":0.167,"y":0.167},"t":177,"s":[{"i":[[-3.396,-0.694],[-2.033,-0.452],[-3.328,-1.877],[-3.596,-0.579]],"o":[[5.027,1.218],[5.448,1.122],[4.899,1.636],[0,0]],"v":[[0.045,4.282],[11.473,6.026],[24.482,15.649],[36.718,19.295]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.6,"y":0},"t":186,"s":[{"i":[[-2.838,-0.073],[-2.309,0],[-3.159,-4.652],[-4.411,-8.209]],"o":[[2.799,0.072],[6.593,0],[3.159,4.652],[0,0]],"v":[[2.773,3.73],[10.753,3.773],[24.114,14.681],[34.584,32.542]],"c":false}]},{"t":222,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"body","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.538,"y":1},"o":{"x":0.525,"y":0},"t":0,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[262.842,450.324,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":28,"s":[262.842,403.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":0.3},"o":{"x":0.167,"y":0.167},"t":49,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.167,"y":0.167},"t":66,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.6,"y":0.6},"t":172,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.167,"y":0.167},"t":183,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"t":215,"s":[262.842,433.748,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,14.211,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":3,"s":[515,485,100]},{"i":{"x":[0.566,0.566,0.566],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":10,"s":[489.464,510.536,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":15,"s":[515,485,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":28,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":37,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":49,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":57,"s":[515,485,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":66,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":78,"s":[485,515,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":95,"s":[500,500,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":172,"s":[500,500,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":183,"s":[500,500,100]},{"t":215,"s":[500,500,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0]],"v":[[-12.432,-14.575],[-11.977,7.814],[-8.487,12.849],[0.4,13.948],[9.223,12.916],[12.656,7.499],[12.482,-13.225]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-1.991,-0.83],[-3.105,0],[-2.6,1.063],[-0.091,2.131],[0,0]],"o":[[0,0],[0.148,2.059],[2.638,1.099],[3.053,0],[2.064,-0.844],[0,0],[0,0]],"v":[[-14.565,-8.333],[-13.375,7.863],[-9.896,12.562],[0.353,14.211],[10.467,12.616],[13.992,7.749],[14.426,-7.239]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":24,"s":[{"i":[[0.942,-11.03],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.041,-0.17]],"v":[[-11.292,-16.725],[-11.425,7.863],[-8.369,12.562],[0.392,14.211],[9.036,12.616],[12.131,7.749],[13.652,-10.268]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.942,-11.03],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.041,-0.17]],"v":[[-11.74,-17.039],[-11.432,7.863],[-8.377,12.562],[0.392,14.211],[9.044,12.616],[12.139,7.749],[13.308,-10.843]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":37,"s":[{"i":[[0.942,-11.03],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.041,-0.17]],"v":[[-11.292,-16.725],[-11.425,7.863],[-8.369,12.562],[0.392,14.211],[9.036,12.616],[12.131,7.749],[13.652,-10.268]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":46,"s":[{"i":[[0.942,-11.03],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.041,-0.17]],"v":[[-11.74,-17.039],[-11.432,7.863],[-8.377,12.562],[0.392,14.211],[9.044,12.616],[12.139,7.749],[13.308,-10.843]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":57,"s":[{"i":[[0.942,-11.03],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.041,-0.17]],"v":[[-11.292,-16.725],[-11.425,7.863],[-8.369,12.562],[0.392,14.211],[9.036,12.616],[12.131,7.749],[13.652,-10.268]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.5,"y":0},"t":66,"s":[{"i":[[0.942,-11.03],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.041,-0.17]],"v":[[-11.74,-17.039],[-11.432,7.863],[-8.377,12.562],[0.392,14.211],[9.044,12.616],[12.139,7.749],[13.308,-10.843]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.5,"y":0},"t":95,"s":[{"i":[[7.932,-7.722],[-0.296,-1.817],[-2.373,-1.147],[-2.726,0.008],[-2.179,1.263],[-0.336,1.409],[-1.022,3.53]],"o":[[-7.574,7.373],[0.296,1.817],[2.309,1.116],[2.681,-0.008],[2.218,-1.285],[0.336,-1.409],[0.041,-0.17]],"v":[[-11.795,-13.512],[-17.064,5.317],[-13.796,10.808],[-2.427,12.362],[5.214,10.746],[8.296,5.87],[10.454,-3.469]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.6,"y":0},"t":172,"s":[{"i":[[7.932,-7.722],[-0.296,-1.817],[-2.373,-1.147],[-2.726,0.008],[-2.179,1.263],[-0.336,1.409],[-1.022,3.53]],"o":[[-7.574,7.373],[0.296,1.817],[2.309,1.116],[2.681,-0.008],[2.218,-1.285],[0.336,-1.409],[0.041,-0.17]],"v":[[-11.795,-13.512],[-17.064,5.317],[-13.796,10.808],[-2.427,12.362],[5.214,10.746],[8.296,5.87],[10.454,-3.469]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":183,"s":[{"i":[[0,0],[0,0],[-1.638,-0.83],[-2.555,0],[-2.139,1.063],[-0.075,2.131],[0,0]],"o":[[0,0],[0.122,2.059],[2.171,1.099],[2.512,0],[1.698,-0.844],[0,0],[0,0]],"v":[[-11.432,-21.961],[-10.647,7.863],[-7.784,12.562],[0.304,14.211],[8.281,12.616],[11.182,7.749],[11.432,-19.411]],"c":false}]},{"t":215,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0]],"v":[[-12.432,-14.575],[-11.977,7.814],[-8.487,12.849],[0.4,13.948],[9.223,12.916],[12.656,7.499],[12.482,-13.225]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":244,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"hand_5","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.75,47.75,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[11,11,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":240,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_hand6.json b/submodules/TelegramUI/Resources/Animations/anim_hand6.json deleted file mode 100644 index 1864a8703b..0000000000 --- a/submodules/TelegramUI/Resources/Animations/anim_hand6.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.7.6","fr":60,"ip":0,"op":240,"w":100,"h":100,"nm":"hand_6 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.989,275.127,0],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":243,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"head","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":11,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":23.514,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.17],"y":[0]},"t":159,"s":[21]},{"i":{"x":[0.836],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":169,"s":[-18]},{"i":{"x":[0.14],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":181,"s":[0]},{"t":202,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[122.706,17.15,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":11,"s":[122.706,68.999,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.589,"y":1},"o":{"x":0.275,"y":0},"t":23.514,"s":[150.706,5.999,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.206,"y":0},"t":36.148,"s":[150.706,32.05,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.29,"y":1},"o":{"x":0.295,"y":0},"t":55.52,"s":[150.706,27.67,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.71,"y":0.684},"o":{"x":0.75,"y":0},"t":72,"s":[150.706,31.062,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.205,"y":0.943},"o":{"x":0.097,"y":0.337},"t":100,"s":[158.75,127.141,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.205,"y":0.901},"o":{"x":0.328,"y":0.041},"t":123,"s":[140.564,110.895,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.205,"y":0.205},"o":{"x":0.328,"y":0.328},"t":147,"s":[148.731,122.942,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.71,"y":0},"t":159,"s":[148.731,122.942,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.14,"y":1},"o":{"x":0.167,"y":0},"t":181,"s":[122.706,1.199,0],"to":[0,0,0],"ti":[0,0,0]},{"t":202,"s":[122.706,17.15,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":1,"s":[500,500,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":11,"s":[525,475,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":16,"s":[485,515,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":23.514,"s":[500,500,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":36,"s":[500,500,100]},{"i":{"x":[0.605,0.605,0.605],"y":[1,1,1]},"o":{"x":[0.709,0.709,0.709],"y":[0,0,0]},"t":55.52,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":72,"s":[500,500,100]},{"i":{"x":[0.605,0.605,0.605],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":84,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.195,0.195,0.195],"y":[0,0,0]},"t":100,"s":[531,465,100]},{"i":{"x":[0.666,0.666,0.666],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":111,"s":[508,482,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":123,"s":[531,465,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":147,"s":[531,465,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.71,0.71,0.71],"y":[0,0,0]},"t":159,"s":[531,465,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":169,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":181,"s":[500,500,100]},{"i":{"x":[0.14,0.14,0.14],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":190,"s":[515,485,100]},{"t":202,"s":[500,500,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],"o":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],"v":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"NULL CONTROL","parent":7,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-1.541,-17.777,0],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":3,"nm":"hands","parent":3,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":23.514,"s":[8]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.71],"y":[0]},"t":167,"s":[8]},{"t":187,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[111,115,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[111,155,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":23.514,"s":[115,115,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":32.309,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":48.244,"s":[115,115,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":0.5},"o":{"x":0.167,"y":0.167},"t":73.717,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":0.5},"o":{"x":0.167,"y":0.167},"t":104,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.71,"y":0},"t":167,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"t":187,"s":[111,115,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"hands 3","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.666,-0.138]],"o":[[-3.232,5.731],[0.566,4.637],[3.994,-0.505],[1.186,0.024]],"v":[[-18.219,-21.464],[-27.079,-2.224],[-7.644,3.484],[11.485,2.829]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":23.514,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],"o":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],"v":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":27.281,"s":[{"i":[[0,0],[-0.511,-3.901],[-4.724,-3.504],[-8.291,0.526]],"o":[[2.238,7.884],[0.807,6.156],[4.331,3.14],[1.184,-0.075]],"v":[[-18.139,-37.479],[-16.265,-15.877],[-7.359,2.636],[10.941,2.657]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":32.309,"s":[{"i":[[0,0],[0.125,-3.969],[-5.052,-4.524],[-8.435,1.023]],"o":[[-2.798,7.352],[-0.197,6.263],[4,3.582],[1.177,-0.143]],"v":[[-6.969,-38.346],[-13.919,-17.314],[-7.359,2.636],[9.48,3.009]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":39.988,"s":[{"i":[[0,0],[-0.447,-3.908],[-4.757,-3.608],[-8.213,0.625]],"o":[[-4.119,5.691],[0.705,6.167],[4.297,3.185],[1.182,-0.09]],"v":[[-10.167,-36.731],[-16.027,-16.023],[-7.359,2.636],[11.49,2.929]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":48,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],"o":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],"v":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":59,"s":[{"i":[[0,0],[-0.745,-3.894],[-5.61,-1.137],[-7.557,0.15]],"o":[[0.04,7.276],[1.176,6.145],[4.685,1.226],[2.785,0.43]],"v":[[-21.221,-29.776],[-21.735,-7.592],[-7.832,2.454],[9.503,3.125]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.7,"y":0},"t":77,"s":[{"i":[[0,0],[-0.534,-3.934],[-6.701,0.467],[-7.857,-1.7]],"o":[[-1.477,7.685],[0.843,6.208],[4.802,-0.335],[4.346,0.94]],"v":[[-22.17,-22.408],[-25.448,-0.509],[-8.293,2.277],[8.34,3.892]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":83,"s":[{"i":[[0,0],[-1.283,-3.822],[-6.716,0.378],[-7.766,-1.789]],"o":[[-1.159,7.502],[1.533,5.663],[4.777,-0.271],[4.347,1.005]],"v":[[-23.938,-21.192],[-25.341,0.043],[-8.232,2.315],[8.497,4.584]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":89,"s":[{"i":[[0,0],[-4.461,-3.345],[-6.782,-0.001],[-7.382,-2.168]],"o":[[0.19,6.726],[4.461,3.345],[4.673,0.001],[4.35,1.277]],"v":[[-31.448,-16.026],[-24.888,2.391],[-7.974,2.476],[8.356,5.481]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":95,"s":[{"i":[[0,0],[-5.006,-0.871],[-6.739,0.618],[-3.595,-1.191]],"o":[[3.138,5.078],[7.808,0.677],[3.051,-0.206],[4.086,1.935]],"v":[[-35.589,-2.901],[-23.582,6.559],[-7.395,2.829],[5.349,5.214]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":96,"s":[{"i":[[0,0],[-5.097,-0.459],[-6.732,0.721],[-1.487,-0.398]],"o":[[3.63,4.803],[8.366,0.233],[2.78,-0.24],[4.048,2.025]],"v":[[-36.279,-0.714],[-23.364,7.254],[-7.298,2.887],[-0.817,3.327]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":98,"s":[{"i":[[0,0],[-5.278,0.366],[-6.718,0.928],[-1.571,-0.06]],"o":[[4.612,4.253],[9.482,-0.657],[2.24,-0.309],[3.971,2.206]],"v":[[-37.66,3.661],[-22.928,8.643],[-7.105,3.005],[-2.464,2.943]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":102,"s":[{"i":[[0,0],[-3.051,3.557],[-6.774,-0.323],[-0.439,-0.145]],"o":[[6.234,-0.011],[3.051,-3.557],[1.059,0.05],[4.488,-0.165]],"v":[[-32.128,21.461],[-18.515,15.867],[-6.941,3.526],[-5.387,3.41]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":104,"s":[{"i":[[0,0],[-1.97,4.904],[-6.781,-0.025],[-0.04,0.149]],"o":[[5.985,-2.499],[1.97,-4.904],[0.507,0.073],[4.271,1.301]],"v":[[-26.349,30.241],[-16.309,19.479],[-6.859,3.786],[-6.722,3.667]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":106,"s":[{"i":[[0,0],[-0.723,4.978],[-6.358,0.441],[-0.086,-0.015]],"o":[[3.449,-4.195],[0.723,-4.978],[0.651,-0.017],[4.461,-0.533]],"v":[[-21.092,35.493],[-14.798,22.142],[-6.816,3.923],[-6.291,3.85]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":116,"s":[{"i":[[0,0],[0.27,3.962],[-5.659,-0.43],[0,0]],"o":[[-2.38,-4.193],[-0.461,-6.764],[0.813,0.105],[0,0]],"v":[[-6.789,40.46],[-11.955,22.079],[-6.74,4.165],[-6.541,4.123]],"c":false}]},{"i":{"x":0.56,"y":1},"o":{"x":0.167,"y":0},"t":132,"s":[{"i":[[0,0],[0.028,3.971],[-6.776,0.271],[0,0]],"o":[[0.92,-6.575],[-0.044,-6.266],[0.437,0.042],[0,0]],"v":[[-14.554,41.248],[-13.978,22.06],[-6.74,4.165],[-6.04,4.181]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.44,"y":0},"t":149,"s":[{"i":[[0,0],[0.028,3.971],[-6.696,-1.076],[0,0]],"o":[[-1.06,-6.297],[-0.044,-6.266],[0.486,0.034],[0,0]],"v":[[-11.825,41.269],[-13.468,22.09],[-6.74,4.165],[-4.523,4.686]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":163,"s":[{"i":[[0,0],[0.028,3.971],[-6.696,-1.076],[0,0]],"o":[[-1.06,-6.297],[-0.044,-6.266],[0.486,0.034],[0,0]],"v":[[-11.825,41.269],[-13.468,22.09],[-6.74,4.165],[-4.523,4.686]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":167,"s":[{"i":[[0,0],[0.028,3.971],[-6.774,0.312],[0.114,0.062]],"o":[[-1.06,-6.297],[-0.044,-6.266],[0.175,-0.024],[0.128,0.161]],"v":[[-11.825,41.269],[-13.468,22.09],[-6.74,4.165],[-3.2,4.045]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":168,"s":[{"i":[[0,0],[-0.332,3.858],[-6.615,-0.065],[-6.227,-2.487]],"o":[[-1.197,-5.882],[0.411,-5.901],[4.645,0.044],[4.032,1.605]],"v":[[-13.447,39.061],[-15.196,20.338],[-6.713,4.082],[8.845,6.116]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[{"i":[[0,0],[-2.006,3.333],[-5.838,-0.364],[-5.143,-1.039]],"o":[[-1.832,-3.948],[2.53,-4.202],[4.517,0.245],[3.33,0.673]],"v":[[-20.998,28.78],[-23.239,12.181],[-6.64,3.301],[7.921,5.13]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":172,"s":[{"i":[[0,0],[-3.833,2.656],[-5.544,-0.477],[-4.639,-1.02]],"o":[[-0.634,-3.479],[4.095,-3.091],[4.469,0.321],[2.949,0.736]],"v":[[-27.21,21.956],[-23.39,8.31],[-6.628,2.86],[7.594,4.819]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":173,"s":[{"i":[[0,0],[-5.661,1.98],[-5.25,-0.59],[-4.136,-1]],"o":[[0.565,-3.01],[5.66,-1.98],[4.421,0.397],[2.568,0.798]],"v":[[-33.422,15.133],[-23.541,4.439],[-6.54,3.01],[7.266,4.508]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[{"i":[[0,0],[-9.318,0.062],[-4.956,-0.704],[-4.288,-0.672]],"o":[[-0.353,0.511],[6.341,-0.042],[4.372,0.473],[2.252,0.625]],"v":[[-39.783,7.131],[-23.543,1.746],[-6.473,2.566],[6.939,4.148]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":175,"s":[{"i":[[0,0],[-7.373,-0.661],[-5.987,-0.908],[-3.473,-0.494]],"o":[[4.936,-1.112],[5.389,0.674],[4.345,0.516],[2.073,0.526]],"v":[[-41.655,1.633],[-24.332,-0.148],[-6.488,2.688],[6.748,3.903]],"c":false}]},{"i":{"x":0.1,"y":1},"o":{"x":0.167,"y":0.167},"t":177,"s":[{"i":[[0,0],[-3.484,-2.106],[-8.05,-1.318],[-3.2,-0.264]],"o":[[2.562,1.927],[3.484,2.106],[4.29,0.602],[1.716,0.329]],"v":[[-36.386,-11.472],[-25.909,-3.937],[-6.358,2.513],[6.464,3.595]],"c":false}]},{"t":195,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"hands 2","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],"o":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],"v":[[2.758,2.83],[11.206,2.761],[28.065,7.836],[30.818,22.809]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":23.514,"s":[{"i":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":34,"s":[{"i":[[-2.805,0.396],[-2.168,-0.779],[-1.2,-3.73],[-2.259,-5.223]],"o":[[3.592,-0.507],[6.085,2.162],[1.511,4.741],[0,0]],"v":[[2.725,4.04],[12.869,3.17],[22.316,18.867],[27.587,32.67]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":48.244,"s":[{"i":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":59,"s":[{"i":[[-2.816,0.149],[-2.272,-0.949],[-1.475,-3.735],[-3.255,-5.39]],"o":[[4.388,-0.192],[5.461,2.271],[1.826,4.641],[0,0]],"v":[[2.315,3.354],[12.923,3.555],[22.3,19.809],[29.56,32.985]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":77,"s":[{"i":[[-2.816,-0.309],[-2.628,-1.238],[-1.2,-3.73],[-2.259,-5.223]],"o":[[5.074,0.557],[5.841,2.753],[1.511,4.741],[0,0]],"v":[[1.925,2.738],[13.189,5.447],[22.65,21.244],[27.921,35.047]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":95,"s":[{"i":[[-0.598,-0.181],[-0.539,-0.561],[-0.196,-2.606],[1.687,-4.266]],"o":[[4.741,1.436],[3.565,3.814],[0.23,3.032],[0,0]],"v":[[5.057,5.135],[15.215,9.739],[20.248,24.608],[18.597,38.923]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":96,"s":[{"i":[[-0.457,-0.364],[-0.52,-0.555],[-0.187,-2.596],[1.723,-4.257]],"o":[[3.097,0.541],[3.544,3.823],[0.218,3.016],[0,0]],"v":[[9.643,6.899],[15.234,9.778],[20.225,24.639],[18.511,38.959]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":98,"s":[{"i":[[-0.369,-0.478],[-0.508,-0.551],[-0.181,-2.589],[1.746,-4.251]],"o":[[0.369,0.484],[3.531,3.83],[0.21,3.006],[0,0]],"v":[[14.421,8.736],[15.245,9.803],[20.211,24.659],[18.457,38.982]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":100,"s":[{"i":[[0,0],[-0.074,-0.046],[-0.361,-3.217],[1.172,-4.139]],"o":[[0,0],[3.532,3.723],[0.33,3.294],[0,0]],"v":[[15.242,9.767],[15.359,9.86],[20.37,24.574],[19.626,38.792]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":102,"s":[{"i":[[-0.412,-0.138],[-0.318,-0.426],[-0.593,-4.024],[0.433,-3.996]],"o":[[0.167,0.033],[3.533,3.586],[0.484,3.664],[0,0]],"v":[[15.365,9.779],[15.505,9.932],[20.575,24.466],[21.13,38.547]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":116,"s":[{"i":[[0,0],[-1.009,-0.802],[-1.352,-6.671],[-1.989,-3.524]],"o":[[0,0],[4.03,3.256],[0.988,4.877],[0,0]],"v":[[12.859,7.391],[15.063,8.718],[21.245,24.11],[26.059,37.745]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":124,"s":[{"i":[[0,0],[-1.097,-1.149],[-0.799,-5.031],[-0.839,-3.94]],"o":[[0,0],[3.609,3.446],[0.71,4.75],[0,0]],"v":[[12.902,6.938],[15.063,8.718],[20.372,24.232],[22.707,38.343]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":137,"s":[{"i":[[0,0],[-0.388,-0.392],[-0.475,-4.068],[-0.163,-4.185]],"o":[[0,0],[3.092,3.858],[0.546,4.676],[0,0]],"v":[[14.961,8.53],[15.063,8.718],[19.859,24.304],[20.739,38.695]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":157,"s":[{"i":[[0,0],[-0.58,-0.466],[-1.352,-6.671],[-0.912,-4.483]],"o":[[0,0],[3.124,3.255],[0.988,4.877],[0,0]],"v":[[14.98,9.235],[15.19,9.407],[20.156,24.263],[23.242,38.545]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":165,"s":[{"i":[[-3.102,-0.816],[-1.57,-1.341],[-1.352,-6.671],[-0.912,-4.483]],"o":[[0.548,0.101],[3.658,3.223],[0.988,4.877],[0,0]],"v":[[12.399,7.597],[15.19,9.407],[20.156,24.263],[23.242,38.545]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":167,"s":[{"i":[[-3.663,-0.964],[-1.749,-1.499],[-1.352,-6.671],[-0.912,-4.483]],"o":[[0.647,0.119],[3.754,3.217],[0.988,4.877],[0,0]],"v":[[8.772,6.017],[15.19,9.407],[20.156,24.263],[23.242,38.545]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":168,"s":[{"i":[[-3.718,-0.617],[-1.769,-1.459],[-1.384,-6.679],[-0.892,-4.426]],"o":[[6.299,1.053],[3.934,3.132],[1.01,4.878],[0,0]],"v":[[1.967,4.432],[15.1,9.259],[20.419,24.094],[23.49,38.317]],"c":false}]},{"t":189,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"body","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[116.171,151.524,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.723,"y":1},"o":{"x":0.395,"y":0},"t":21,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.361,"y":0},"t":32.309,"s":[116.171,147.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.723,"y":1},"o":{"x":0.395,"y":0},"t":48.244,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.723,"y":0.723},"o":{"x":0.167,"y":0.167},"t":73.717,"s":[116.171,147.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.71,"y":0},"t":167,"s":[116.171,147.924,0],"to":[0,0,0],"ti":[0,0,0]},{"t":187,"s":[116.171,146.724,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,14.211,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":15,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":21,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":26.025,"s":[98,102,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":32.309,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":39.988,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.48,0.48,0.48],"y":[0,0,0]},"t":48.244,"s":[100,100,100]},{"i":{"x":[0.471,0.471,0.471],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":59.158,"s":[98,102,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":73.717,"s":[100,100,100]},{"i":{"x":[0.471,0.471,0.471],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":89,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":104,"s":[100,100,100]},{"i":{"x":[0.471,0.471,0.471],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":114,"s":[98,102,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":133,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.71,0.71,0.71],"y":[0,0,0]},"t":167,"s":[100,100,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":175,"s":[98,102,100]},{"t":187,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0]],"v":[[-12.432,-14.575],[-11.977,7.814],[-8.487,12.849],[0.4,13.948],[9.223,12.916],[12.656,7.499],[12.482,-13.225]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0]],"v":[[-14.632,-6.25],[-13.927,7.863],[-10.625,12.562],[0.35,14.211],[11.198,12.616],[14.544,7.749],[14.632,-5.171]],"c":false}]},{"i":{"x":0.833,"y":0.913},"o":{"x":0.5,"y":0},"t":24,"s":[{"i":[[2.01,-10.893],[-0.221,-1.818],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395]],"o":[[-1.585,8.587],[0.221,1.818],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.024,-0.319]],"v":[[-11.288,-18.4],[-12.135,7.863],[-8.833,12.562],[0.35,14.211],[9.406,12.616],[12.752,7.749],[13.837,-10.133]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.087},"t":32,"s":[{"i":[[3.168,-10.612],[-0.338,-1.852],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-3.171,3.957]],"o":[[-2.669,8.935],[0.338,1.852],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.078,-0.146]],"v":[[-11.587,-18.638],[-12.157,7.863],[-8.855,12.562],[0.35,14.211],[9.428,12.616],[12.773,7.749],[14.172,-9.711]],"c":false}]},{"i":{"x":0.833,"y":0.954},"o":{"x":0.5,"y":0},"t":48,"s":[{"i":[[2.01,-10.893],[-0.209,-1.898],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395]],"o":[[-1.585,8.587],[0.209,1.898],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.024,-0.319]],"v":[[-11.08,-17.817],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[13.83,-9.727]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":77,"s":[{"i":[[3.203,-10.603],[-0.305,-1.276],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-0.275,6.886]],"o":[[-2.702,8.946],[0.305,1.276],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.08,-0.14]],"v":[[-11.404,-15.827],[-12.123,7.863],[-8.82,12.562],[0.35,14.211],[9.394,12.616],[12.739,7.749],[14.048,-5.48]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":95,"s":[{"i":[[5.646,-9.501],[-0.274,-1.374],[-1.915,-1.173],[-2.813,0],[-2.356,1.063],[-0.75,2.369],[-0.131,5.096]],"o":[[-5.397,9.039],[0.274,1.374],[1.958,1.192],[2.767,0],[1.87,-0.844],[0.743,-2.219],[0.08,-0.14]],"v":[[-10.068,-13.558],[-13.242,7.677],[-9.903,12.562],[-0.168,14.211],[8.516,12.616],[12.267,7.934],[13.587,-2.439]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":98,"s":[{"i":[[5.771,-9.444],[-0.346,-1.217],[-1.916,-1.191],[-2.806,0],[-2.35,1.063],[-0.784,2.382],[-0.124,5.004]],"o":[[-5.535,9.043],[0.346,1.217],[1.93,1.197],[2.76,0],[1.866,-0.844],[0.782,-2.332],[0.08,-0.14]],"v":[[-9.999,-13.441],[-13.299,7.667],[-9.958,12.562],[-0.195,14.211],[8.471,12.616],[12.243,7.944],[13.58,-2.197]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":104,"s":[{"i":[[5.832,-9.417],[-0.459,-1.398],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[-0.12,4.96]],"o":[[-5.602,9.046],[0.459,1.398],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.08,-0.14]],"v":[[-9.966,-13.385],[-13.327,7.663],[-9.985,12.562],[-0.208,14.211],[8.449,12.616],[12.232,7.949],[13.872,-2.099]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":117,"s":[{"i":[[5.832,-9.417],[-0.392,-1.279],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[0.974,5.882]],"o":[[-5.602,9.046],[0.392,1.279],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.08,-0.14]],"v":[[-8.982,-13.385],[-12.343,7.663],[-9.001,12.562],[-0.208,14.211],[8.058,12.616],[11.841,7.949],[13.276,-4.427]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":124,"s":[{"i":[[5.832,-9.417],[-0.23,-1.373],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[0.177,5.21]],"o":[[-5.602,9.046],[0.23,1.373],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.08,-0.14]],"v":[[-9.699,-13.385],[-13.06,7.663],[-9.718,12.562],[-0.208,14.211],[8.343,12.616],[12.126,7.949],[13.7,-2.8]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":134,"s":[{"i":[[5.832,-9.417],[-0.359,-1.248],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[-0.12,4.96]],"o":[[-5.602,9.046],[0.359,1.248],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.08,-0.14]],"v":[[-9.966,-13.385],[-13.327,7.663],[-9.985,12.562],[-0.208,14.211],[8.449,12.616],[12.232,7.949],[13.652,-2.058]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.71,"y":0},"t":167,"s":[{"i":[[5.832,-9.417],[-0.259,-1.248],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[-0.12,4.96]],"o":[[-5.602,9.046],[0.259,1.248],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.08,-0.14]],"v":[[-9.966,-13.385],[-13.327,7.663],[-9.985,12.562],[-0.208,14.211],[8.449,12.616],[12.232,7.949],[13.852,-2.099]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":171,"s":[{"i":[[4.926,-9.164],[-0.379,-1.503],[-1.916,-1.181],[-2.81,0],[-2.354,1.063],[-0.764,2.375],[-0.114,4.713]],"o":[[-4.766,9.084],[0.379,1.503],[1.946,1.194],[2.764,0],[1.868,-0.844],[0.76,-2.269],[0.076,-0.133]],"v":[[-10.024,-13.626],[-12.944,7.673],[-9.604,12.562],[-0.18,14.211],[8.309,12.616],[12.07,7.939],[13.516,-5.476]],"c":false}]},{"t":187,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0]],"v":[[-12.432,-14.575],[-11.977,7.814],[-8.487,12.849],[0.4,13.948],[9.223,12.916],[12.656,7.499],[12.482,-13.225]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":244,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"hand_6","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.75,47.75,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[11,11,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":240,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_hand7.json b/submodules/TelegramUI/Resources/Animations/anim_hand7.json deleted file mode 100644 index f8f9d1967d..0000000000 --- a/submodules/TelegramUI/Resources/Animations/anim_hand7.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.7.6","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"hand_7 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"handle","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.6],"y":[0]},"t":10,"s":[174]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[147]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.213],"y":[0]},"t":23,"s":[-17]},{"i":{"x":[0.61],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":27,"s":[-35.589]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.5],"y":[0]},"t":32,"s":[27.015]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":37,"s":[47.22]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0]},"t":42,"s":[-18]},{"i":{"x":[0.61],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":48,"s":[-35.589]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.5],"y":[0]},"t":54,"s":[27.015]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":61,"s":[47.22]},{"i":{"x":[0.785],"y":[-2.394]},"o":{"x":[0.518],"y":[0]},"t":73,"s":[-18]},{"i":{"x":[0.312],"y":[1]},"o":{"x":[0.204],"y":[0.31]},"t":81,"s":[-9.175]},{"t":97,"s":[174]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":10,"s":[-9.96,1.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":16,"s":[-16.661,-20.671,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":23,"s":[-36.588,-27.368,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":27,"s":[-30.271,-30.836,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":32,"s":[-11.215,-32.394,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":37,"s":[-16.116,-31.018,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":42,"s":[-36.588,-27.368,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":48,"s":[-30.271,-30.836,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":54,"s":[-11.215,-32.394,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":61,"s":[-16.116,-31.018,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":73,"s":[-36.588,-27.368,0],"to":[0,0,0],"ti":[0,0,0]},{"t":97,"s":[-9.802,-0.869,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-215.5,-116,0],"ix":1,"l":2},"s":{"a":0,"k":[20.019,19.981,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-215.146,-127.132]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-210.023,-182.103]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-207.5,-198.5]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":86,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-207.5,-198.5]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":87,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-211.874,-179.057]],"c":false}]},{"t":89,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-215.991,-154.088]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":10,"op":92,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"flag","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":92.982,"ix":10},"p":{"a":0,"k":[-204.72,-162.79,0],"ix":2,"l":2},"a":{"a":0,"k":[-122.092,14.474,0],"ix":1,"l":2},"s":{"a":0,"k":[99.986,100.014,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":12,"s":[{"i":[[0.317,-0.377],[9.871,0.008],[-5.426,0.66]],"o":[[-6.226,-0.015],[-1.013,-0.074],[4.743,0.541]],"v":[[-5.96,154.637],[-50.486,154.578],[-27.743,151.11]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0.317,-1.093],[9.871,0.023],[-3.056,2.72]],"o":[[-6.226,-0.044],[-1.013,-0.215],[7.435,1.476]],"v":[[-5.96,154.637],[-28.783,155.672],[-28.323,148.174]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0.317,-4.427],[9.871,0.094],[-7.661,10.615]],"o":[[-6.226,-0.178],[-1.013,-0.869],[16.085,12.234]],"v":[[-5.96,154.637],[-39.634,154.55],[-66.371,118.451]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-21.058,28.062]],"o":[[-6.226,-0.312],[-1.013,-1.524],[1.096,39.632]],"v":[[-5.96,154.637],[-50.486,153.428],[-40.692,84.982]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-6.509,17.627]],"o":[[-6.226,-0.312],[-1.013,-1.524],[5.307,24.241]],"v":[[-5.96,154.637],[-50.486,153.428],[-25.785,87.237]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-32.021,13.36]],"o":[[-6.226,-0.312],[-1.013,-1.524],[-16.69,26.199]],"v":[[-5.96,154.637],[-50.486,153.428],[15.356,105.805]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":25,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-5.426,13.583]],"o":[[-6.226,-0.312],[-1.013,-1.524],[4.743,11.147]],"v":[[-5.96,154.637],[-50.486,153.428],[-27.743,82.045]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[12.378,29.044]],"o":[[-6.226,-0.312],[-1.013,-1.524],[29.83,8.678]],"v":[[-5.96,154.637],[-50.486,153.428],[-51.505,85]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-31.325,-39.218]],"o":[[-6.226,0.289],[-1.013,1.414],[-3.169,-41.969]],"v":[[-5.83,154.152],[-51.499,151.85],[-15.606,221.238]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":34,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-5.426,-12.606]],"o":[[-6.226,0.289],[-1.013,1.414],[4.743,-10.345]],"v":[[-5.83,154.152],[-51.499,151.85],[-27.874,222.489]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":37,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[11.573,-55.334]],"o":[[-6.226,0.289],[-1.013,1.414],[39.449,-25.407]],"v":[[-5.83,154.152],[-51.499,151.85],[-52.752,224.049]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":42,"s":[{"i":[[0.317,-6.39],[9.871,0.136],[-11.942,3.44]],"o":[[-6.226,-0.257],[-1.013,-1.254],[-1.26,15.905]],"v":[[-5.948,154.592],[-50.579,153.284],[-20.684,99.413]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[12.378,29.044]],"o":[[-6.226,-0.312],[-1.013,-1.524],[29.83,8.678]],"v":[[-5.96,154.637],[-50.486,153.428],[-51.505,85]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":53,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-31.325,-39.218]],"o":[[-6.226,0.289],[-1.013,1.414],[-3.169,-41.969]],"v":[[-5.83,154.152],[-51.499,151.85],[-15.606,221.238]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":55.842,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-5.426,-12.606]],"o":[[-6.226,0.289],[-1.013,1.414],[4.743,-10.345]],"v":[[-5.83,154.152],[-51.499,151.85],[-27.874,222.489]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":63,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[11.573,-55.334]],"o":[[-6.226,0.289],[-1.013,1.414],[39.449,-25.407]],"v":[[-5.83,154.152],[-51.499,151.85],[-52.752,224.049]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":71,"s":[{"i":[[0.317,-5.893],[9.871,0.126],[-21.71,21.684]],"o":[[-6.226,-0.237],[-1.013,-1.157],[-2.05,24.036]],"v":[[-5.944,154.576],[-50.612,153.231],[-14.362,101.878]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":75,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-5.426,13.583]],"o":[[-6.226,-0.312],[-1.013,-1.524],[4.743,11.147]],"v":[[-5.96,154.637],[-50.486,153.428],[-27.743,82.045]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":81,"s":[{"i":[[0.317,5.961],[9.871,-0.127],[30.989,-21.541]],"o":[[-6.226,0.24],[-1.013,1.17],[42.685,-3.8]],"v":[[-5.841,154.192],[-51.415,151.981],[-117.05,184.128]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":83,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-22.34,-40.1]],"o":[[-6.226,0.289],[-1.013,1.414],[1.595,-36.326]],"v":[[-5.83,154.152],[-51.499,151.85],[-28.747,222.978]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":86,"s":[{"i":[[0.139,7.208],[9.871,0.091],[-5.112,-12.737]],"o":[[-6.231,0.135],[-1.048,1.388],[4.997,-10.225]],"v":[[-5.313,153.232],[-48.29,151.152],[-29.044,221.002]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":87,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-5.426,-12.606]],"o":[[-13.386,-2.417],[-1.013,1.414],[4.743,-10.345]],"v":[[-4.828,156.44],[-48.115,167.327],[-27.874,222.489]],"c":true}]},{"t":89,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-0.737,-10.604]],"o":[[-6.226,0.289],[-1.013,1.414],[4.743,-10.345]],"v":[[-5.83,154.152],[-15.405,184.967],[-16.93,216.13]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-113.297,-133.109],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":14,"op":90,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.989,275.127,0],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":182,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"head","parent":8,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":11,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":23,"s":[-13]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.16],"y":[0]},"t":71,"s":[-13]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0]},"t":98.215,"s":[0]},{"i":{"x":[0.09],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":106.715,"s":[7]},{"t":127.357421875,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[0,-37.327,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":11,"s":[0,-26.827,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":23,"s":[-4.4,-39.427,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":36,"s":[-2.401,-30.424,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":50,"s":[-4.4,-39.427,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.315,"y":0},"t":71,"s":[-2.401,-30.424,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.496,"y":1},"o":{"x":0.6,"y":0},"t":98,"s":[5.201,-26.827,0],"to":[0,0,0],"ti":[0,0,0]},{"t":127.357421875,"s":[0,-37.327,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":1,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":11,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":16,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":23,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":28,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":36,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":44,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":50,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":58,"s":[97,103,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.16,0.16,0.16],"y":[0,0,0]},"t":71,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":98.215,"s":[105,95,100]},{"i":{"x":[0.09,0.09,0.09],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":106.715,"s":[95,105,100]},{"t":127.357421875,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],"o":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],"v":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":183,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":3,"nm":"hands","parent":8,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":21,"s":[-10]},{"i":{"x":[0.17],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":70,"s":[-10]},{"t":128.572265625,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[-2.341,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[-2.341,-9.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[-3.741,-17.977,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":35,"s":[-1.541,-11.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.5,"y":0},"t":49,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.544,"y":0},"t":70,"s":[-1.541,-11.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.134,"y":1},"o":{"x":0.5,"y":0},"t":97,"s":[-2.341,-9.777,0],"to":[0,0,0],"ti":[0,0,0]},{"t":128.572265625,"s":[-2.341,-17.777,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":183,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"hands 3","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.659,-0.445]],"o":[[-4.86,-9.215],[0.566,4.637],[3.994,-0.505],[1.183,0.079]],"v":[[-9.125,0.522],[-27.079,-2.224],[-7.644,3.484],[11.49,2.929]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0,0],[-1.902,-5.873],[-9.833,-0.14],[-7.372,0.373]],"o":[[-4.328,1.758],[1.817,5.11],[4.766,0.217],[1.182,0.009]],"v":[[-17.041,-20.099],[-26.446,-5.406],[-7.078,3.541],[9.615,2.763]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":23,"s":[{"i":[[0,0],[-4.205,-5.986],[-9.957,-1.926],[-4.458,0.474]],"o":[[5.753,4.944],[4.232,6.025],[3.773,0.73],[1.179,-0.125]],"v":[[-37.604,-27.175],[-25.224,-11.553],[-5.844,2.761],[7.055,3.069]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[0,0],[-1.728,-7.979],[-9.811,-4.073],[-4.458,0.474]],"o":[[3.018,5.449],[1.024,4.728],[4.574,1.385],[1.179,-0.125]],"v":[[-30.389,-31.532],[-24.556,-12.392],[-6.174,2.474],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":32,"s":[{"i":[[0,0],[-0.133,-5.267],[-9.077,-3.752],[-4.458,0.474]],"o":[[-4.804,4.722],[0.193,7.66],[5.825,2.408],[1.179,-0.125]],"v":[[-10.804,-33.837],[-18.834,-15.906],[-6.69,2.025],[7.055,3.069]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":37,"s":[{"i":[[0,0],[-0.271,-4.621],[-9.517,-2.839],[-4.458,0.474]],"o":[[-3.394,4.297],[0.242,4.13],[4.799,1.569],[1.179,-0.125]],"v":[[-15.907,-30.468],[-22.029,-13.729],[-6.267,2.393],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":42,"s":[{"i":[[0,0],[-3.928,-6.171],[-9.957,-1.926],[-4.563,0.793]],"o":[[5.522,5.2],[3.954,6.211],[3.773,0.73],[1.168,-0.203]],"v":[[-36.207,-29.267],[-24.551,-13.099],[-5.703,2.786],[7.055,3.069]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":48,"s":[{"i":[[0,0],[-1.728,-7.979],[-9.811,-4.073],[-4.458,0.474]],"o":[[3.018,5.449],[1.024,4.728],[4.574,1.385],[1.179,-0.125]],"v":[[-30.389,-31.532],[-24.556,-12.392],[-6.174,2.474],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":54,"s":[{"i":[[0,0],[-0.133,-5.267],[-9.319,-3.103],[-4.568,0.674]],"o":[[-4.804,4.722],[0.193,7.66],[5.735,1.909],[1.173,-0.173]],"v":[[-10.804,-33.837],[-18.834,-15.906],[-6.656,1.832],[7.072,2.972]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[0,0],[-0.271,-4.621],[-9.517,-2.839],[-4.458,0.474]],"o":[[-3.394,4.297],[0.242,4.13],[4.799,1.569],[1.179,-0.125]],"v":[[-15.907,-30.468],[-22.029,-13.729],[-6.267,2.393],[7.055,3.069]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":73,"s":[{"i":[[0,0],[-4.205,-5.986],[-9.957,-1.926],[-4.458,0.474]],"o":[[5.753,4.944],[4.232,6.025],[3.773,0.73],[1.179,-0.125]],"v":[[-37.604,-27.175],[-25.224,-11.553],[-5.844,2.761],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":97,"s":[{"i":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.659,-0.445]],"o":[[-4.86,-9.215],[0.566,4.637],[3.994,-0.505],[1.183,0.079]],"v":[[-9.125,0.522],[-27.079,-2.224],[-7.644,3.484],[11.49,2.929]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":110,"s":[{"i":[[0,0],[-3.924,-6.477],[-10.094,-0.98],[-3.546,0.075]],"o":[[4.365,3.67],[3.808,6.286],[3.961,0.384],[1.183,-0.028]],"v":[[-37.672,-26.337],[-25.271,-10.97],[-5.942,2.371],[7.258,2.943]],"c":false}]},{"t":131,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":183,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"hands 2","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],"o":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],"v":[[2.763,2.93],[11.324,2.869],[28.065,7.836],[30.818,22.809]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[-2.833,0.019],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.963,-0.034],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.632,3.384],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":36,"s":[{"i":[[-2.83,-0.135],[-3.536,-1.048],[-1.592,-3.476],[-1.09,-6.235]],"o":[[7.035,0.337],[5.485,1.626],[1.67,3.647],[0,0]],"v":[[1.498,3.488],[12.683,2.542],[23.022,15.874],[26.398,31.323]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":50,"s":[{"i":[[-2.829,0.15],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.712,-0.25],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.417,3.447],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":71,"s":[{"i":[[-2.833,0.023],[-3.532,-0.206],[-1.416,-3.799],[-0.329,-6.321]],"o":[[4.317,-0.036],[4.623,0.27],[1.401,3.758],[0,0]],"v":[[1.983,3.325],[12.861,3.014],[25.932,11.288],[27.784,26.515]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":97,"s":[{"i":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],"o":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],"v":[[2.763,2.93],[11.324,2.869],[28.065,7.836],[30.818,22.809]],"c":false}]},{"t":124.927734375,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":183,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"body","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[116.171,151.524,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[116.171,140.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":35,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.5,"y":0},"t":49,"s":[116.171,143.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.544,"y":0},"t":70,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.134,"y":1},"o":{"x":0.5,"y":0},"t":97,"s":[116.171,151.524,0],"to":[0,0,0],"ti":[0,0,0]},{"t":128.572265625,"s":[116.171,146.724,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,14.211,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":15,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":21,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":27,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":35,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":42,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":49,"s":[100,100,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":56,"s":[97,103,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.422,0.422,0.422],"y":[0,0,0]},"t":70,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":97,"s":[100,100,100]},{"i":{"x":[0.17,0.17,0.17],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":109.143,"s":[97,103,100]},{"t":128.572265625,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0]],"v":[[-12.432,-14.575],[-11.977,7.814],[-8.487,12.849],[0.4,13.948],[9.223,12.916],[12.656,7.499],[12.482,-13.225]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0]],"v":[[-14.032,-4.05],[-13.127,7.863],[-9.825,12.562],[0.35,14.211],[10.398,12.616],[13.744,7.749],[14.232,-7.571]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[{"i":[[-1.863,-5.271],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.3,6.472]],"o":[[1.863,5.271],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.223,-1.111]],"v":[[-14.632,-13.411],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[11.232,-16.011]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":35,"s":[{"i":[[-1.922,-5.439],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[2.094,6.249]],"o":[[1.922,5.439],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.323]],"v":[[-15.346,-10.627],[-12.527,7.863],[-9.225,12.562],[0.35,14.211],[9.798,12.616],[13.144,7.749],[11.974,-10.116]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":42,"s":[{"i":[[-1.882,-5.326],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.559,6.399]],"o":[[1.882,5.326],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.197,-0.919]],"v":[[-14.481,-12.569],[-12.098,7.863],[-8.796,12.562],[0.35,14.211],[9.369,12.616],[12.715,7.749],[11.484,-13.946]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":49,"s":[{"i":[[-1.863,-5.271],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.3,6.472]],"o":[[1.863,5.271],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.223,-1.111]],"v":[[-14.732,-16.111],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[11.532,-15.511]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":54,"s":[{"i":[[-1.872,-5.295],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.413,6.44]],"o":[[1.872,5.295],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.191,-0.998]],"v":[[-14.729,-16.667],[-12.144,7.863],[-8.842,12.562],[0.35,14.211],[9.415,12.616],[12.761,7.749],[11.555,-14.741]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":57,"s":[{"i":[[-1.882,-5.326],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.559,6.399]],"o":[[1.882,5.326],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0.274]],"v":[[-14.818,-14.61],[-12.155,7.863],[-8.853,12.562],[0.35,14.211],[9.426,12.616],[12.771,7.749],[11.18,-14.235]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":70,"s":[{"i":[[-1.922,-5.439],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[2.094,6.249]],"o":[[1.922,5.439],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.323]],"v":[[-15.324,-9.227],[-12.527,7.863],[-9.225,12.562],[0.35,14.211],[9.798,12.616],[13.144,7.749],[11.974,-10.116]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":97,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0]],"v":[[-14.032,-4.05],[-13.127,7.863],[-9.825,12.562],[0.35,14.211],[10.398,12.616],[13.744,7.749],[14.232,-7.571]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":104,"s":[{"i":[[-0.837,-2.368],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0.912,2.72]],"o":[[0.837,2.368],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.141]],"v":[[-13.849,-7.596],[-12.139,7.863],[-8.836,12.562],[0.35,14.211],[9.41,12.616],[12.755,7.749],[12.383,-8.209]],"c":false}]},{"i":{"x":0.17,"y":1},"o":{"x":0.167,"y":0.167},"t":112,"s":[{"i":[[0,-1.426],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0.292,0.87]],"o":[[0,0.803],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.045]],"v":[[-13.276,-13.757],[-12.148,7.863],[-8.845,12.562],[0.35,14.211],[9.419,12.616],[12.764,7.749],[12.791,-11.807]],"c":false}]},{"t":128.572265625,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0]],"v":[[-12.432,-14.575],[-11.977,7.814],[-8.487,12.849],[0.4,13.948],[9.223,12.916],[12.656,7.499],[12.482,-13.225]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":183,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"hand_7","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.75,47.75,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[11,11,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_moretosort_r.json b/submodules/TelegramUI/Resources/Animations/anim_moretosort_r.json deleted file mode 100644 index c1466c90e7..0000000000 --- a/submodules/TelegramUI/Resources/Animations/anim_moretosort_r.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.12.1","fr":60,"ip":0,"op":90,"w":512,"h":512,"nm":"more_to_sort_cw","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"1","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[101,150,0],"to":[2.333,0,0],"ti":[-2.333,0,0]},{"t":89,"s":[115,150,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,-0.823],[-0.823,0],[0,0],[0,0.823],[0.812,0]],"o":[[-0.823,0],[0,0.823],[0,0],[0.812,0],[0,-0.823],[0,0]],"v":[[-0.01,-1.5],[-1.5,0],[-0.01,1.5],[0.01,1.5],[1.5,0],[0.01,-1.5]],"c":true}]},{"t":89,"s":[{"i":[[0,0],[0,-0.367],[-0.367,0],[0,0],[0,0.367],[0.367,0]],"o":[[-0.367,0],[0,0.367],[0,0],[0.367,0],[0,-0.367],[0,0]],"v":[[-6,-0.665],[-6.665,0],[-6,0.665],[6,0.665],[6.665,0],[6,-0.665]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[1000,1000],"ix":3},"r":{"a":0,"k":-90,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 202 (Stroke)","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"2","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[150.5,150,0],"to":[0.75,0,0],"ti":[-0.75,0,0]},{"t":89,"s":[155,150,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,-0.823],[-0.823,0],[0,0],[0,0.823],[0.812,0]],"o":[[-0.823,0],[0,0.823],[0,0],[0.812,0],[0,-0.823],[0,0]],"v":[[-0.01,-1.5],[-1.5,0],[-0.01,1.5],[0.01,1.5],[1.5,0],[0.01,-1.5]],"c":true}]},{"t":89,"s":[{"i":[[0,0],[0,-0.367],[-0.367,0],[0,0],[0,0.367],[0.367,0]],"o":[[-0.367,0],[0,0.367],[0,0],[0.367,0],[0,-0.367],[0,0]],"v":[[-4,-0.665],[-4.665,0],[-4,0.665],[4,0.665],[4.665,0],[4,-0.665]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[1000,1000],"ix":3},"r":{"a":0,"k":-90,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 202 (Stroke)","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"3","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[200,150,0],"to":[-0.833,0,0],"ti":[0.833,0,0]},{"t":89,"s":[195,150,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[0,-0.823],[-0.823,0],[0,0],[0,0.823],[0.812,0]],"o":[[-0.823,0],[0,0.823],[0,0],[0.812,0],[0,-0.823],[0,0]],"v":[[-0.01,-1.5],[-1.5,0],[-0.01,1.5],[0.01,1.5],[1.5,0],[0.01,-1.5]],"c":true}]},{"t":89,"s":[{"i":[[0,0],[0,-0.367],[-0.367,0],[0,0],[0,0.367],[0.367,0]],"o":[[-0.367,0],[0,0.367],[0,0],[0.367,0],[0,-0.367],[0,0]],"v":[[-2,-0.665],[-2.665,0],[-2,0.665],[2,0.665],[2.665,0],[2,-0.665]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[1000,1000],"ix":3},"r":{"a":0,"k":-90,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 202 (Stroke)","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"more and search","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":0,"s":[0]},{"t":89,"s":[90]}],"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2,"l":2},"a":{"a":0,"k":[150,150,0],"ix":1,"l":2},"s":{"a":0,"k":[170.667,170.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-55.228,0],[0,55.229],[55.229,0],[0,-55.228]],"o":[[55.229,0],[0,-55.228],[-55.228,0],[0,55.229]],"v":[[23.5,123.5],[123.5,23.5],[23.5,-76.5],[-76.5,23.5]],"c":true}]},{"t":102,"s":[{"i":[[-55.228,0],[0,55.229],[55.229,0],[0,-55.228]],"o":[[55.229,0],[0,-55.228],[-55.228,0],[0,55.229]],"v":[[23.5,123.5],[123.5,23.5],[23.5,-76.5],[-76.5,23.5]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13.33,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[126.5,126.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"c","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-10,"op":92,"st":-10,"ct":1,"bm":0}],"markers":[],"props":{}} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_notificationsoundprogress.json b/submodules/TelegramUI/Resources/Animations/anim_notificationsoundprogress.json deleted file mode 100644 index 7c93f3b7b7..0000000000 --- a/submodules/TelegramUI/Resources/Animations/anim_notificationsoundprogress.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"4.8.0","meta":{"g":"LottieFiles AE ","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":60,"w":512,"h":512,"nm":"Sound Download","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL ALL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.1,"y":0},"t":-1,"s":[256,56,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.7,"y":0},"t":11,"s":[256,322,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":25,"s":[256,228,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.41,"y":1},"o":{"x":0.3,"y":0},"t":39,"s":[256,288,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[256,256,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.359,0.359,0.115],"y":[0,0,0]},"t":0,"s":[20,60,100]},{"i":{"x":[0.54,0.54,0.738],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":11,"s":[102,70,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":25,"s":[90,110,100]},{"i":{"x":[0.41,0.41,0.41],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":39,"s":[105,95,100]},{"t":59,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":1,"op":179,"st":-1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Arrow 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.7,"y":1},"o":{"x":0.7,"y":0},"t":8,"s":[387,21.333,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":22,"s":[389,181.333,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.41,"y":1},"o":{"x":0.3,"y":0},"t":36,"s":[399,111.333,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[395,141.333,0]}],"ix":2},"a":{"a":0,"k":[139,-114.667,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[112.991,-95.024],[139.219,-96.402],[166.009,-94.955]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[112.991,-101.972],[139.219,-36.333],[166.009,-105.232]],"c":false}]},{"i":{"x":0.71,"y":1},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[68,-84],[139.173,-36.333],[210,-84]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.29,"y":0},"t":36,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[86.5,-84],[139.137,-36.333],[193,-84.5]],"c":false}]},{"t":59,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[70.667,-86.5],[139.167,-36.333],[207.333,-86.5]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[138.582,-93.931],[139.036,-96.361]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[138.582,-154],[139.036,-38.319]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":19,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[138.145,-207.143],[139.051,-37.851]],"c":false}]},{"i":{"x":0.71,"y":1},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[138.063,-170],[139.063,-37.5]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.29,"y":0},"t":36,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[139.063,-193],[139.051,-37.5]],"c":false}]},{"t":59,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[139.063,-193],[139.063,-37.5]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":40,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":12,"op":179,"st":-1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"R","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[88.174,82.369,0],"ix":2},"a":{"a":0,"k":[88.174,82.369,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.6,"y":1},"o":{"x":0.2,"y":0},"t":14,"s":[{"i":[[-0.626,-19.83],[0,-0.716],[39.672,0],[0,30.227],[-39.672,0],[-10.43,-4.012],[0,0],[-5.043,-3.301]],"o":[[0,0],[0,30.227],[-39.672,0],[0,-30.227],[12.077,0],[0,0],[3.48,-0.35],[11.788,7.716]],"v":[[159.476,-150.419],[160.007,103.453],[84.753,158.183],[16.341,103.453],[84.753,48.723],[118.958,55.005],[118.979,-191.031],[139.841,-186.806]],"c":true}]},{"i":{"x":0.517,"y":1},"o":{"x":0.589,"y":0},"t":22,"s":[{"i":[[-4.753,-0.752],[0,-0.716],[39.672,0],[0,30.227],[-39.672,0],[-10.43,-4.012],[0,0],[-5.911,-1.178]],"o":[[0,0],[0,30.227],[-39.672,0],[0,-30.227],[12.077,0],[0,0],[5.313,2.033],[9.821,1.957]],"v":[[159.61,61.441],[160.007,103.453],[84.753,158.183],[16.341,103.453],[84.753,48.723],[118.958,55.005],[119.173,53.964],[136.554,58.5]],"c":true}]},{"i":{"x":0.41,"y":1},"o":{"x":0.433,"y":0},"t":36,"s":[{"i":[[-6.917,3.879],[0,-0.716],[39.672,0],[0,30.227],[-39.672,0],[-10.43,-4.012],[0,0],[-6.028,0]],"o":[[0,0],[0,30.227],[-39.672,0],[0,-30.227],[12.077,0],[0,0],[5.313,2.033],[8.494,0]],"v":[[159.348,-48.29],[160.007,103.453],[84.753,158.183],[16.341,103.453],[84.753,48.723],[118.958,55.005],[118.851,-45.346],[135.959,-42.2]],"c":true}]},{"t":59,"s":[{"i":[[-6.917,3.879],[0,-0.716],[39.672,0],[0,30.227],[-39.672,0],[-10.43,-4.012],[0,0],[-6.028,0]],"o":[[0,0],[0,30.227],[-39.672,0],[0,-30.227],[12.077,0],[0,0],[5.313,2.033],[8.494,0]],"v":[[159.452,6.555],[160.007,103.453],[84.753,158.183],[16.341,103.453],[84.753,48.723],[118.958,55.005],[118.955,9.499],[136.063,12.646]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":181,"st":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"L","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-58.912,2.378,0],"ix":2},"a":{"a":0,"k":[-58.912,2.378,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":0.751},"o":{"x":0.05,"y":0},"t":13,"s":[{"i":[[0,0],[0,0],[0,0],[0.171,-0.721],[0,-0.687],[39.672,0],[0,30.227],[-39.672,0],[-9.268,-3.321],[0,0],[-19.193,4.657]],"o":[[0,0],[0,0],[-0.752,0.152],[0,0],[0,30.227],[-39.672,0],[0,-30.227],[10.92,0],[0,0],[0,-19.896],[0,0]],"v":[[118.79,-191.298],[119.935,-149.636],[-61.03,-115.682],[-62.53,-114.233],[-62.334,137.659],[-134.167,192.389],[-206,137.659],[-134.167,82.929],[-103.627,88.107],[-103.629,-113.786],[-70.799,-155.552]],"c":true}]},{"i":{"x":0.564,"y":1},"o":{"x":0.514,"y":0.128},"t":32,"s":[{"i":[[0,0],[0,0],[0,0],[0.171,-0.721],[0,-0.687],[39.672,0],[0,30.227],[-39.672,0],[-9.268,-3.322],[0,0],[-19.192,4.657]],"o":[[0,0],[0,0],[-0.752,0.152],[0,0],[0,30.227],[-39.672,0],[0,-30.227],[10.92,0],[0,0],[0,-19.896],[0,0]],"v":[[54.288,-183.407],[54.277,-141.529],[-61.03,-115.682],[-62.53,-114.233],[-62.334,137.659],[-134.167,192.389],[-206,137.659],[-134.167,82.929],[-103.627,88.107],[-103.629,-113.786],[-70.799,-155.552]],"c":true}]},{"t":59,"s":[{"i":[[0,0],[0,0],[0,0],[0.171,-0.721],[0,-0.687],[39.672,0],[0,30.227],[-39.672,0],[-9.268,-3.322],[0,0],[-19.193,4.657]],"o":[[0,0],[0,0],[-0.752,0.152],[0,0],[0,30.227],[-39.672,0],[0,-30.227],[10.92,0],[0,0],[0,-19.896],[0,0]],"v":[[88.175,-187.632],[88.165,-145.754],[-61.03,-115.682],[-62.53,-114.233],[-62.334,137.659],[-134.167,192.389],[-206,137.659],[-134.167,82.929],[-103.628,88.107],[-103.629,-113.786],[-70.799,-155.552]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":179,"st":-1,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/anim_voicemute.json b/submodules/TelegramUI/Resources/Animations/anim_voicemute.json deleted file mode 100644 index a37a9c8609..0000000000 --- a/submodules/TelegramUI/Resources/Animations/anim_voicemute.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.5.9","fr":60,"ip":0,"op":20,"w":72,"h":72,"nm":"ic_mute","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[34.5,37.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 5","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[34.5,37.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.109803922474,0.109803922474,0.117647059262,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Icon","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[36,37.2,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.36,0],[0,-0.36],[3.81,-0.34],[0,0],[0.37,0],[0.04,0.32],[0,0],[0,0],[0,3.9],[-0.36,0],[0,-0.36],[-3.38,0],[0,3.39]],"o":[[0.37,0],[0,3.9],[0,0],[0,0.37],[-0.33,0],[0,0],[0,0],[-3.81,-0.34],[0,-0.36],[0.37,0],[0,3.39],[3.39,0],[0,-0.36]],"v":[[6.796,-1.464],[7.466,-0.804],[0.666,6.636],[0.666,9.196],[-0.004,9.866],[-0.654,9.286],[-0.664,9.196],[-0.664,6.636],[-7.464,-0.804],[-6.804,-1.464],[-6.134,-0.804],[-0.004,5.336],[6.136,-0.804]],"c":true},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-2.35,0],[-0.09,-2.28],[0,0],[0,0],[2.36,0],[0.1,2.27],[0,0],[0,0]],"o":[[2.3,0],[0,0],[0,0],[0,2.36],[-2.29,0],[0,0],[0,0],[0,-2.35]],"v":[[-0.004,-9.864],[4.256,-5.774],[4.266,-5.604],[4.266,-0.804],[-0.004,3.466],[-4.264,-0.624],[-4.264,-0.804],[-4.264,-5.604]],"c":true},"ix":2},"nm":"Контур 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[1.62,0],[0.09,-1.55],[0,0],[0,0],[-1.63,0],[-0.08,1.55],[0,0],[0,0]],"o":[[-1.57,0],[0,0],[0,0],[0,1.62],[1.57,0],[0,0],[0,0],[0,-1.63]],"v":[[0.004,-8.536],[-2.936,-5.756],[-2.936,-5.596],[-2.936,-0.796],[0.004,2.134],[2.934,-0.646],[2.934,-0.796],[2.934,-5.596]],"c":true},"ix":2},"nm":"Контур 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Объединить контуры 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Icon","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/device_edge.json b/submodules/TelegramUI/Resources/Animations/device_edge.json deleted file mode 100644 index 487209ad55..0000000000 --- a/submodules/TelegramUI/Resources/Animations/device_edge.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.7.4","fr":60,"ip":0,"op":120,"w":30,"h":30,"nm":"edge_30","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Union","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.003,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-0.082,0.078],[-0.014,0.019],[0,0.42],[0.001,0.035],[0,0],[0,0],[0,0.009],[0.002,0.03],[0.293,0.391],[0.348,0.176],[0.39,0.003],[0.247,-0.117],[0,0],[0,0],[0.009,-0.004],[0.226,-0.375],[0.005,-0.438],[-3.697,0],[-0.897,0.336],[-0.267,0.139],[-0.069,-0.011],[-0.048,-0.051],[-0.007,-0.069],[0.037,-0.059],[2.152,-0.744],[0,0],[0.253,-0.054],[-0.228,0.072],[2.252,0.966],[1.04,2.219],[-0.014,1.51],[-0.03,0.216],[0.003,-0.209],[-1.868,1.842],[-2.628,0],[-1.655,-3.238],[-0.012,-0.024],[-0.003,-0.006],[0.019,-1.66],[0.417,-0.725],[0.722,-0.422],[0.832,-0.002],[0.001,0],[1.105,0.768],[0,0.204]],"o":[[0.024,-0.023],[0.395,-0.514],[0,-0.029],[0,0],[0,0],[0,-0.009],[-0.001,-0.027],[-0.029,-0.486],[-0.233,-0.313],[-0.348,-0.176],[-0.533,-0.01],[0,0],[0,0],[-0.009,0.004],[-0.384,0.209],[-0.226,0.375],[0,3.263],[0.958,0.002],[0.282,-0.106],[0.061,-0.034],[0.069,0.011],[0.048,0.051],[0.007,0.069],[-1.215,1.926],[0,0],[-0.193,0.061],[0.234,-0.044],[-2.325,0.775],[-2.252,-0.966],[-0.636,-1.37],[0,-0.213],[-0.033,0.203],[0.041,-2.622],[1.871,-1.845],[3.886,0],[0.011,0.022],[0.003,0.006],[0.283,0.552],[-0.002,0.836],[-0.417,0.725],[-0.716,0.425],[0,0],[-0.073,0.003],[-0.236,-0.165],[0,-0.191]],"v":[[1.84,1.691],[1.9,1.628],[2.509,0.073],[2.508,-0.023],[2.508,-0.022],[2.508,-0.021],[2.507,-0.048],[2.504,-0.134],[2.011,-1.479],[1.127,-2.223],[0.004,-2.495],[-1.181,-2.196],[-1.181,-2.196],[-1.183,-2.195],[-1.21,-2.182],[-2.142,-1.29],[-2.493,-0.049],[4.423,5.757],[7.228,5.252],[8.051,4.885],[8.251,4.849],[8.43,4.943],[8.514,5.128],[8.467,5.325],[3.264,9.45],[3.162,9.484],[2.478,9.66],[3.171,9.487],[-3.935,9.19],[-9.045,4.245],[-9.991,-0.13],[-9.945,-0.774],[-10,-0.156],[-7.022,-7.122],[-0.001,-10.001],[9.114,-4.9],[9.149,-4.832],[9.157,-4.815],[10,-1.47],[9.361,0.912],[7.622,2.661],[5.258,3.314],[5.256,3.314],[2.011,2.635],[1.642,2.071]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":40,"s":[{"i":[[-0.082,0.078],[-0.014,0.019],[0,0.42],[0.001,0.035],[0,0],[0,0],[0,0.009],[0.002,0.03],[0.293,0.391],[0.348,0.176],[0.39,0.003],[0.247,-0.117],[0,0],[0,0],[0.009,-0.004],[0.226,-0.375],[0.005,-0.438],[-3.279,0.493],[-0.897,0.336],[-0.267,0.139],[-0.069,-0.011],[-0.048,-0.051],[-0.007,-0.069],[0.037,-0.059],[2.152,-0.744],[0,0],[0.253,-0.054],[-0.228,0.072],[2.252,0.966],[1.04,2.219],[-0.014,1.51],[-0.03,0.216],[0.003,-0.209],[-1.868,1.842],[-2.628,0],[-1.655,-3.238],[-0.012,-0.024],[-0.003,-0.006],[0.019,-1.66],[0.417,-0.725],[0.722,-0.422],[0.832,-0.002],[0.001,0],[1.105,0.768],[0,0.204]],"o":[[0.024,-0.023],[0.395,-0.514],[0,-0.029],[0,0],[0,0],[0,-0.009],[-0.001,-0.027],[-0.029,-0.486],[-0.233,-0.313],[-0.348,-0.176],[-0.533,-0.01],[0,0],[0,0],[-0.009,0.004],[-0.384,0.209],[-0.226,0.375],[0,3.263],[1.152,-0.173],[0.282,-0.106],[0.061,-0.034],[0.069,0.011],[0.048,0.051],[0.007,0.069],[-1.215,1.926],[0,0],[-0.193,0.061],[0.234,-0.044],[-2.325,0.775],[-2.252,-0.966],[-0.636,-1.37],[0,-0.213],[-0.033,0.203],[0.041,-2.622],[1.871,-1.845],[3.886,0],[0.011,0.022],[0.003,0.006],[0.283,0.552],[-0.002,0.836],[-0.417,0.725],[-0.716,0.425],[0,0],[-0.073,0.003],[-0.236,-0.165],[0,-0.191]],"v":[[1.319,2.836],[1.379,2.774],[2.509,0.073],[2.508,-0.023],[2.508,-0.022],[2.508,-0.021],[2.507,-0.048],[2.504,-0.134],[2.011,-1.479],[1.127,-2.223],[0.004,-2.495],[-1.181,-2.196],[-1.181,-2.196],[-1.183,-2.195],[-1.21,-2.182],[-2.142,-1.29],[-2.493,-0.049],[4.423,5.757],[6.189,5.293],[7.114,4.78],[7.313,4.745],[7.493,4.839],[7.577,5.024],[7.529,5.221],[3.264,9.45],[3.162,9.484],[2.478,9.66],[3.171,9.487],[-3.935,9.19],[-9.045,4.245],[-9.991,-0.13],[-9.945,-0.774],[-10,-0.156],[-7.022,-7.122],[-0.001,-10.001],[9.114,-4.9],[9.149,-4.832],[9.157,-4.815],[10,-1.47],[8.84,2.058],[7.102,3.807],[4.737,4.459],[4.735,4.459],[1.49,3.781],[1.121,3.216]],"c":true}]},{"t":50,"s":[{"i":[[-0.082,0.078],[-0.014,0.019],[0,0.42],[0.001,0.035],[0,0],[0,0],[0,0.009],[0.002,0.03],[0.293,0.391],[0.348,0.176],[0.39,0.003],[0.247,-0.117],[0,0],[0,0],[0.009,-0.004],[0.226,-0.375],[0.005,-0.438],[-3.697,0],[-0.897,0.336],[-0.267,0.139],[-0.069,-0.011],[-0.048,-0.051],[-0.007,-0.069],[0.037,-0.059],[2.152,-0.744],[0,0],[0.253,-0.054],[-0.228,0.072],[2.252,0.966],[1.04,2.219],[-0.014,1.51],[-0.03,0.216],[0.003,-0.209],[-1.868,1.842],[-2.628,0],[-1.655,-3.238],[-0.012,-0.024],[-0.003,-0.006],[0.019,-1.66],[0.417,-0.725],[0.722,-0.422],[0.832,-0.002],[0.001,0],[1.105,0.768],[0,0.204]],"o":[[0.024,-0.023],[0.395,-0.514],[0,-0.029],[0,0],[0,0],[0,-0.009],[-0.001,-0.027],[-0.029,-0.486],[-0.233,-0.313],[-0.348,-0.176],[-0.533,-0.01],[0,0],[0,0],[-0.009,0.004],[-0.384,0.209],[-0.226,0.375],[0,3.263],[0.958,0.002],[0.282,-0.106],[0.061,-0.034],[0.069,0.011],[0.048,0.051],[0.007,0.069],[-1.215,1.926],[0,0],[-0.193,0.061],[0.234,-0.044],[-2.325,0.775],[-2.252,-0.966],[-0.636,-1.37],[0,-0.213],[-0.033,0.203],[0.041,-2.622],[1.871,-1.845],[3.886,0],[0.011,0.022],[0.003,0.006],[0.283,0.552],[-0.002,0.836],[-0.417,0.725],[-0.716,0.425],[0,0],[-0.073,0.003],[-0.236,-0.165],[0,-0.191]],"v":[[1.84,1.691],[1.9,1.628],[2.509,0.073],[2.508,-0.023],[2.508,-0.022],[2.508,-0.021],[2.507,-0.048],[2.504,-0.134],[2.011,-1.479],[1.127,-2.223],[0.004,-2.495],[-1.181,-2.196],[-1.181,-2.196],[-1.183,-2.195],[-1.21,-2.182],[-2.142,-1.29],[-2.493,-0.049],[4.423,5.757],[7.228,5.252],[8.051,4.885],[8.251,4.849],[8.43,4.943],[8.514,5.128],[8.467,5.325],[3.264,9.45],[3.162,9.484],[2.478,9.66],[3.171,9.487],[-3.935,9.19],[-9.045,4.245],[-9.991,-0.13],[-9.945,-0.774],[-10,-0.156],[-7.022,-7.122],[-0.001,-10.001],[9.114,-4.9],[9.149,-4.832],[9.157,-4.815],[10,-1.47],[9.361,0.912],[7.622,2.661],[5.258,3.314],[5.256,3.314],[2.011,2.635],[1.642,2.071]],"c":true}]}],"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0.005]],"o":[[0,-0.005],[0,0]],"v":[[-10,-0.142],[-10,-0.156]],"c":false},"ix":2},"nm":"Контур 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Объединить контуры 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Union","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":-60,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"Ellipse 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":40,"s":[1090]},{"t":50,"s":[1080]}],"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[16.667,16.667,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[11.667,11.667,100]},{"t":40,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"ip":0,"op":120,"st":-60,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Fonts/SFCompactRounded-Semibold.otf b/submodules/TelegramUI/Resources/Fonts/SFCompactRounded-Semibold.otf deleted file mode 100644 index 100e58bff7..0000000000 Binary files a/submodules/TelegramUI/Resources/Fonts/SFCompactRounded-Semibold.otf and /dev/null differ diff --git a/submodules/TelegramUI/Resources/Fonts/latinmodern-math.otf b/submodules/TelegramUI/Resources/Fonts/latinmodern-math.otf new file mode 100644 index 0000000000..a7347598ae Binary files /dev/null and b/submodules/TelegramUI/Resources/Fonts/latinmodern-math.otf differ diff --git a/submodules/TelegramUI/Resources/Fonts/latinmodern-math.plist b/submodules/TelegramUI/Resources/Fonts/latinmodern-math.plist new file mode 100755 index 0000000000..c9240d8b76 Binary files /dev/null and b/submodules/TelegramUI/Resources/Fonts/latinmodern-math.plist differ diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index eca7ccd52f..354ab8cf9a 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -20,6 +20,8 @@ import FetchManagerImpl import InAppPurchaseManager import AnimationCache import MultiAnimationRenderer +import DCTAnimationCacheImpl +import DCTMultiAnimationRendererImpl import AppBundle import DirectMediaImageCache @@ -317,19 +319,19 @@ public final class AccountContextImpl: AccountContext { self.cachedGroupCallContexts = AccountGroupCallContextCacheImpl() let cacheStorageBox = self.account.postbox.mediaBox.cacheStorageBox - self.animationCache = AnimationCacheImpl(basePath: self.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + self.animationCache = DCTAnimationCacheImpl(basePath: self.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { return TempBox.shared.tempFile(fileName: "file").path }, updateStorageStats: { path, size in if let pathData = path.data(using: .utf8) { cacheStorageBox.update(id: pathData, size: size) } }) - self.animationRenderer = MultiAnimationRendererImpl() - (self.animationRenderer as? MultiAnimationRendererImpl)?.useYuvA = sharedContext.immediateExperimentalUISettings.compressedEmojiCache + self.animationRenderer = DCTMultiAnimationRendererImpl() + (self.animationRenderer as? DCTMultiAnimationRendererImpl)?.useYuvA = sharedContext.immediateExperimentalUISettings.compressedEmojiCache - let updatedLimitsConfiguration = account.postbox.preferencesView(keys: [PreferencesKeys.limitsConfiguration]) + let updatedLimitsConfiguration = self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.limitsConfiguration)) |> map { preferences -> LimitsConfiguration in - return preferences.values[PreferencesKeys.limitsConfiguration]?.get(LimitsConfiguration.self) ?? LimitsConfiguration.defaultValue + return preferences?.get(LimitsConfiguration.self) ?? LimitsConfiguration.defaultValue } self.currentLimitsConfiguration = Atomic(value: limitsConfiguration) @@ -351,7 +353,7 @@ public final class AccountContextImpl: AccountContext { let _ = currentContentSettings.swap(value) }) - let updatedAppConfiguration = getAppConfiguration(postbox: account.postbox) + let updatedAppConfiguration = getAppConfiguration(engine: self.engine) self.currentAppConfiguration = Atomic(value: appConfiguration) self._appConfiguration.set(.single(appConfiguration) |> then(updatedAppConfiguration)) @@ -376,22 +378,24 @@ public final class AccountContextImpl: AccountContext { }).start() } }) + + let queue = Queue() + self.deviceSpecificContactImportContexts = QueueLocalObject(queue: queue, generate: { + return DeviceSpecificContactImportContexts(queue: queue) + }) let langCode = sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode self.currentCountriesConfiguration = Atomic(value: CountriesConfiguration(countries: loadCountryCodes())) if !temp { let currentCountriesConfiguration = self.currentCountriesConfiguration self.countriesConfigurationDisposable = (self.engine.localization.getCountriesList(accountManager: sharedContext.accountManager, langCode: langCode) - |> deliverOnMainQueue).start(next: { value in - let _ = currentCountriesConfiguration.swap(CountriesConfiguration(countries: value)) + |> deliverOnMainQueue).start(next: { [weak self] value in + let configuration = CountriesConfiguration(countries: value) + let _ = currentCountriesConfiguration.swap(configuration) + self?._countriesConfiguration.set(.single(configuration)) }) } - let queue = Queue() - self.deviceSpecificContactImportContexts = QueueLocalObject(queue: queue, generate: { - return DeviceSpecificContactImportContexts(queue: queue) - }) - if let contactDataManager = sharedContext.contactDataManager { let deviceSpecificContactImportContexts = self.deviceSpecificContactImportContexts self.managedAppSpecificContactsDisposable = (contactDataManager.appSpecificReferences() @@ -496,7 +500,7 @@ public final class AccountContextImpl: AccountContext { guard let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.experimentalUISettings]?.get(ExperimentalUISettings.self) else { return } - (self.animationRenderer as? MultiAnimationRendererImpl)?.useYuvA = settings.compressedEmojiCache + (self.animationRenderer as? DCTMultiAnimationRendererImpl)?.useYuvA = settings.compressedEmojiCache }) } @@ -919,10 +923,10 @@ private final class ChatLocationReplyContextHolderImpl: ChatLocationContextHolde } } -func getAppConfiguration(postbox: Postbox) -> Signal { - return postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) +func getAppConfiguration(engine: TelegramEngine) -> Signal { + return engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) |> map { view -> AppConfiguration in - let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue + let appConfiguration: AppConfiguration = view?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue return appConfiguration } |> distinctUntilChanged diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 3746158718..1c1631c776 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -1687,7 +1687,7 @@ private func extractAccountManagerState(records: AccountRecordsView Bool = { allowExpansion in + if let minimizedContainer = strongSelf.rootController.minimizedContainer, minimizedContainer.isExpanded { + minimizedContainer.collapse() + } else if let topContoller = strongSelf.rootController.topViewController as? AttachmentController { + topContoller.minimizeIfNeeded() + } else if let topContoller = strongSelf.rootController.topViewController as? BrowserScreen { + topContoller.requestMinimize(topEdgeOffset: nil, initialVelocity: nil) + } + + for controller in strongSelf.rootController.viewControllers { + if let controller = controller as? ChatControllerImpl, controller.chatLocation.peerId == chatLocation.peerId, (controller.chatLocation.threadId == nil || controller.chatLocation.threadId == chatLocation.threadId) { + if allowExpansion { + return true + } else { + strongSelf.notificationController.removeItemsWithGroupingKey(firstMessage.id.peerId) + + let chatController = ChatControllerImpl(context: strongSelf.context, chatLocation: chatLocation.asChatLocation, mode: .overlay(strongSelf.rootController)) + let presentationArguments = ChatControllerOverlayPresentationData(expandData: (nil, {})) + chatController.presentationArguments = presentationArguments + (strongSelf.rootController.viewControllers.last as? ViewController)?.present(chatController, in: .window(.root), with: presentationArguments) + return false + } + } + } + + strongSelf.notificationController.removeItemsWithGroupingKey(firstMessage.id.peerId) + + var processed = false + for media in firstMessage.media { + if let action = media as? TelegramMediaAction, case .geoProximityReached = action.action { + strongSelf.context.sharedContext.openLocationScreen(context: strongSelf.context, messageId: firstMessage.id, navigationController: strongSelf.rootController) + processed = true + break + } + } + + if !processed { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf.rootController, context: strongSelf.context, chatLocation: chatLocation)) + } + + return false } - - for controller in strongSelf.rootController.viewControllers { - if let controller = controller as? ChatControllerImpl, controller.chatLocation.peerId == chatLocation.peerId, (controller.chatLocation.threadId == nil || controller.chatLocation.threadId == chatLocation.threadId) { - return true + + if let topController = strongSelf.rootController.topViewController as? ChatControllerImpl { + let didPresentAlert = topController.presentVoiceMessageDiscardAlert(action: { + let _ = proceedAction(false) + }, discardIfVideo: true, performAction: false) + if didPresentAlert { + return false } } - strongSelf.notificationController.removeItemsWithGroupingKey(firstMessage.id.peerId) - - var processed = false - for media in firstMessage.media { - if let action = media as? TelegramMediaAction, case .geoProximityReached = action.action { - strongSelf.context.sharedContext.openLocationScreen(context: strongSelf.context, messageId: firstMessage.id, navigationController: strongSelf.rootController) - processed = true - break - } - } - - if !processed { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: strongSelf.rootController, context: strongSelf.context, chatLocation: chatLocation)) - } + return proceedAction(true) } return false }, expandAction: { expandData in @@ -755,9 +780,9 @@ final class AuthorizedApplicationContext { }) let importableContacts = self.context.sharedContext.contactDataManager?.importable() ?? .single([:]) - let optionalImportableContacts = self.context.account.postbox.preferencesView(keys: [PreferencesKeys.contactsSettings]) + let optionalImportableContacts = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.contactsSettings)) |> mapToSignal { preferences -> Signal<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], NoError> in - let settings: ContactsSettings = preferences.values[PreferencesKeys.contactsSettings]?.get(ContactsSettings.self) ?? .defaultSettings + let settings: ContactsSettings = preferences?.get(ContactsSettings.self) ?? .defaultSettings if settings.synchronizeContacts { return importableContacts } else { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift index 0a9d87e4b2..741427088e 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import Display @@ -14,6 +13,8 @@ extension ChatControllerImpl { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } + let hasSilentPosting = peer.id != self.context.account.peerId + let hasSchedule = self.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat && self.presentationInterfaceState.sendPaidMessageStars == nil legacyMediaEditor( context: self.context, peer: peer, @@ -27,7 +28,24 @@ extension ChatControllerImpl { getCaptionPanelView: { [weak self] in return self?.getCaptionPanelView(isFile: false, hasTimer: false) }, - sendMessagesWithSignals: { [weak self] signals, _, _, isCaptionAbove in + hasSilentPosting: hasSilentPosting, + hasSchedule: hasSchedule, + reminder: peer.id == self.context.account.peerId, + presentSchedulePicker: { [weak self] _, done in + guard let self else { + return + } + self.presentScheduleTimePicker(style: .media, completion: { [weak self] result in + guard let self else { + return + } + done(result.time, result.silentPosting) + if self.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { + self.openScheduledMessages() + } + }) + }, + sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, isCaptionAbove in guard let self else { return } @@ -39,8 +57,8 @@ extension ChatControllerImpl { fromGallery: false, signals: signals, originalMediaReference: file.abstract, - silentPosting: false, - scheduleTime: nil, + silentPosting: silentPosting, + scheduleTime: scheduleTime == 0 ? nil : scheduleTime, replyToSubject: nil, parameters: parameters, getAnimatedTransitionSource: nil, @@ -48,7 +66,8 @@ extension ChatControllerImpl { ) }, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) + c.navigationPresentation = .flatModal + self?.push(c) } ) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerEditSticker.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerEditSticker.swift index 6387cf9113..7b812b9607 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerEditSticker.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerEditSticker.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import Display diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerFrozenAccount.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerFrozenAccount.swift index 5a0366e74a..1152571b03 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerFrozenAccount.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerFrozenAccount.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import Display diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 568d6bf79b..d99dbe3ecd 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 @@ -531,6 +530,8 @@ extension ChatControllerImpl { let initialInterfaceState = contentData.initialInterfaceState contentData.initialInterfaceState = nil + let initialTextInputState = self.initialTextInputState + self.initialTextInputState = nil if !self.didInitializePersistentPeerInterfaceData, let initialPersistentPeerData = contentData.initialPersistentPeerData { self.didInitializePersistentPeerInterfaceData = true @@ -542,6 +543,9 @@ extension ChatControllerImpl { if let initialInterfaceState { interfaceState = initialInterfaceState.interfaceState } + if let initialTextInputState { + interfaceState = interfaceState.withUpdatedComposeInputState(initialTextInputState) + } if let channel = contentData.state.renderedPeer?.peer as? TelegramChannel { if channel.hasBannedPermission(.banSendVoice) != nil && channel.hasBannedPermission(.banSendInstantVideos) != nil { @@ -761,7 +765,14 @@ extension ChatControllerImpl { }) } - let currentAccountPeer = self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) + let currentAccountPeer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> map { peer in return SendAsPeer(peer: peer, subscribers: nil, isPremiumRequired: false) } @@ -796,7 +807,7 @@ extension ChatControllerImpl { } } if !hasAnonymousPeer { - allPeers?.insert(SendAsPeer(peer: channel, subscribers: 0, isPremiumRequired: false), at: 0) + allPeers?.insert(SendAsPeer(peer: EnginePeer(channel), subscribers: 0, isPremiumRequired: false), at: 0) } } else if let channel = peerViewMainPeer(peerView) as? TelegramChannel, case let .broadcast(info) = channel.info, (info.flags.contains(.messagesShouldHaveSignatures) || info.flags.contains(.messagesShouldHaveProfiles)) { allPeers = peers @@ -814,7 +825,7 @@ extension ChatControllerImpl { allPeers?.insert(currentAccountPeer, at: 0) } if !hasAnonymousPeer { - allPeers?.insert(SendAsPeer(peer: channel, subscribers: 0, isPremiumRequired: false), at: 0) + allPeers?.insert(SendAsPeer(peer: EnginePeer(channel), subscribers: 0, isPremiumRequired: false), at: 0) } } else { allPeers = peers.filter { $0.peer.id != peerViewMainPeer(peerView)?.id } @@ -867,6 +878,12 @@ extension ChatControllerImpl { self?.requestLayout(transition: transition) } + var enableSendAnimationV2 = false + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_send_animation_v2"] { + } else { + enableSendAnimationV2 = true + } + self.chatDisplayNode.setupSendActionOnViewUpdate = { [weak self] f, messageCorrelationId in //print("setup layoutActionOnViewTransition") @@ -875,6 +892,11 @@ extension ChatControllerImpl { } self.layoutActionOnViewTransitionAction = f + if !enableSendAnimationV2 { + self.chatDisplayNode.historyNode.pinToTopStableId = nil + } else if !self.chatDisplayNode.historyNode.isStrictlyScrolledToPinToEdgeItem() { + self.chatDisplayNode.historyNode.pinToTopStableId = nil + } self.chatDisplayNode.historyNode.layoutActionOnViewTransition = ({ [weak self] transition in f() if let strongSelf = self, let validLayout = strongSelf.validLayout { @@ -894,8 +916,8 @@ extension ChatControllerImpl { let shouldUseFastMessageSendAnimation = strongSelf.chatDisplayNode.shouldUseFastMessageSendAnimation - strongSelf.chatDisplayNode.containerLayoutUpdated(validLayout, navigationBarHeight: strongSelf.navigationLayout(layout: validLayout).navigationFrame.maxY, transition: .animated(duration: duration, curve: curve), listViewTransaction: { updateSizeAndInsets, _, _, _ in - + strongSelf.chatDisplayNode.containerLayoutUpdated(validLayout, navigationBarHeight: strongSelf.navigationLayout(layout: validLayout).navigationFrame.maxY, transition: .animated(duration: duration, curve: curve), listViewTransaction: { [weak strongSelf] + updateSizeAndInsets, _, _, _ in var options = transition.options let _ = options.insert(.Synchronous) let _ = options.insert(.LowLatency) @@ -925,10 +947,15 @@ extension ChatControllerImpl { insertItems.append(ListViewInsertItem(index: item.index, previousIndex: item.previousIndex, item: item.item, directionHint: item.directionHint == .Down ? .Up : nil)) } - if isScheduledMessages, let insertedIndex = insertedIndex { + if isScheduledMessages, let insertedIndex { scrollToItem = ListViewScrollToItem(index: insertedIndex, position: .visible, animated: true, curve: .Custom(duration: duration, controlPoints.0, controlPoints.1, controlPoints.2, controlPoints.3), directionHint: .Down) } else if transition.historyView.originalView.laterId == nil { scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Custom(duration: duration, controlPoints.0, controlPoints.1, controlPoints.2, controlPoints.3), directionHint: .Up) + if enableSendAnimationV2, Thread.isMainThread, let strongSelf { + if strongSelf.chatDisplayNode.historyNode.isStrictlyScrolledToPinToEdgeItem() { + scrollToItem = nil + } + } } if let maxInsertedItem = maxInsertedItem { @@ -1007,7 +1034,8 @@ extension ChatControllerImpl { } } - let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting ?? false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone) + let effectiveSilentPosting = silentPosting ?? strongSelf.presentationInterfaceState.interfaceState.silentPosting + let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: effectiveSilentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone) var forwardedMessages: [[EnqueueMessage]] = [] var forwardSourcePeerIds = Set() @@ -1172,7 +1200,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, groupId: nil, tags: nil, minDate: nil, maxDate: nil, folderId: nil)))) } } @@ -1962,7 +1990,7 @@ extension ChatControllerImpl { } self.beginDeleteMessagesWithUndo(messageIds: Set(messages.map({ $0.id })), type: .forEveryone) }), - TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}) + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}) ], actionLayout: .vertical ) @@ -2082,9 +2110,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)) } @@ -2161,15 +2189,17 @@ extension ChatControllerImpl { } var invertedMediaAttribute: InvertMediaMessageAttribute? - if let attribute = message.attributes.first(where: { $0 is InvertMediaMessageAttribute }) { - invertedMediaAttribute = attribute as? InvertMediaMessageAttribute - } - - if let mediaCaptionIsAbove = editMessage.mediaCaptionIsAbove { - if mediaCaptionIsAbove { - invertedMediaAttribute = InvertMediaMessageAttribute() - } else { - invertedMediaAttribute = nil + if webpagePreviewAttribute == nil { + if let attribute = message.attributes.first(where: { $0 is InvertMediaMessageAttribute }) { + invertedMediaAttribute = attribute as? InvertMediaMessageAttribute + } + + if let mediaCaptionIsAbove = editMessage.mediaCaptionIsAbove { + if mediaCaptionIsAbove { + invertedMediaAttribute = InvertMediaMessageAttribute() + } else { + invertedMediaAttribute = nil + } } } @@ -2412,7 +2442,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 @@ -3993,7 +4023,7 @@ extension ChatControllerImpl { if !(peer is TelegramGroup || peer is TelegramChannel) { return } - presentAddMembersImpl(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, parentController: strongSelf, groupPeer: peer, selectAddMemberDisposable: strongSelf.selectAddMemberDisposable, addMemberDisposable: strongSelf.addMemberDisposable) + presentAddMembersImpl(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, parentController: strongSelf, groupPeer: EnginePeer(peer), selectAddMemberDisposable: strongSelf.selectAddMemberDisposable, addMemberDisposable: strongSelf.addMemberDisposable) }, presentGigagroupHelp: { [weak self] in if let strongSelf = self { strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_GigagroupDescription, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return true }), in: .current) @@ -4349,7 +4379,7 @@ extension ChatControllerImpl { if let controller = self.context.sharedContext.makePeerInfoController( context: self.context, updatedPresentationData: nil, - peer: peer, + peer: EnginePeer(peer), mode: .gifts, avatarInitiallyExpanded: false, fromChat: false, @@ -4957,7 +4987,7 @@ extension ChatControllerImpl { } let engine = self.context.engine - let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) + let previousPeerCache = Atomic<[PeerId: EnginePeer]>(value: [:]) let activitySpace: PeerActivitySpace? switch self.chatLocation { @@ -4972,9 +5002,9 @@ extension ChatControllerImpl { if let activitySpace = activitySpace, let peerId = peerId { self.peerInputActivitiesDisposable?.dispose() self.peerInputActivitiesDisposable = (self.context.account.peerInputActivities(peerId: activitySpace) - |> mapToSignal { activities -> Signal<[(Peer, PeerInputActivity)], NoError> in + |> mapToSignal { activities -> Signal<[(EnginePeer, PeerInputActivity)], NoError> in var foundAllPeers = true - var cachedResult: [(Peer, PeerInputActivity)] = [] + var cachedResult: [(EnginePeer, PeerInputActivity)] = [] previousPeerCache.with { dict -> Void in for (peerId, activity) in activities { if let peer = dict[peerId] { @@ -4991,13 +5021,13 @@ extension ChatControllerImpl { return engine.data.get(EngineDataMap( activities.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0.0) } )) - |> map { peerMap -> [(Peer, PeerInputActivity)] in - var result: [(Peer, PeerInputActivity)] = [] - var peerCache: [PeerId: Peer] = [:] + |> map { peerMap -> [(EnginePeer, PeerInputActivity)] in + var result: [(EnginePeer, PeerInputActivity)] = [] + var peerCache: [PeerId: EnginePeer] = [:] for (peerId, activity) in activities { if let maybePeer = peerMap[peerId], let peer = maybePeer { - result.append((peer._asPeer(), activity)) - peerCache[peerId] = peer._asPeer() + result.append((peer, activity)) + peerCache[peerId] = peer } } let _ = previousPeerCache.swap(peerCache) @@ -5020,7 +5050,7 @@ extension ChatControllerImpl { peerId: peerId, items: displayActivities.map { item -> ChatTitleComponent.Activities.Item in return ChatTitleComponent.Activities.Item( - peer: EnginePeer(item.0), + peer: item.0, activity: item.1 ) } @@ -5113,56 +5143,68 @@ extension ChatControllerImpl { } } })) + + let isScheduledMessages: Bool + if case .scheduledMessages = self.presentationInterfaceState.subject { + isScheduledMessages = true + } else { + isScheduledMessages = false + } - self.failedMessageEventsDisposable.set((self.context.account.pendingMessageManager.failedMessageEvents(peerId: peerId) + self.failedMessageEventsDisposable.set((self.context.account.pendingMessageManager.failedMessageEvents(peerId: peerId, isScheduled: isScheduledMessages) |> deliverOnMainQueue).startStrict(next: { [weak self] reason in - if let strongSelf = self, strongSelf.currentFailedMessagesAlertController == nil { - let text: String - var title: String? - let moreInfo: Bool - switch reason { - case .flood: - text = strongSelf.presentationData.strings.Conversation_SendMessageErrorFlood - moreInfo = true - case .sendingTooFast: - text = strongSelf.presentationData.strings.Conversation_SendMessageErrorTooFast - title = strongSelf.presentationData.strings.Conversation_SendMessageErrorTooFastTitle - moreInfo = false - case .publicBan: - text = strongSelf.presentationData.strings.Conversation_SendMessageErrorGroupRestricted - moreInfo = true - case .mediaRestricted: - text = strongSelf.restrictedSendingContentsText() - moreInfo = false - case .slowmodeActive: - text = strongSelf.presentationData.strings.Chat_SlowmodeSendError - moreInfo = false - case .tooMuchScheduled: - text = strongSelf.presentationData.strings.Conversation_SendMessageErrorTooMuchScheduled - moreInfo = false - case .voiceMessagesForbidden: - strongSelf.interfaceInteraction?.displayRestrictedInfo(.premiumVoiceMessages, .alert) - return - case .nonPremiumMessagesForbidden: - if let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer { - text = strongSelf.presentationData.strings.Conversation_SendMessageErrorNonPremiumForbidden(EnginePeer(peer).compactDisplayTitle).string - moreInfo = false - } else { - return - } - } - let actions: [TextAlertAction] - if moreInfo { - actions = [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Generic_ErrorMoreInfo, action: { - self?.openPeerMention("spambot", navigation: .chat(textInputState: nil, subject: nil, peekData: nil)) - }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})] - } else { - actions = [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})] - } - let controller = textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: title, text: text, actions: actions) - strongSelf.currentFailedMessagesAlertController = controller - strongSelf.present(controller, in: .window(.root)) + guard let strongSelf = self else { + return } + guard strongSelf.currentFailedMessagesAlertController == nil else { + return + } + + let text: String + var title: String? + let moreInfo: Bool + switch reason { + case .flood: + text = strongSelf.presentationData.strings.Conversation_SendMessageErrorFlood + moreInfo = true + case .sendingTooFast: + text = strongSelf.presentationData.strings.Conversation_SendMessageErrorTooFast + title = strongSelf.presentationData.strings.Conversation_SendMessageErrorTooFastTitle + moreInfo = false + case .publicBan: + text = strongSelf.presentationData.strings.Conversation_SendMessageErrorGroupRestricted + moreInfo = true + case .mediaRestricted: + text = strongSelf.restrictedSendingContentsText() + moreInfo = false + case .slowmodeActive: + text = strongSelf.presentationData.strings.Chat_SlowmodeSendError + moreInfo = false + case .tooMuchScheduled: + text = strongSelf.presentationData.strings.Conversation_SendMessageErrorTooMuchScheduled + moreInfo = false + case .voiceMessagesForbidden: + strongSelf.interfaceInteraction?.displayRestrictedInfo(.premiumVoiceMessages, .alert) + return + case .nonPremiumMessagesForbidden: + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer { + text = strongSelf.presentationData.strings.Conversation_SendMessageErrorNonPremiumForbidden(EnginePeer(peer).compactDisplayTitle).string + moreInfo = false + } else { + return + } + } + let actions: [TextAlertAction] + if moreInfo { + actions = [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Generic_ErrorMoreInfo, action: { + self?.openPeerMention("spambot", navigation: .chat(textInputState: nil, subject: nil, peekData: nil)) + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_OK, action: {})] + } else { + actions = [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})] + } + let controller = textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: title, text: text, actions: actions) + strongSelf.currentFailedMessagesAlertController = controller + strongSelf.present(controller, in: .window(.root)) })) self.sentPeerMediaMessageEventsDisposable.dispose() diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift index 89b1b723d9..57ba2caca7 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 @@ -132,7 +131,7 @@ extension ChatControllerImpl { } var resumeData: AudioRecorderResumeData? - if let existingDraft, let path = self.context.account.postbox.mediaBox.completedResourcePath(existingDraft.resource), let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let recorderResumeData = existingDraft.resumeData { + if let existingDraft, let path = self.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(existingDraft.resource.id)), let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedIfSafe]), let recorderResumeData = existingDraft.resumeData { resumeData = AudioRecorderResumeData(compressedData: compressedData, resumeData: recorderResumeData) } @@ -183,7 +182,7 @@ extension ChatControllerImpl { viewOnceAvailable: viewOnceAvailable, inputPanelFrame: (currentInputPanelFrame, self.chatDisplayNode.inputNode != nil), chatNode: self.chatDisplayNode.historyNode, - completion: { [weak self] message, silentPosting, scheduleTime in + completion: { [weak self] message, silentPosting, scheduleTime, repeatPeriod in guard let self, let videoController = self.videoRecorderValue else { return } @@ -231,14 +230,8 @@ extension ChatControllerImpl { }, usedCorrelationId ? correlationId : nil) let messages = [message] - let transformedMessages: [EnqueueMessage] - if let silentPosting { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting) - } else if let scheduleTime { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime) - } else { - transformedMessages = self.transformEnqueueMessages(messages) - } + let effectiveSilentPosting = silentPosting ?? self.presentationInterfaceState.interfaceState.silentPosting + let transformedMessages = self.transformEnqueueMessages(messages, silentPosting: effectiveSilentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod) self.sendMessages(transformedMessages) } @@ -312,7 +305,7 @@ extension ChatControllerImpl { } else if let waveform = data.waveform { if resource == nil { resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: Int64(data.compressedData.count)) - strongSelf.context.account.postbox.mediaBox.storeResourceData(resource!.id, data: data.compressedData) + strongSelf.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource!.id), data: data.compressedData) } let audioWaveform: AudioWaveform @@ -362,7 +355,7 @@ extension ChatControllerImpl { let randomId = Int64.random(in: Int64.min ... Int64.max) let resource = LocalFileMediaResource(fileId: randomId) - strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data.compressedData) + strongSelf.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data.compressedData) let waveformBuffer: Data? = data.waveform @@ -478,6 +471,9 @@ extension ChatControllerImpl { if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState { self.dismissMediaRecorder(pause ? .pause : .preview) } else { + if case .video = self.presentationInterfaceState.interfaceState.mediaDraftState { + return + } self.videoRecorder.set(.single(nil)) } } @@ -496,6 +492,10 @@ extension ChatControllerImpl { }.updatedInterfaceState { $0.withUpdatedMediaDraftState(nil) } }) } else { + if case .video = self.presentationInterfaceState.interfaceState.mediaDraftState { + return + } + let proceed = { self.withAudioRecorder(resuming: true, { audioRecorder in audioRecorder.resume() @@ -607,7 +607,11 @@ extension ChatControllerImpl { } } - let location = CGRect(origin: CGPoint(x: screenWidth - layout.safeInsets.right - 50.0, y: layout.size.height - insets.bottom - 128.0), size: CGSize()) + var sideOffset: CGFloat = 18.0 + if let inputHeight = layout.inputHeight, inputHeight > 0.0 { + sideOffset = 0.0 + } + let location = CGRect(origin: CGPoint(x: screenWidth - layout.safeInsets.right - 50.0 - sideOffset, y: layout.size.height - insets.bottom - 128.0), size: CGSize()) let tooltipController = TooltipScreen( account: self.context.account, @@ -737,7 +741,7 @@ extension ChatControllerImpl { let randomId = Int64.random(in: Int64.min ... Int64.max) let tempPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).ogg" resource = LocalFileAudioMediaResource(randomId: randomId, path: tempPath, trimRange: trimRange) - self.context.account.postbox.mediaBox.moveResourceData(audio.resource.id, toTempPath: tempPath) + self.context.engine.resources.moveResourceData(id: EngineMediaResource.Id(audio.resource.id), toTempPath: tempPath) waveform = waveform.subwaveform(from: trimRange.lowerBound / Double(audio.duration), to: trimRange.upperBound / Double(audio.duration)) finalDuration = Int(trimRange.upperBound - trimRange.lowerBound) } else { @@ -748,14 +752,8 @@ extension ChatControllerImpl { let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: finalDuration, title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] - let transformedMessages: [EnqueueMessage] - if let silentPosting = silentPosting { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting, postpone: postpone) - } else if let scheduleTime = scheduleTime { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone) - } else { - transformedMessages = self.transformEnqueueMessages(messages) - } + let effectiveSilentPosting = silentPosting ?? self.presentationInterfaceState.interfaceState.silentPosting + let transformedMessages = self.transformEnqueueMessages(messages, silentPosting: effectiveSilentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone) guard let peerId = self.chatLocation.peerId else { return @@ -770,7 +768,10 @@ extension ChatControllerImpl { donateSendMessageIntent(account: self.context.account, sharedContext: self.context.sharedContext, intentContext: .chat, peerIds: [peerId]) case .video: - self.videoRecorderValue?.sendVideoRecording(silentPosting: silentPosting, scheduleTime: scheduleTime, messageEffect: messageEffect) + guard let videoRecorderValue = self.videoRecorderValue else { + return + } + videoRecorderValue.sendVideoRecording(silentPosting: silentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, messageEffect: messageEffect) } } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift index b08831c0d5..4777dc06af 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 @@ -439,7 +438,7 @@ extension ChatControllerImpl { if peer.restrictionText(platform: "ios", contentSettings: self.context.currentContentSettings.with { $0 }) == nil && !self.presentationInterfaceState.isNotAccessible { if peer.id == self.context.account.peerId { - if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer, let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { + if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer, let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), mode: .generic, avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { self.effectiveNavigationController?.pushViewController(infoController) } } else { @@ -459,17 +458,17 @@ extension ChatControllerImpl { default: mode = .generic } - if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer, mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: true, requestsContext: self.contentData?.inviteRequestsContext) { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: true, requestsContext: self.contentData?.inviteRequestsContext) { self.effectiveNavigationController?.pushViewController(infoController) } } - + let _ = self.dismissPreviewing?(false) } })) case .replyThread: if let peer = self.presentationInterfaceState.renderedPeer?.peer, case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.peerId == self.context.account.peerId { - if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer, mode: .forumTopic(thread: replyThreadMessage), avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), mode: .forumTopic(thread: replyThreadMessage), avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { self.effectiveNavigationController?.pushViewController(infoController) } } else if let monoforumPeer = self.presentationInterfaceState.renderedPeer?.peer, case let .replyThread(replyThreadMessage) = self.chatLocation, monoforumPeer.isMonoForum { @@ -484,13 +483,13 @@ extension ChatControllerImpl { guard let self else { return } - if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer._asPeer(), mode: .monoforum(monoforumPeer.id), avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: peer, mode: .monoforum(monoforumPeer.id), avatarInitiallyExpanded: false, fromChat: true, requestsContext: nil) { self.effectiveNavigationController?.pushViewController(infoController) } } } } else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.isForumOrMonoForum, case let .replyThread(message) = self.chatLocation { - if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: channel, mode: .forumTopic(thread: message), avatarInitiallyExpanded: false, fromChat: true, requestsContext: self.contentData?.inviteRequestsContext) { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(channel), mode: .forumTopic(thread: message), avatarInitiallyExpanded: false, fromChat: true, requestsContext: self.contentData?.inviteRequestsContext) { self.effectiveNavigationController?.pushViewController(infoController) } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenBankCardContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenBankCardContextMenu.swift index 0b4e34a494..ecf806cfc1 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenBankCardContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenBankCardContextMenu.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import Display @@ -31,15 +30,10 @@ extension ChatControllerImpl { } } - let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer - let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture - - let source: ContextContentSource -// if let location = location { -// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) -// } else { - source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) -// } + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = params.gesture + let gesture: ContextGesture? = nil + + let source: ContextContentSource = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) params.progress?.set(.single(true)) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenCommandContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenCommandContextMenu.swift index bddb29baca..4a4a8e8081 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenCommandContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenCommandContextMenu.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import Display @@ -31,15 +30,10 @@ extension ChatControllerImpl { } } - let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer - let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = params.gesture + let gesture: ContextGesture? = nil - let source: ContextContentSource -// if let location = location { -// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) -// } else { - source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) -// } + let source: ContextContentSource = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) var items: [ContextMenuItem] = [] diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenDateContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenDateContextMenu.swift index 6eb88e48b4..c1aadf5f56 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenDateContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenDateContextMenu.swift @@ -35,15 +35,10 @@ extension ChatControllerImpl: EKEventEditViewDelegate { } } - let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer - let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture - - let source: ContextContentSource -// if let location = location { -// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) -// } else { - source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) -// } + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = params.gesture + let gesture: ContextGesture? = nil + + let source: ContextContentSource = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) var items: [ContextMenuItem] = [] items.append( diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenDocumentScanner.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenDocumentScanner.swift index d90e71ce45..da32ec1cf0 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenDocumentScanner.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenDocumentScanner.swift @@ -49,12 +49,12 @@ extension ChatControllerImpl: VNDocumentCameraViewControllerDelegate { arc4random_buf(&randomId, 8) let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) var previewResource: LocalFileMediaResource? if let image = generatePdfPreviewImage(data: data, size: CGSize(width: 256, height: 256.0)), let jpegData = image.jpegData(compressionQuality: 0.5) { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: jpegData, synchronous: true) + self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: jpegData, synchronous: true) previewResource = resource } @@ -65,7 +65,7 @@ extension ChatControllerImpl: VNDocumentCameraViewControllerDelegate { let image = scan.imageOfPage(at: i) if let data = image.jpegData(compressionQuality: 0.87) { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) var fileTitle = title if scan.pageCount > 1 { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenHashtagContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenHashtagContextMenu.swift index 4cdca9f8bb..caee4eac16 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenHashtagContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenHashtagContextMenu.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import Display @@ -31,15 +30,10 @@ extension ChatControllerImpl { } } - let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer - let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = params.gesture + let gesture: ContextGesture? = nil - let source: ContextContentSource -// if let location = location { -// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) -// } else { - source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) -// } + let source: ContextContentSource = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) var items: [ContextMenuItem] = [] diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift index 8831a82eb1..0b6afc013c 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import Display @@ -141,15 +140,10 @@ extension ChatControllerImpl { openText = self.presentationData.strings.Conversation_FileOpenIn } - let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer - let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = params.gesture + let gesture: ContextGesture? = nil - let source: ContextContentSource -// if let location = location { -// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) -// } else { - source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) -// } + let source: ContextContentSource = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) var items: [ContextMenuItem] = [] diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index 0080cb5043..08750090f1 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -18,6 +18,7 @@ import TooltipUI import TopMessageReactions import TelegramNotices import PresentationDataUtils +import ChatPresentationInterfaceState extension ChatControllerImpl { func openMessageContextMenu(message: Message, selectAll: Bool, node: ASDisplayNode, frame: CGRect, anyRecognizer: UIGestureRecognizer?, location: CGPoint?) -> Void { @@ -45,13 +46,15 @@ extension ChatControllerImpl { guard let topMessage = messages.first else { return } - + + let canBypassReactionRestrictions = canBypassRestrictions(chatPresentationInterfaceState: self.presentationInterfaceState) + let _ = combineLatest(queue: .mainQueue(), self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: self.presentationInterfaceState, context: self.context, messages: updatedMessages, controllerInteraction: self.controllerInteraction, selectAll: selectAll, interfaceInteraction: self.interfaceInteraction, messageNode: node as? ChatMessageItemView), - peerMessageAllowedReactions(context: self.context, message: topMessage), + peerMessageAllowedReactions(context: self.context, message: topMessage, ignoreDefault: canBypassReactionRestrictions), peerMessageSelectedReactions(context: self.context, message: topMessage), - topMessageReactions(context: self.context, message: topMessage, subPeerId: self.chatLocation.threadId.flatMap(EnginePeer.Id.init)), + topMessageReactions(context: self.context, message: topMessage, subPeerId: self.chatLocation.threadId.flatMap(EnginePeer.Id.init), ignoreDefault: canBypassReactionRestrictions), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: self.context.sharedContext.accountManager) ).startStandalone(next: { [weak self] peer, actions, allowedReactionsAndStars, selectedReactions, topReactions, chatTextSelectionTips in guard let self else { @@ -370,6 +373,17 @@ extension ChatControllerImpl { controller?.view.endEditing(true) if case .stars = chosenUpdatedReaction.reaction { + if !canSendReactionsToChat(self.presentationInterfaceState) { + if let controller { + controller.dismiss(completion: { [weak self] in + self?.displaySendReactionRestrictedToast() + }) + } else { + self.displaySendReactionRestrictedToast() + } + return + } + if isLarge { if let controller { controller.dismiss(completion: { [weak self] in @@ -505,6 +519,17 @@ extension ChatControllerImpl { isFirst = !currentReactions.contains(where: { $0.value == chosenReaction }) } + if removedReaction == nil && !canSendReactionsToChat(self.presentationInterfaceState) { + if let controller { + controller.dismiss(completion: { [weak self] in + self?.displaySendReactionRestrictedToast() + }) + } else { + self.displaySendReactionRestrictedToast() + } + return + } + if message.areReactionsTags(accountPeerId: self.context.account.peerId) { if removedReaction == nil, !topReactions.contains(where: { $0.reaction.rawValue == chosenReaction }) { if !self.presentationInterfaceState.isPremium { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift index 0aa529ccfb..2a69b0af4a 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 @@ -180,13 +179,21 @@ extension ChatControllerImpl { if let messageId = fromMessage?.id { peerSignal = loadedPeerFromMessage(account: self.context.account, peerId: peer.id, messageId: messageId) } else { - peerSignal = self.context.account.postbox.loadedPeerWithId(peer.id) |> map(Optional.init) + peerSignal = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + |> map { Optional($0._asPeer()) } } self.navigationActionDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] peer in if let strongSelf = self, let peer = peer { var mode: PeerInfoControllerMode = .generic - if let _ = fromMessage, let chatPeerId = chatPeerId { - mode = .group(chatPeerId) + if let messageId = fromMessage?.id, chatPeerId != nil { + mode = .group(sourceMessageId: messageId) } if let fromReactionMessageId = fromReactionMessageId { mode = .reaction(fromReactionMessageId) @@ -208,7 +215,7 @@ extension ChatControllerImpl { if let validLayout = strongSelf.validLayout, validLayout.deviceMetrics.type == .tablet { expandAvatar = false } - if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: false, requestsContext: nil) { + if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: false, requestsContext: nil) { strongSelf.effectiveNavigationController?.pushViewController(infoController) } } @@ -348,8 +355,8 @@ extension ChatControllerImpl { guard let self, let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer else { return } - - guard let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + + guard let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: EnginePeer(peer), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { return } (self.navigationController as? NavigationController)?.pushViewController(controller) @@ -410,39 +417,45 @@ extension ChatControllerImpl { } }))) } else { - items.append(.action(ContextMenuActionItem(text: strings.Chat_CreateTopic, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] action in - guard let self else { - return - } - - action.dismissWithResult(.default) - - let controller = ForumCreateTopicScreen(context: self.context, peerId: peerId, mode: .create) - controller.navigationPresentation = .modal - - controller.completion = { [weak self, weak controller] title, fileId, iconColor, _ in - controller?.isInProgress = true - controller?.view.endEditing(true) - + var canCreateTopics = false + if let peer = self.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramUser, let botInfo = peer.botInfo, botInfo.flags.contains(.forumManagedByUser) { + canCreateTopics = true + } + if canCreateTopics { + items.append(.action(ContextMenuActionItem(text: strings.Chat_CreateTopic, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] action in guard let self else { return } - let _ = (self.context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: iconColor, iconFileId: fileId) - |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] topicId in + action.dismissWithResult(.default) + + let controller = ForumCreateTopicScreen(context: self.context, peerId: peerId, mode: .create) + controller.navigationPresentation = .modal + + controller.completion = { [weak self, weak controller] title, fileId, iconColor, _ in + controller?.isInProgress = true + controller?.view.endEditing(true) + guard let self else { return } - self.updateChatLocationThread(threadId: topicId) - controller?.dismiss() - }, error: { _ in - controller?.isInProgress = false - }) - } - self.push(controller) - }))) + + let _ = (self.context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: iconColor, iconFileId: fileId) + |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] topicId in + guard let self else { + return + } + self.updateChatLocationThread(threadId: topicId) + controller?.dismiss() + }, error: { _ in + controller?.isInProgress = false + }) + } + self.push(controller) + }))) + } } let presentationData = self.presentationData diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift index 511a8b1711..b7dd5f35fc 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import SwiftSignalKit -import Postbox import TelegramCore import AsyncDisplayKit import Display @@ -35,15 +34,10 @@ extension ChatControllerImpl: MFMessageComposeViewControllerDelegate { } } - let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer - let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = params.gesture + let gesture: ContextGesture? = nil - let source: ContextContentSource -// if let location = location { -// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) -// } else { - source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) -// } + let source: ContextContentSource = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) params.progress?.set(.single(true)) @@ -79,7 +73,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/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/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/Chat/ChatControllerOpenTimecodeContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTimecodeContextMenu.swift index 086d87ae1a..6d1870731e 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTimecodeContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenTimecodeContextMenu.swift @@ -31,15 +31,10 @@ extension ChatControllerImpl { } } - let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer - let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = params.gesture + let gesture: ContextGesture? = nil - let source: ContextContentSource -// if let location = location { -// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) -// } else { - source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) -// } + let source: ContextContentSource = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) var items: [ContextMenuItem] = [] 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/Chat/ChatControllerOpenUsernameContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift index 61b871b9bd..60c97a35bc 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift @@ -19,15 +19,10 @@ extension ChatControllerImpl { return } - let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer - let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = params.gesture + let gesture: ContextGesture? = nil - let source: ContextContentSource -// if let location = location { -// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) -// } else { - source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) -// } + let source: ContextContentSource = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) params.progress?.set(.single(true)) 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/ChatControllerOpenWebApp.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift index 42fd82e66f..3c22fcaffb 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenWebApp.swift @@ -482,7 +482,7 @@ public extension ChatControllerImpl { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: subject, keepStack: .always, peekData: peekData)) case .info: if peer.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) == nil { - if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { navigationController.pushViewController(infoController) } } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift index 5d489d7069..72f91cd13c 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerPaste.swift @@ -10,7 +10,6 @@ import MediaPickerUI import MediaPasteboardUI import LegacyMediaPickerUI import MediaEditor -import ChatEntityKeyboardInputNode extension ChatControllerImpl { func displayPasteMenu(_ subjects: [MediaPickerScreenImpl.Subject.Media]) { @@ -24,6 +23,7 @@ extension ChatControllerImpl { if strongSelf.presentationInterfaceState.interfaceState.postSuggestionState != nil { enableMultiselection = false } + let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText strongSelf.chatDisplayNode.dismissInput() let controller = mediaPasteboardScreen( @@ -33,18 +33,21 @@ extension ChatControllerImpl { subjects: subjects, presentMediaPicker: { [weak self] subject, saveEditedPhotos, bannedSendPhotos, bannedSendVideos, present in if let strongSelf = self { - strongSelf.presentMediaPicker(subject: subject, saveEditedPhotos: saveEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, enableMultiselection: enableMultiselection, present: present, updateMediaPickerContext: { _ in }, completion: { [weak self] fromGallery, signals, silentPosting, scheduleTime, parameters, getAnimatedTransitionSource, completion in + strongSelf.presentMediaPicker(subject: subject, saveEditedPhotos: saveEditedPhotos, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, enableMultiselection: enableMultiselection, present: { controller, mediaPickerContext in + if !inputText.string.isEmpty { + mediaPickerContext?.setCaption(inputText) + } + present(controller, mediaPickerContext) + }, updateMediaPickerContext: { _ in }, completion: { [weak self] fromGallery, signals, silentPosting, scheduleTime, parameters, getAnimatedTransitionSource, completion in + if !inputText.string.isEmpty { + self?.clearInputText() + } self?.enqueueMediaMessages(fromGallery: fromGallery, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) }) } }, getSourceRect: nil, - makeEntityInputView: { [weak self] in - guard let self else { - return nil - } - return EntityInputView(context: self.context, isDark: false, areCustomEmojiEnabled: self.presentationInterfaceState.customEmojiAvailable) - } + customEmojiAvailable: strongSelf.presentationInterfaceState.customEmojiAvailable ) controller.navigationPresentation = .flatModal strongSelf.push(controller) @@ -103,7 +106,7 @@ extension ChatControllerImpl { self.enqueueMediaMessageDisposable.set((convertToWebP(image: image, targetSize: size, targetBoundingSize: size, quality: 0.9) |> deliverOnMainQueue).startStrict(next: { [weak self] data in if let strongSelf = self, !data.isEmpty { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - strongSelf.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) + strongSelf.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) var fileAttributes: [TelegramMediaFileAttribute] = [] fileAttributes.append(.FileName(fileName: "sticker.webp")) @@ -251,7 +254,7 @@ extension ChatControllerImpl { let previewRepresentations: [TelegramMediaImageRepresentation] = [] let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - self.context.account.postbox.mediaBox.copyResourceData(resource.id, fromTempPath: path) + self.context.engine.resources.copyResourceData(id: EngineMediaResource.Id(resource.id), fromTempPath: path) let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/webm", size: 0, attributes: fileAttributes, alternativeRepresentations: []) self.enqueueStickerFile(file) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift index 2f485b8010..63900b1530 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerReport.swift @@ -1,6 +1,5 @@ import Foundation import UIKit -import Postbox import SwiftSignalKit import Display import AsyncDisplayKit @@ -18,7 +17,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..3fd6ca7e9f 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerThemeManagement.swift @@ -1,6 +1,5 @@ import Foundation import UIKit -import Postbox import SwiftSignalKit import Display import AsyncDisplayKit @@ -18,7 +17,6 @@ import AccountContext import TelegramStringFormatting import OverlayStatusController import DeviceLocationManager -import ShareController import UrlEscaping import ContextUI import AlertUI diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift index f2ddc3926b..8e37932d93 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerToasts.swift @@ -8,8 +8,85 @@ import Display import UndoUI import AccountContext import ChatControllerInteraction +import TelegramStringFormatting extension ChatControllerImpl { + func displaySendReactionRestrictedToast() { + self.controllerInteraction?.presentControllerInCurrent(UndoOverlayController( + presentationData: self.presentationData, + content: .banned(text: self.presentationData.strings.Chat_SendReactionRestricted), + elevatedLayout: false, + position: .bottom, + animateInAsReplacement: false, + action: { _ in return true } + ), nil) + } + + func displayPollRestrictedToast(messageId: EngineMessage.Id) { + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Messages.Message(id: messageId), + TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] message, peer in + guard let self, let message, let peer else { + return + } + let peerName = peer.compactDisplayTitle + guard let poll = message.media.first(where: { $0 is TelegramMediaPoll }) as? TelegramMediaPoll else { + return + } + + var accountCountry = "" + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let country = data["phone_country_iso2"] as? String { + accountCountry = (country) + } + var text = "" + if !poll.countries.isEmpty && !poll.countries.contains(accountCountry) { + let locale = localeWithStrings(self.presentationData.strings) + let countryNames = poll.countries.map { id in + if id == "FT" { + return "Fragment" + } else if let countryName = locale.localizedString(forRegionCode: id) { + return countryName + } else { + return id + } + } + var countries: String = "" + if countryNames.count == 1, let country = countryNames.first { + countries = "**\(country)**" + } else { + for i in 0 ..< countryNames.count { + countries.append("**\(countryNames[i])**") + if i == countryNames.count - 2 { + countries.append(self.presentationData.strings.Chat_Poll_Restriction_Country_CountriesLastDelimiter) + } else if i < countryNames.count - 2 { + countries.append(self.presentationData.strings.Chat_Poll_Restriction_Country_CountriesDelimiter) + } + } + } + if poll.restrictToSubscribers { + text = self.presentationData.strings.Chat_Poll_Restriction_SubscribersCountry(peerName, countries).string + } else { + text = self.presentationData.strings.Chat_Poll_Restriction_Country(countries).string + } + } else { + if case let .channel(channel) = peer, case .member = channel.participationStatus { + text = self.presentationData.strings.Chat_Poll_Restriction_Subscribers_TimeLimit + } else { + text = self.presentationData.strings.Chat_Poll_Restriction_Subscribers(peerName).string + } + } + let controller = UndoOverlayController( + presentationData: self.presentationData, + content: .banned(text: text), + position: .bottom, + action: { _ in return true } + ) + self.controllerInteraction?.presentControllerInCurrent(controller, nil) + }) + } + func displayPostedScheduledMessagesToast(ids: [EngineMessage.Id]) { let timestamp = CFAbsoluteTimeGetCurrent() if self.lastPostedScheduledMessagesToastTimestamp + 0.4 >= timestamp { diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index d09670712e..1d59b973cb 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -27,6 +27,17 @@ private enum OptionsId: Hashable { case link } +private func chatLocationMatchesDestination(_ chatLocation: ChatLocation, peerId: EnginePeer.Id, threadId: Int64?) -> Bool { + switch chatLocation { + case let .peer(id): + return id == peerId && threadId == nil + case let .replyThread(replyThreadMessage): + return replyThreadMessage.peerId == peerId && replyThreadMessage.threadId == threadId + case .customChatContents: + return false + } +} + private func presentChatInputOptions(selfController: ChatControllerImpl, sourceView: UIView, initialId: OptionsId) { var getContextController: (() -> ContextController?)? @@ -147,10 +158,8 @@ private func chatForwardOptions(selfController: ChatControllerImpl, sourceView: } var isDice = false - var isMusic = false for media in message.media { if let media = media as? TelegramMediaFile, media.isMusic { - isMusic = true if !message.text.isEmpty { hasCaptions = true } @@ -164,7 +173,7 @@ private func chatForwardOptions(selfController: ChatControllerImpl, sourceView: hasPaid = true } } - if !isDice && !isMusic { + if !isDice { hasOther = true } } @@ -554,85 +563,95 @@ 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 + + var isPinnedMessages = false + if case .pinnedMessages = selfController.presentationInterfaceState.subject { + isPinnedMessages = true + } + + if chatLocationMatchesDestination(selfController.chatLocation, peerId: peerId, threadId: threadId), 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) }) } @@ -640,7 +659,7 @@ func moveReplyToChat(selfController: ChatControllerImpl, peerId: EnginePeer.Id, if let navigationController = selfController.effectiveNavigationController { for controller in navigationController.viewControllers { if let maybeChat = controller as? ChatControllerImpl { - if case .peer(peerId) = maybeChat.chatLocation { + if chatLocationMatchesDestination(maybeChat.chatLocation, peerId: peerId, threadId: threadId) { var isChatPinnedMessages = false if case .pinnedMessages = maybeChat.presentationInterfaceState.subject { isChatPinnedMessages = true diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift index acf7b004a7..74e4e93605 100644 --- a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -28,7 +28,7 @@ func updateChatPresentationInterfaceStateImpl( completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void ) { var transition = transition - if !selfController.didAppear { + if !selfController.enableAnimations { transition = .immediate } @@ -94,7 +94,7 @@ func updateChatPresentationInterfaceStateImpl( guard let selfController, value else { return } - selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_Cancel, action: { + selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .genericAction, title: selfController.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_OK, action: { [weak selfController] in guard let selfController else { return @@ -151,7 +151,7 @@ func updateChatPresentationInterfaceStateImpl( case .generic: break case let .inlineBotLocationRequest(peerId): - selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_Cancel, action: { [weak selfController] in + selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .genericAction, title: selfController.presentationData.strings.Common_Cancel, action: { [weak selfController] in guard let selfController else { return } 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 3269558e13..68457c5b26 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 @@ -253,6 +252,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let context: AccountContext public internal(set) var chatLocation: ChatLocation public internal(set) var subject: ChatControllerSubject? + var initialTextInputState: ChatTextInputState? var botStart: ChatControllerInitialBotStart? var attachBotStart: ChatControllerInitialAttachBotStart? @@ -387,7 +387,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var chatUnreadMentionCountDisposable: Disposable? var peerInputActivitiesDisposable: Disposable? - var peerInputActivitiesPromise = Promise<[(Peer, PeerInputActivity)]>() + var peerInputActivitiesPromise = Promise<[(EnginePeer, PeerInputActivity)]>() var interactiveEmojiSyncDisposable = MetaDisposable() var recentlyUsedInlineBotsValue: [Peer] = [] @@ -459,6 +459,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var willAppear = false var didAppear = false + var enableAnimations = false var scheduledActivateInput: ChatControllerActivateInput? var raiseToListen: RaiseToListenManager? @@ -639,10 +640,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G botAppStart: ChatControllerInitialBotAppStart? = nil, mode: ChatControllerPresentationMode = .standard(.default), peekData: ChatPeekTimeout? = nil, - peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], customChatNavigationStack: [EnginePeer.Id]? = nil, + initialTextInputState: ChatTextInputState? = nil, params: ChatControllerParams? = nil ) { self.initTimestamp = CFAbsoluteTimeGetCurrent() @@ -655,6 +656,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.chatLocation = chatLocation self.chatLocationContextHolder = chatLocationContextHolder self.subject = subject + self.initialTextInputState = initialTextInputState self.botStart = botStart self.attachBotStart = attachBotStart self.botAppStart = botAppStart @@ -690,7 +692,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.stickerSettings = ChatInterfaceStickerSettings() - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, preferredGlassType: .default, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: mode, chatLocation: chatLocation, subject: subject, peerNearbyData: peerNearbyData, greetingData: context.prefetchManager?.preloadedGreetingSticker, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, preferredGlassType: .default, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: mode, chatLocation: chatLocation, subject: subject, greetingData: context.prefetchManager?.preloadedGreetingSticker, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) if case let .customChatContents(customChatContents) = subject { switch customChatContents.kind { @@ -1406,14 +1408,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) } @@ -1744,7 +1750,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let _ = (peerMessageAllowedReactions(context: strongSelf.context, message: message) + let _ = (peerMessageAllowedReactions(context: strongSelf.context, message: message, ignoreDefault: canBypassRestrictions(chatPresentationInterfaceState: strongSelf.presentationInterfaceState)) |> deliverOnMainQueue).startStandalone(next: { allowedReactions, _ in guard let strongSelf = self else { return @@ -1788,6 +1794,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if case .stars = chosenReaction { + if !canSendReactionsToChat(strongSelf.presentationInterfaceState) { + strongSelf.displaySendReactionRestrictedToast() + return + } + if strongSelf.selectPollOptionFeedback == nil { strongSelf.selectPollOptionFeedback = HapticFeedback() } @@ -1954,6 +1965,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if removedReaction == nil { + if !canSendReactionsToChat(strongSelf.presentationInterfaceState) { + strongSelf.displaySendReactionRestrictedToast() + return + } + if !canAddMessageReactions(message: message) { itemNode.openMessageContextMenu() return @@ -2278,87 +2294,203 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf else { return } - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in - var current = current - current = current.updatedInterfaceState { interfaceState in - var interfaceState = interfaceState - interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) - if clearInput { - interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) - } - return interfaceState - }.updatedInputMode { current in - if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { - return .media(mode: mode, expanded: nil, focused: focused) - } - return current - } - - return current - }) - } - }, shouldAnimateMessageTransition ? correlationId : nil) - if shouldAnimateMessageTransition { - if let sourceNode = sourceView?.asyncdisplaykit_node as? ChatMediaInputStickerGridItemNode { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: { - guard let strongSelf = self else { - return - } - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in - var current = current - current = current.updatedInputMode { current in - if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { - return .media(mode: mode, expanded: nil, focused: focused) - } - return current + let addToTransitionNodeIfNeeded: () -> Void = { + guard let strongSelf = self else { + return + } + if shouldAnimateMessageTransition { + if let sourceNode = sourceView?.asyncdisplaykit_node as? ChatMediaInputStickerGridItemNode { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: { + guard let strongSelf = self else { + return } - - return current - }) - }) - } else if let sourceNode = sourceView?.asyncdisplaykit_node as? HorizontalStickerGridItemNode { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .mediaPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: {}) - } else if let sourceNode = sourceView?.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .emptyPanel(itemNode: sourceNode), replyPanel: nil), initiated: {}) - } else if let sourceLayer, let sourceView, let sourceRect { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .universal(sourceContainerView: sourceView, sourceRect: sourceRect, sourceLayer: sourceLayer), replyPanel: replyPanel), initiated: { - guard let strongSelf = self else { - return - } - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in - var current = current - current = current.updatedInputMode { current in - if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { - return .media(mode: mode, expanded: nil, focused: focused) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current } + return current - } - - return current + }) }) - }) + } else if let sourceNode = sourceView?.asyncdisplaykit_node as? HorizontalStickerGridItemNode { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .mediaPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: {}) + } else if let sourceNode = sourceView?.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .emptyPanel(itemNode: sourceNode), replyPanel: nil), initiated: {}) + } else if let sourceLayer, let sourceView, let sourceRect { + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .universal(sourceContainerView: sourceView, sourceRect: sourceRect, sourceLayer: sourceLayer), replyPanel: replyPanel), initiated: { + guard let strongSelf = self else { + return + } + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current + } + + return current + }) + }) + } } } - let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: fileReference.abstract, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)] + let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + + let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: fileReference.abstract, threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)] if silentPosting { + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInterfaceState { interfaceState in + var interfaceState = interfaceState + interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) + if clearInput { + interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) + } + return interfaceState + }.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current + } + + return current + }) + } + }, shouldAnimateMessageTransition ? correlationId : nil) + + addToTransitionNodeIfNeeded() let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting, postpone: postpone) strongSelf.sendMessages(transformedMessages) } else if schedule { - strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime, repeatPeriod in + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { - let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone) + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInterfaceState { interfaceState in + var interfaceState = interfaceState + interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) + if clearInput { + interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) + } + return interfaceState + }.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current + } + + return current + }) + } + }, shouldAnimateMessageTransition ? correlationId : nil) + + strongSelf.presentScheduleTimePicker(completion: { [weak self] result in + if let strongSelf = self { + let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod, postpone: postpone) strongSelf.sendMessages(transformedMessages) } }) } else { - let transformedMessages = strongSelf.transformEnqueueMessages(messages, postpone: postpone) - strongSelf.sendMessages(transformedMessages) + let messages = strongSelf.transformEnqueueMessages(messages, postpone: postpone) + + var targetThreadId: Int64? + var clearMainThreadForward = false + if strongSelf.chatLocation.threadId == nil, let user = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum), botInfo.flags.contains(.forumManagedByUser) { + if let message = messages.first { + switch message { + case let .message(_, _, _, _, _, replyToMessageId, _, _, _, _): + if let _ = replyToMessageId { + if let replyMessage = strongSelf.presentationInterfaceState.replyMessage { + targetThreadId = replyMessage.threadId + } + } else { + targetThreadId = EngineMessage.newTopicThreadId + } + case let .forward(_, threadId, _, _, _): + if let threadId { + targetThreadId = threadId + } else { + targetThreadId = EngineMessage.newTopicThreadId + clearMainThreadForward = true + } + } + } + } + + let doSend: (Int64?) -> Void = { [weak self] overrideThreadId in + guard let strongSelf = self else { + return + } + + var messages = messages + if let overrideThreadId { + messages = messages.map { message in + return message.withUpdatedThreadId(overrideThreadId) + } + } + + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() + + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { current in + var current = current + current = current.updatedInterfaceState { interfaceState in + var interfaceState = interfaceState + interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) + if clearInput { + interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString())) + } + return interfaceState + }.updatedInputMode { current in + if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil { + return .media(mode: mode, expanded: nil, focused: focused) + } + return current + } + + return current + }) + } + }, shouldAnimateMessageTransition ? correlationId : nil) + + addToTransitionNodeIfNeeded() + + strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }) + } + + if let targetThreadId { + strongSelf.chatDisplayNode.historyNode.stopHistoryUpdates() + strongSelf.updateChatLocationThread(threadId: targetThreadId, animationDirection: .right, transferInputState: true, completion: { [weak strongSelf] in + guard let strongSelf else { + return + } + doSend(targetThreadId) + if clearMainThreadForward, let peerId = strongSelf.chatLocation.peerId { + let _ = ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: nil, { current in + return current.withUpdatedForwardMessageIds(nil) + }).startStandalone() + } + }) + } else { + doSend(nil) + } } }) return true @@ -2438,9 +2570,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G messages = strongSelf.transformEnqueueMessages(messages, silentPosting: true) strongSelf.sendMessages(messages) } else if schedule { - strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime, repeatPeriod in + strongSelf.presentScheduleTimePicker(completion: { [weak self] result in if let strongSelf = self { - let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod) + let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod) strongSelf.sendMessages(transformedMessages) } }) @@ -2930,7 +3062,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return $0 } }) - strongSelf.messageActionUrlAuthDisposable.set(((combineLatest(strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId), strongSelf.context.engine.messages.requestMessageActionUrlAuth(subject: subject) |> afterDisposed { + let urlAuthAccountPeer = strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + strongSelf.messageActionUrlAuthDisposable.set(((combineLatest(urlAuthAccountPeer, strongSelf.context.engine.messages.requestMessageActionUrlAuth(subject: subject) |> afterDisposed { Queue.mainQueue().async { if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { @@ -2958,7 +3098,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .default: strongSelf.openUrl(defaultUrl, concealed: false, skipUrlAuth: true) case let .request(domain, bot, _, flags, _, _): - let controller = chatMessageActionUrlAuthController(context: strongSelf.context, defaultUrl: defaultUrl, domain: domain, bot: bot, requestWriteAccess: flags.contains(.requestWriteAccess), displayName: EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), open: { [weak self] authorize, allowWriteAccess in + let controller = chatMessageActionUrlAuthController(context: strongSelf.context, defaultUrl: defaultUrl, domain: domain, bot: bot, requestWriteAccess: flags.contains(.requestWriteAccess), displayName: peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder), open: { [weak self] authorize, allowWriteAccess in if let strongSelf = self { if authorize { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { @@ -3117,10 +3257,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Conversation_ShareBotContactConfirmationTitle, text: strongSelf.presentationData.strings.Conversation_ShareBotContactConfirmation, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: { if let strongSelf = self { - let _ = (strongSelf.context.account.postbox.loadedPeerWithId(strongSelf.context.account.peerId) + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).startStandalone(next: { peer in - if let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + if case let .user(user) = peer, let phone = user.phone, !phone.isEmpty { + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } }) } @@ -3468,7 +3615,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) } @@ -3618,7 +3765,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 { @@ -3749,20 +3896,26 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ) strongSelf.present(controller, in: .current) } - }, error: { _ in + }, error: { error in guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else { return } if controllerInteraction.pollActionState.pollMessageIdsInProgress.removeValue(forKey: id) != nil { strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) } + + switch error { + case .restrictedToSubscribers: + strongSelf.displayPollRestrictedToast(messageId: id) + default: + break + } }, completed: { guard let strongSelf = self, let controllerInteraction = strongSelf.controllerInteraction else { return } if controllerInteraction.pollActionState.pollMessageIdsInProgress.removeValue(forKey: id) != nil { Queue.mainQueue().after(1.0, { - strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id) }) } @@ -3912,15 +4065,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard !self.presentAccountFrozenInfoIfNeeded(delay: true) else { return } - self.presentScheduleTimePicker(completion: { [weak self] time, repeatPeriod in + self.presentScheduleTimePicker(completion: { [weak self] result in if let strongSelf = self { if let _ = strongSelf.presentationInterfaceState.interfaceState.mediaDraftState { - strongSelf.sendMediaRecording(scheduleTime: time, messageEffect: (params?.effect).flatMap { + strongSelf.sendMediaRecording(silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod, messageEffect: (params?.effect).flatMap { return ChatSendMessageEffect(id: $0.id) }) } else { - let silentPosting = strongSelf.presentationInterfaceState.interfaceState.silentPosting - strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: silentPosting, scheduleTime: time, repeatPeriod: repeatPeriod, messageEffect: (params?.effect).flatMap { + strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod, messageEffect: (params?.effect).flatMap { return ChatSendMessageEffect(id: $0.id) }) { [weak self] in if let strongSelf = self { @@ -3928,7 +4080,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } }) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { strongSelf.openScheduledMessages() } } @@ -3995,7 +4147,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)) } @@ -4214,6 +4366,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) } }) @@ -4230,7 +4383,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.displayPollSolution(solution: solution, sourceNode: sourceNode, isAutomatic: false) } else if let messageId = self.controllerInteraction?.currentPollMessageWithTooltip { self.controllerInteraction?.currentPollMessageWithTooltip = nil - self.controllerInteraction?.requestMessageUpdate(messageId, false) + self.controllerInteraction?.requestMessageUpdate(messageId, false, nil) } }, displayPsa: { [weak self] type, sourceNode in self?.displayPsa(type: type, sourceNode: sourceNode, isAutomatic: false) @@ -5170,9 +5323,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self.present(controller, in: .window(.root)) }) } - }, requestMessageUpdate: { [weak self] id, scroll in + }, requestMessageUpdate: { [weak self] id, scroll, customTransition in if let self { - self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll) + self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll, customTransition: customTransition) } }, cancelInteractiveKeyboardGestures: { [weak self] in if let self { @@ -5517,6 +5670,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } self.interfaceInteraction?.openSetPeerAvatar() + }, displayPollRestrictedToast: { [weak self] messageId in + guard let self else { + return + } + self.displayPollRestrictedToast(messageId: messageId) }, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: self.stickerSettings, presentationContext: ChatPresentationContext(context: context, backgroundNode: self.chatBackgroundNode)) controllerInteraction.enableFullTranslucency = context.sharedContext.energyUsageSettings.fullTranslucency @@ -7113,6 +7271,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.enableAnimations = true + } + if case let .replyThread(message) = self.chatLocation, message.isForumPost { if self.keepMessageCountersSyncrhonizedDisposable == nil { self.keepMessageCountersSyncrhonizedDisposable = self.context.engine.messages.keepMessageCountersSyncrhonized(peerId: message.peerId, threadId: message.threadId).startStrict() @@ -8058,7 +8223,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let messageId = item.message.id self.controllerInteraction?.currentPollMessageWithTooltip = messageId - self.controllerInteraction?.requestMessageUpdate(messageId, false) + self.controllerInteraction?.requestMessageUpdate(messageId, false, nil) } public func displayPromoAnnouncement(text: String) { @@ -8559,9 +8724,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } } else { - self.presentScheduleTimePicker(style: media ? .media : .default, dismissByTapOutside: false, completion: { [weak self] time, repeatPeriod in + self.presentScheduleTimePicker(style: media ? .media : .default, dismissByTapOutside: false, completion: { [weak self] result in if let strongSelf = self { - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: time, repeatPeriod: repeatPeriod, postpone: postpone), commit: true) + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod, postpone: postpone), commit: true) } }) } @@ -8599,6 +8764,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {} ) { + var animateTransition = true + if let validLayout = self.chatDisplayNode.validLayout?.0 { + if validLayout.metrics.widthClass != .compact { + animateTransition = false + } + } + self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: self.context, account: self.context.account, signals: signals!, originalMediaReference: originalMediaReference) |> deliverOnMainQueue).startStrict(next: { [weak self] items in guard let strongSelf = self else { @@ -8625,6 +8797,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if shouldDivert { skipAddingTransitions = true } + if !animateTransition { + skipAddingTransitions = true + } for item in items { var message = item.message @@ -8685,10 +8860,58 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G mappedMessages.append(message) } + + let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime, postpone: postpone) + let replyMessageSubject = replyToSubject ?? strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject + + var targetThreadId: Int64? + var clearMainThreadForward = false + if strongSelf.chatLocation.threadId == nil, let user = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum), botInfo.flags.contains(.forumManagedByUser) { + if let message = messages.first { + switch message { + case let .message(_, _, _, _, _, replyToMessageId, _, _, _, _): + if let _ = replyToMessageId { + if let replyMessage = strongSelf.presentationInterfaceState.replyMessage { + targetThreadId = replyMessage.threadId + } + } else { + targetThreadId = EngineMessage.newTopicThreadId + } + case let .forward(_, threadId, _, _, _): + if let threadId { + targetThreadId = threadId + } else { + targetThreadId = EngineMessage.newTopicThreadId + clearMainThreadForward = true + } + } + } + } - if addedTransitions.count > 1 { - var transitions: [(Int64, ChatMessageTransitionNodeImpl.Source, () -> Void)] = [] - for (correlationId, uniqueIds, initiated) in addedTransitions { + let addTransitionNodes: () -> Void = { + guard let strongSelf = self else { + return + } + + if addedTransitions.count > 1 { + var transitions: [(Int64, ChatMessageTransitionNodeImpl.Source, () -> Void)] = [] + for (correlationId, uniqueIds, initiated) in addedTransitions { + var source: ChatMessageTransitionNodeImpl.Source? + if uniqueIds.count > 1 { + source = .groupedMediaInput(ChatMessageTransitionNodeImpl.Source.GroupedMediaInput(extractSnapshots: { + return uniqueIds.compactMap({ getAnimatedTransitionSource?($0) }) + })) + } else if let uniqueId = uniqueIds.first { + source = .mediaInput(ChatMessageTransitionNodeImpl.Source.MediaInput(extractSnapshot: { + return getAnimatedTransitionSource?(uniqueId) + })) + } + if let source = source { + transitions.append((correlationId, source, initiated)) + } + } + strongSelf.chatDisplayNode.messageTransitionNode.add(grouped: transitions) + } else if let (correlationId, uniqueIds, initiated) = addedTransitions.first { var source: ChatMessageTransitionNodeImpl.Source? if uniqueIds.count > 1 { source = .groupedMediaInput(ChatMessageTransitionNodeImpl.Source.GroupedMediaInput(extractSnapshots: { @@ -8700,26 +8923,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } if let source = source { - transitions.append((correlationId, source, initiated)) + strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: source, initiated: { + initiated() + }) } } - strongSelf.chatDisplayNode.messageTransitionNode.add(grouped: transitions) - } else if let (correlationId, uniqueIds, initiated) = addedTransitions.first { - var source: ChatMessageTransitionNodeImpl.Source? - if uniqueIds.count > 1 { - source = .groupedMediaInput(ChatMessageTransitionNodeImpl.Source.GroupedMediaInput(extractSnapshots: { - return uniqueIds.compactMap({ getAnimatedTransitionSource?($0) }) - })) - } else if let uniqueId = uniqueIds.first { - source = .mediaInput(ChatMessageTransitionNodeImpl.Source.MediaInput(extractSnapshot: { - return getAnimatedTransitionSource?(uniqueId) - })) - } - if let source = source { - strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: source, initiated: { - initiated() - }) - } } if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject, let messageLimit = customChatContents.messageLimit { @@ -8736,21 +8944,50 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } } - - let messages = strongSelf.transformEnqueueMessages(mappedMessages, silentPosting: silentPosting, scheduleTime: scheduleTime, postpone: postpone) - let replyMessageSubject = replyToSubject ?? strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ - if let strongSelf = self { - strongSelf.chatDisplayNode.collapseInput() - - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } - }) + + let doSend: (Int64?) -> Void = { [weak strongSelf] overrideThreadId in + guard let strongSelf else { + return } - completionImpl?() - }, usedCorrelationId) + + var messages = messages + if let overrideThreadId { + messages = messages.map { message in + return message.withUpdatedThreadId(overrideThreadId) + } + } + + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.chatDisplayNode.collapseInput() - strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }, media: true) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } + }) + } + completionImpl?() + }, usedCorrelationId) + + addTransitionNodes() + strongSelf.sendMessages(messages.map { $0.withUpdatedReplyToMessageId(replyMessageSubject?.subjectModel) }, media: true) + } + + if let targetThreadId { + strongSelf.chatDisplayNode.historyNode.stopHistoryUpdates() + strongSelf.updateChatLocationThread(threadId: targetThreadId, animationDirection: .right, transferInputState: true, completion: { [weak strongSelf] in + guard let strongSelf else { + return + } + doSend(targetThreadId) + if clearMainThreadForward, let peerId = strongSelf.chatLocation.peerId { + let _ = ChatInterfaceState.update(engine: strongSelf.context.engine, peerId: peerId, threadId: nil, { current in + return current.withUpdatedForwardMessageIds(nil) + }).startStandalone() + } + }) + } else { + doSend(nil) + } if let _ = scheduleTime { completion() @@ -8759,7 +8996,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true, postpone: Bool = false) { + func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, resetTextInputState: Bool = true, postpone: Bool = false) { if !canSendMessagesToChat(self.presentationInterfaceState) { return } @@ -8773,7 +9010,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G isScheduledMessages = true } - let sendMessage: (Int32?) -> Void = { [weak self] scheduleTime in + let sendMessage: (Int32?, Bool) -> Void = { [weak self] scheduleTime, silentPosting in guard let self else { return } @@ -8809,12 +9046,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - if isScheduledMessages { - self.presentScheduleTimePicker(style: .default, dismissByTapOutside: false, completion: { time, repeatPeriod in - sendMessage(time) + if let scheduleTime { + sendMessage(scheduleTime, silentPosting) + } else if isScheduledMessages { + self.presentScheduleTimePicker(style: .default, dismissByTapOutside: false, completion: { result in + sendMessage(result.time, result.silentPosting) }) } else { - sendMessage(nil) + sendMessage(nil, silentPosting) } } @@ -9005,8 +9244,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } else if let peerId = self.chatLocation.peerId { - resolveSignal = self.context.account.postbox.loadedPeerWithId(peerId) - |> map(Optional.init) + resolveSignal = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + |> map { Optional($0._asPeer()) } } else { resolveSignal = .single(nil) } @@ -9057,12 +9303,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func shareAccountContact() { - let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> deliverOnMainQueue).startStandalone(next: { [weak self] accountPeer in guard let strongSelf = self else { return } - guard let user = accountPeer as? TelegramUser, let phoneNumber = user.phone else { + guard case let .user(user) = accountPeer, let phoneNumber = user.phone else { return } guard let peer = strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer as? TelegramUser else { @@ -9106,6 +9359,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 @@ -9140,10 +9395,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard case let .peer(peerId) = self.chatLocation else { return } + let navigationTarget = self.effectiveNavigationController.flatMap { navigationController in + resolveChatListNavigationTarget(navigationController: navigationController, excluding: self) + } self.commitPurposefulAction() self.chatDisplayNode.historyNode.disconnect() let _ = self.context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: reportChatSpam).startStandalone() - self.effectiveNavigationController?.popToRoot(animated: true) + if let navigationController = self.effectiveNavigationController { + if let navigationTarget, let popToController = navigationTarget.popToController { + let _ = navigationController.popToViewController(popToController, animated: true) + } else { + navigationController.popToRoot(animated: true) + } + } let _ = self.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peerId, isBlocked: true).startStandalone() } @@ -9202,7 +9466,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G commit() case .info: dismissWebAppControllers() - strongSelf.navigationActionDisposable.set((strongSelf.context.account.postbox.loadedPeerWithId(peerId.id) + strongSelf.navigationActionDisposable.set((strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId.id)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] peer in if let strongSelf = self, peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil { @@ -9469,7 +9740,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let tooltipController = self.mediaRecordingModeTooltipController { tooltipController.updateContent(.text(text), animated: true, extendTimer: true) - } else if let rect = rect { + } else if let rect { let tooltipController = TooltipController(content: .text(text), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, padding: 2.0) self.mediaRecordingModeTooltipController = tooltipController tooltipController.dismissed = { [weak self, weak tooltipController] _ in @@ -10065,6 +10336,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, selectedRepeatPeriod: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32, Int32?) -> Void) { + self.presentScheduleTimePicker(style: style, selectedTime: selectedTime, selectedRepeatPeriod: selectedRepeatPeriod, dismissByTapOutside: dismissByTapOutside, completion: { result in + completion(result.time, result.repeatPeriod) + }) + } + + func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, selectedRepeatPeriod: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (ChatScheduleTimeScreen.Result) -> Void) { guard let peerId = self.chatLocation.peerId else { return } @@ -10086,7 +10363,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if peerId == strongSelf.context.account.peerId { mode = .reminders } else { - mode = .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable) + mode = .scheduledMessages(peerId: peer.id, sendWhenOnlineAvailable: sendWhenOnlineAvailable) } let controller = ChatScheduleTimeScreen( @@ -10095,42 +10372,61 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G currentTime: selectedTime, currentRepeatPeriod: selectedRepeatPeriod, minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout, + silentPosting: strongSelf.presentationInterfaceState.interfaceState.silentPosting, isDark: style == .media, completion: { result in - completion(result.time, result.repeatPeriod) + completion(result) } ) strongSelf.chatDisplayNode.dismissInput() - strongSelf.push(controller) + if strongSelf.videoRecorderValue != nil { + strongSelf.present(controller, in: .window(.root)) + } else { + strongSelf.push(controller) + } }) } - func presentTimerPicker(style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32) -> Void) { + func presentTimerPicker(style: ChatTimerScreenStyle = .default, selectedTime: Int32? = nil, completion: @escaping (Int32) -> Void) { guard case .peer = self.chatLocation else { return } - let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, style: style, currentTime: selectedTime, dismissByTapOutside: dismissByTapOutside, completion: { time in + let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, style: style, currentTime: selectedTime, completion: { time in completion(time) }) self.chatDisplayNode.dismissInput() self.present(controller, in: .window(.root)) } - func presentVoiceMessageDiscardAlert(action: @escaping () -> Void = {}, alertAction: (() -> Void)? = nil, delay: Bool = false, performAction: Bool = true) -> Bool { - if let _ = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState { + func presentVoiceMessageDiscardAlert( + action: @escaping () -> Void = {}, + alertAction: (() -> Void)? = nil, + discardIfVideo: Bool = false, + delay: Bool = false, + performAction: Bool = true + ) -> Bool { + if let mediaRecordingState = self.presentationInterfaceState.inputTextPanelState.mediaRecordingState { + var discard = false + if discardIfVideo, case .video = mediaRecordingState { + discard = true + } alertAction?() Queue.mainQueue().after(delay ? 0.2 : 0.0) { let alertController = textAlertController( context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, - text: self.presentationData.strings.Conversation_StopVoiceMessageDescription, + text: discard ? self.presentationData.strings.Conversation_DiscardRecordedVoiceMessageDescription : self.presentationData.strings.Conversation_StopVoiceMessageDescription, actions: [ TextAlertAction( type: .defaultAction, - title: self.presentationData.strings.Conversation_StopVoiceMessagePauseAction, + title: discard ? self.presentationData.strings.Conversation_DiscardRecordedVoiceMessageAction : self.presentationData.strings.Conversation_StopVoiceMessagePauseAction, action: { [weak self] in - self?.stopMediaRecorder(pause: true) + if discard { + self?.dismissMediaRecorder(.dismiss) + } else { + self?.stopMediaRecorder(pause: true) + } Queue.mainQueue().after(0.1) { action() } @@ -10155,32 +10451,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } return false } - - func presentRecordedVoiceMessageDiscardAlert(action: @escaping () -> Void = {}, alertAction: (() -> Void)? = nil, delay: Bool = false, performAction: Bool = true) -> Bool { - if let _ = self.presentationInterfaceState.interfaceState.mediaDraftState { - alertAction?() - Queue.mainQueue().after(delay ? 0.2 : 0.0) { - self.present(textAlertController(context: self.context, updatedPresentationData: self.updatedPresentationData, title: nil, text: self.presentationData.strings.Conversation_DiscardRecordedVoiceMessageDescription, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_DiscardRecordedVoiceMessageAction, action: { [weak self] in - self?.stopMediaRecorder() - Queue.mainQueue().after(0.1) { - action() - } - })]), in: .window(.root)) - } - - return true - } else if performAction { - action() - } - return false - } - + func presentAutoremoveSetup() { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } - let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, style: .default, mode: .autoremove, currentTime: self.presentationInterfaceState.autoremoveTimeout, dismissByTapOutside: true, completion: { [weak self] value in + let controller = ChatTimerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, style: .default, mode: .autoremove, currentTime: self.presentationInterfaceState.autoremoveTimeout, completion: { [weak self] value in guard let strongSelf = self else { return } diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index a8834e5866..958341234f 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -21,7 +21,7 @@ fileprivate struct InitialBannedRights { } extension ChatControllerImpl { - fileprivate func applyAdminUserActionsResult(messageIds: Set, result: AdminUserActionsSheet.ChatResult, initialUserBannedRights: [EnginePeer.Id: InitialBannedRights]) { + fileprivate func applyAdminUserActionsResult(messageIds: Set, reactionPeerId: EnginePeer.Id? = nil, result: AdminUserActionsSheet.ChatResult, initialUserBannedRights: [EnginePeer.Id: InitialBannedRights]) { guard let messagesPeerId = self.chatLocation.peerId else { return } @@ -30,7 +30,12 @@ extension ChatControllerImpl { } - var title: String? = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple + var title: String? + if let _ = reactionPeerId { + title = self.presentationData.strings.Chat_AdminAction_ToastReactionsDeletedTitleSingle + } else { + title = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple + } if !result.deleteAllFromPeers.isEmpty { title = self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple } @@ -67,13 +72,24 @@ extension ChatControllerImpl { } do { - let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() + if let reactionPeerId { + if let messageId = messageIds.first { + let _ = self.context.engine.messages.deleteReaction(messageId: messageId, authorId: reactionPeerId).startStandalone() + } + } else { + let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone() + } for authorId in result.deleteAllFromPeers { let _ = self.context.engine.messages.deleteAllMessagesWithAuthor(peerId: messagesPeerId, authorId: authorId, namespace: Namespaces.Message.Cloud).startStandalone() let _ = self.context.engine.messages.clearAuthorHistory(peerId: messagesPeerId, memberId: authorId).startStandalone() } + let aroundMessageId = messageIds.count == 1 ? messageIds.first : nil + for authorId in result.deleteAllReactionsFromPeers { + let _ = self.context.engine.messages.deleteAllReactionsWithAuthor(peerId: messagesPeerId, authorId: authorId, aroundMessageId: aroundMessageId).startStandalone() + } + for authorId in result.reportSpamPeers { let _ = self.context.engine.peers.reportPeer(peerId: authorId, reason: .spam, message: "").startStandalone() } @@ -88,9 +104,17 @@ extension ChatControllerImpl { } if text.isEmpty { - text = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple - if !result.deleteAllFromPeers.isEmpty { + if let _ = reactionPeerId { + text = self.presentationData.strings.Chat_AdminAction_ToastReactionsDeletedTextSingle + } else { + text = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple + } + if !result.deleteAllFromPeers.isEmpty && !result.deleteAllReactionsFromPeers.isEmpty { + text = self.presentationData.strings.Chat_AdminAction_ToastMessagesAndReactionsDeletedText + } else if !result.deleteAllFromPeers.isEmpty { text = self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple + } else if !result.deleteAllReactionsFromPeers.isEmpty { + text = self.presentationData.strings.Chat_AdminAction_ToastReactionsDeletedTextMultiple } title = nil } @@ -206,7 +230,7 @@ extension ChatControllerImpl { let peer = author renderedParticipants.append(RenderedChannelParticipant( participant: participant, - peer: peer + peer: EnginePeer(peer) )) switch participant { case .creator: @@ -238,7 +262,31 @@ extension ChatControllerImpl { })) } - func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set, options: ChatAvailableMessageActionOptions) { + public func presentReactionDeletionOptions(author: Peer, messageId: MessageId) { + guard self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudChannel, author.id != self.context.account.peerId else { + return + } + let _ = (self.context.sharedContext.chatAvailableMessageActions( + engine: self.context.engine, + accountPeerId: self.context.account.peerId, + messageIds: Set([messageId]), + keepUpdated: false + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] actions in + guard let self, !actions.options.isEmpty else { + return + } + self.presentBanMessageOptions( + accountPeerId: self.context.account.peerId, + author: author, + messageIds: Set([messageId]), + options: actions.options, + reaction: true + ) + }) + } + + func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set, options: ChatAvailableMessageActionOptions, reaction: Bool = false) { guard let peerId = self.chatLocation.peerId else { return } @@ -325,14 +373,17 @@ extension ChatControllerImpl { initialUserBannedRights[participant.peerId] = InitialBannedRights(value: nil) } } - self.push(AdminUserActionsSheet( - context: self.context, - chatPeer: chatPeer, - peers: [RenderedChannelParticipant( - participant: participant, - peer: authorPeer._asPeer() - )], - mode: .chat( + + let mode: AdminUserActionsSheet.Mode + if reaction { + mode = .chatReaction(completion: { [weak self] result in + guard let self else { + return + } + self.applyAdminUserActionsResult(messageIds: messageIds, reactionPeerId: authorPeer.id, result: result, initialUserBannedRights: initialUserBannedRights) + }) + } else { + mode = .chat( messageCount: messageIds.count, deleteAllMessageCount: deleteAllMessageCount, completion: { [weak self] result in @@ -342,6 +393,16 @@ extension ChatControllerImpl { self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights) } ) + } + + self.push(AdminUserActionsSheet( + context: self.context, + chatPeer: chatPeer, + peers: [RenderedChannelParticipant( + participant: participant, + peer: authorPeer + )], + mode: mode )) }) })) @@ -415,7 +476,7 @@ extension ChatControllerImpl { } self.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) }), - TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}) + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}) ], actionLayout: .vertical, parseMarkdown: true @@ -477,7 +538,7 @@ extension ChatControllerImpl { } self.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) }), - TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}) + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}) ], actionLayout: .vertical, parseMarkdown: true @@ -555,7 +616,7 @@ extension ChatControllerImpl { let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings) strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Title, text: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Text(dateString).string, actions: [TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Common_Delete, action: { commit() - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { })], parseMarkdown: true), in: .window(.root)) } f(.default) @@ -724,7 +785,7 @@ extension ChatControllerImpl { chatPeer: chatPeer, peers: [RenderedChannelParticipant( participant: participant, - peer: authorPeer._asPeer() + peer: authorPeer )], mode: .monoforum(completion: { [weak self] result in guard let self else { diff --git a/submodules/TelegramUI/Sources/ChatControllerContentData.swift b/submodules/TelegramUI/Sources/ChatControllerContentData.swift index c7661a559a..fcc7e6e66f 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 } @@ -581,12 +602,17 @@ extension ChatControllerImpl { notificationSettings: nil, peerPresences: [:], cachedData: nil - ), customTitle: nil, customSubtitle: strings.Chat_Monoforum_Subtitle, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: nil, customMessageCount: nil, isEnabled: true) + ), customTitle: nil, customSubtitle: strings.Chat_Monoforum_Subtitle, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: nil, customMessageCount: nil, hidePeerStatus: false, isEnabled: true) } else { 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 { + customSubtitle = strings.Chat_SavedMessagesStatusViewAsChats + } + + strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, customSubtitle: customSubtitle, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, hidePeerStatus: false, isEnabled: hasPeerInfo) let imageOverride: AvatarNodeImageOverride? if context.account.peerId == peer.id { @@ -1492,7 +1518,7 @@ extension ChatControllerImpl { customMessageCount = savedMessagesPeer?.messageCount ?? 0 } - strongSelf.state.chatTitleContent = .peer(peerView: mappedPeerData, customTitle: nil, customSubtitle: customSubtitle, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: false, customMessageCount: customMessageCount, isEnabled: true) + strongSelf.state.chatTitleContent = .peer(peerView: mappedPeerData, customTitle: nil, customSubtitle: customSubtitle, onlineMemberCount: (nil, nil), isScheduledMessages: false, isMuted: false, customMessageCount: customMessageCount, hidePeerStatus: false, isEnabled: true) strongSelf.state.peerView = peerView @@ -1586,7 +1612,7 @@ extension ChatControllerImpl { } } - strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: threadInfo.title, customSubtitle: customSubtitle, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: peerIsMuted, customMessageCount: messageAndTopic.messageCount == 0 ? nil : messageAndTopic.messageCount, isEnabled: true) + strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: threadInfo.title, customSubtitle: customSubtitle, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: peerIsMuted, customMessageCount: messageAndTopic.messageCount == 0 ? nil : messageAndTopic.messageCount, hidePeerStatus: true, isEnabled: true) let avatarContent: EmojiStatusComponent.Content if chatLocation.threadId == 1 { @@ -2235,7 +2261,7 @@ extension ChatControllerImpl { } else { cachedData = .single((nil, [:])) } - + self.cachedDataDisposable?.dispose() self.cachedDataDisposable = combineLatest(queue: .mainQueue(), cachedData, diff --git a/submodules/TelegramUI/Sources/ChatControllerDisplayBusinessBotMessageTooltip.swift b/submodules/TelegramUI/Sources/ChatControllerDisplayBusinessBotMessageTooltip.swift index a08466b392..e28d67da18 100644 --- a/submodules/TelegramUI/Sources/ChatControllerDisplayBusinessBotMessageTooltip.swift +++ b/submodules/TelegramUI/Sources/ChatControllerDisplayBusinessBotMessageTooltip.swift @@ -1,7 +1,6 @@ import Foundation import TelegramPresentationData import AccountContext -import Postbox import TelegramCore import SwiftSignalKit import Display diff --git a/submodules/TelegramUI/Sources/ChatControllerDisplayDiceTooltip.swift b/submodules/TelegramUI/Sources/ChatControllerDisplayDiceTooltip.swift index 6b59404eac..39c29d2f35 100644 --- a/submodules/TelegramUI/Sources/ChatControllerDisplayDiceTooltip.swift +++ b/submodules/TelegramUI/Sources/ChatControllerDisplayDiceTooltip.swift @@ -1,6 +1,5 @@ import Foundation import AccountContext -import Postbox import TelegramCore import SwiftSignalKit import Display diff --git a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift index c1ffb6bcd6..ae9fc6719c 100644 --- a/submodules/TelegramUI/Sources/ChatControllerEditChat.swift +++ b/submodules/TelegramUI/Sources/ChatControllerEditChat.swift @@ -1,7 +1,6 @@ import Foundation import TelegramPresentationData import AccountContext -import Postbox import TelegramCore import SwiftSignalKit import Display diff --git a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift index 6913d4cbb1..71c92156fb 100644 --- a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift +++ b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift @@ -317,14 +317,14 @@ extension ChatControllerImpl { let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: true) commit(transformedMessages) case .schedule: - strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime, repeatPeriod in + strongSelf.presentScheduleTimePicker(completion: { [weak self] timeResult in if let strongSelf = self { - let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod) + let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: timeResult.silentPosting, scheduleTime: timeResult.time, repeatPeriod: timeResult.repeatPeriod) commit(transformedMessages) } }) case .whenOnline: - let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleWhenOnlineTimestamp) + let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: strongSelf.presentationInterfaceState.interfaceState.silentPosting, scheduleTime: scheduleWhenOnlineTimestamp) commit(transformedMessages) } } diff --git a/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift b/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift index c52cd0b417..20a936bf7c 100644 --- a/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift +++ b/submodules/TelegramUI/Sources/ChatControllerKeyShortcuts.swift @@ -1,7 +1,6 @@ import Foundation import TelegramPresentationData import AccountContext -import Postbox import ChatInterfaceState import TelegramCore import SwiftSignalKit diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 5f2ab2b337..68f52b510e 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 @@ -492,7 +493,16 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if case let .messageOptions(_, messageIds, info) = subject { switch info { case let .forward(forward): - let messages = combineLatest(context.account.postbox.messagesAtIds(messageIds), context.account.postbox.loadedPeerWithId(context.account.peerId), forward.options) + let accountPeerSignal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + |> map { $0._asPeer() } + let messages = combineLatest(context.account.postbox.messagesAtIds(messageIds), accountPeerSignal, forward.options) |> map { messages, accountPeer, options -> ([Message], Int32, Bool) in var messages = messages let forwardedMessageIds = Set(messages.map { $0.id }) @@ -554,7 +564,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } if let poll = media as? TelegramMediaPoll { var updatedMedia = message.media.filter { !($0 is TelegramMediaPoll) } - updatedMedia.append(TelegramMediaPoll(pollId: poll.pollId, publicity: poll.publicity, kind: poll.kind, text: poll.text, textEntities: poll.textEntities, options: poll.options, correctAnswers: poll.correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil, hasUnseenVotes: false), isClosed: false, deadlineTimeout: nil, deadlineDate: nil, pollHash: poll.pollHash)) + updatedMedia.append(TelegramMediaPoll(pollId: poll.pollId, publicity: poll.publicity, kind: poll.kind, text: poll.text, textEntities: poll.textEntities, options: poll.options, correctAnswers: poll.correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil, hasUnseenVotes: false, canViewStats: false), isClosed: false, deadlineTimeout: nil, deadlineDate: nil, pollHash: poll.pollHash)) messageMedia = updatedMedia } if let _ = media as? TelegramMediaDice { @@ -581,7 +591,16 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } source = .custom(messages: messages, messageId: MessageId(peerId: PeerId(0), namespace: 0, id: 0), quote: nil, isSavedMusic: false, canReorder: false, loadMore: nil) case let .reply(reply): - let messages = combineLatest(context.account.postbox.messagesAtIds(messageIds), context.account.postbox.loadedPeerWithId(context.account.peerId)) + let replyAccountPeerSignal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + |> map { $0._asPeer() } + let messages = combineLatest(context.account.postbox.messagesAtIds(messageIds), replyAccountPeerSignal) |> map { messages, accountPeer -> ([Message], Int32, Bool) in var messages = messages messages.sort(by: { lhsMessage, rhsMessage in @@ -610,10 +629,19 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { stories = .single([:]) } + let linkAccountPeerSignal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } + |> map { $0._asPeer() } if let replyMessageId = options.replyMessageId { return combineLatest( context.account.postbox.messagesAtIds([replyMessageId]), - context.account.postbox.loadedPeerWithId(context.account.peerId), + linkAccountPeerSignal, stories ) |> map { messages, peer, stories -> (ChatControllerSubject.LinkOptions, Peer, Message?, [StoryId: CodableEntry]) in @@ -621,7 +649,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } else { return combineLatest( - context.account.postbox.loadedPeerWithId(context.account.peerId), + linkAccountPeerSignal, stories ) |> map { peer, stories -> (ChatControllerSubject.LinkOptions, Peer, Message?, [StoryId: CodableEntry]) in @@ -832,9 +860,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.setupHistoryNode() - self.interactiveEmojisDisposable = (self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + self.interactiveEmojisDisposable = (self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) |> map { preferencesView -> InteractiveEmojiConfiguration in - let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue + let appConfiguration: AppConfiguration = preferencesView?.get(AppConfiguration.self) ?? .defaultValue return InteractiveEmojiConfiguration.with(appConfiguration: appConfiguration) } |> deliverOnMainQueue).startStrict(next: { [weak self] emojis in @@ -888,7 +916,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.chatPresentationInterfaceState.chatLocation.peerId, peerId.namespace != Namespaces.Peer.SecretChat { self.textInputPanelNode?.isAIEnabled = true } self.textInputPanelNode?.textInputAccessoryPanel = textInputAccessoryPanel @@ -1394,6 +1422,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { data: mediaPlayback, controller: { [weak self] in return self?.controller + }, + shouldPerformAction: { [weak self] action in + guard let controller = self?.controller else { + action() + return + } + let _ = controller.presentVoiceMessageDiscardAlert(action: action) } ))) ) @@ -1647,6 +1682,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } else if let headerPanelsView = self.headerPanelsView { self.headerPanelsView = nil if let headerPanelsComponentView = headerPanelsView.view { + transition.updateTransformScale(layer: headerPanelsComponentView.layer, scale: 0.001) transition.updateAlpha(layer: headerPanelsComponentView.layer, alpha: 0.0, completion: { [weak headerPanelsComponentView] _ in headerPanelsComponentView?.removeFromSuperview() }) @@ -1755,7 +1791,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if inputTextPanelNode.isFocused { self.context.sharedContext.mainWindow?.simulateKeyboardDismiss(transition: .animated(duration: 0.5, curve: .spring)) } - let _ = inputTextPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: inputPanelBottomInset, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, maxOverlayHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) + let _ = inputTextPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: inputPanelBottomInset, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, maxOverlayHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) } if let prevInputPanelNode = self.inputPanelNode, inputPanelNode.canHandleTransition(from: prevInputPanelNode) { inputPanelNodeHandlesTransition = true @@ -1767,7 +1803,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } else { dismissedInputPanelNode = self.inputPanelNode } - let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: inputPanelBottomInset, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, maxOverlayHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: false, transition: inputPanelNode.supernode !== self ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) + let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: inputPanelBottomInset, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, maxOverlayHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: false, transition: inputPanelNode.supernode !== self ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) self.inputPanelNode = inputPanelNode if inputPanelNode.supernode !== self { @@ -1778,7 +1814,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.inputPanelOverlayNode.view.addSubview(viewForOverlayContent) } } else { - let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: inputPanelBottomInset, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset - 120.0, maxOverlayHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) + let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: inputPanelBottomInset, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset - 120.0, maxOverlayHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) } } else { @@ -1789,7 +1825,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { if let secondaryInputPanelNode = inputPanelNodes.secondary, !previewing { if secondaryInputPanelNode !== self.secondaryInputPanelNode { dismissedSecondaryInputPanelNode = self.secondaryInputPanelNode - let inputPanelHeight = secondaryInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: inputPanelBottomInset, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, maxOverlayHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: true, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) + let inputPanelHeight = secondaryInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: inputPanelBottomInset, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, maxOverlayHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: true, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) secondaryInputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) self.secondaryInputPanelNode = secondaryInputPanelNode if secondaryInputPanelNode.supernode == nil { @@ -1800,7 +1836,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.inputPanelOverlayNode.view.addSubview(viewForOverlayContent) } } else { - let inputPanelHeight = secondaryInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: inputPanelBottomInset, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, maxOverlayHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: true, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) + let inputPanelHeight = secondaryInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: inputPanelBottomInset, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset, maxOverlayHeight: layout.size.height - insets.top - inputPanelBottomInset, isSecondary: true, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) secondaryInputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) } } else { @@ -2050,6 +2086,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { messageTransitionNode.transform = previousMessageTransitionNode.transform previousMessageTransitionNode.supernode?.insertSubnode(self.messageTransitionNode, aboveSubnode: previousMessageTransitionNode) + previousMessageTransitionNode.overlayContainerNode.supernode?.insertSubnode( self.messageTransitionNode.overlayContainerNode, aboveSubnode: previousMessageTransitionNode.overlayContainerNode) self.emptyType = nil self.isLoadingValue = false @@ -2223,7 +2260,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { additionalOffset = 80.0 } if let _ = inputPanelSize { - inputPanelHideOffset += -48.0 - additionalOffset + inputPanelHideOffset += -56.0 - additionalOffset } if let accessoryPanelSize = accessoryPanelSize { inputPanelHideOffset += -accessoryPanelSize.height - additionalOffset @@ -2457,6 +2494,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { listInsets.left += floatingTopicsPanelInsets.left listInsets.bottom += floatingTopicsPanelInsets.top + childContentInsets.top = listInsets.bottom + var emptyNodeInsets = insets emptyNodeInsets.bottom += inputPanelsHeight self.validEmptyNodeLayout = (contentBounds.size, emptyNodeInsets, listInsets.left, listInsets.right) @@ -2644,6 +2683,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { var headerPanelsTransition = ComponentTransition(transition) if headerPanelsComponentView.superview == nil { headerPanelsTransition.animateAlpha(view: headerPanelsComponentView, from: 0.0, to: 1.0) + headerPanelsTransition.animateScale(view: headerPanelsComponentView, from: 0.001, to: 1.0) headerPanelsTransition = headerPanelsTransition.withAnimation(.none) self.floatingTopicsPanelContainer.view.addSubview(headerPanelsComponentView) } @@ -3050,6 +3090,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 +3157,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 +3436,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 +3490,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() }) @@ -4001,8 +4058,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { func frameForInputActionButton() -> CGRect? { if let textInputPanelNode = self.textInputPanelNode, self.inputPanelNode === textInputPanelNode { - return textInputPanelNode.frameForInputActionButton().flatMap { - return $0.offsetBy(dx: textInputPanelNode.frame.minX, dy: textInputPanelNode.frame.minY) + return textInputPanelNode.frameForInputActionButton().flatMap { rect in + return self.view.convert(rect, from: textInputPanelNode.view) } } return nil diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index bf77a22fe9..50b7728237 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 @@ -45,29 +45,29 @@ extension ChatControllerImpl { case bot(id: PeerId, payload: String?, justInstalled: Bool) case gift } - + func presentAttachmentMenu(subject: AttachMenuSubject) { guard self.audioRecorderValue == nil && self.videoRecorderValue == nil else { return } - + let context = self.context let inputIsActive = self.presentationInterfaceState.inputMode == .text - + self.chatDisplayNode.dismissInput() - + let canByPassRestrictions = canBypassRestrictions(chatPresentationInterfaceState: self.presentationInterfaceState) - + var banSendText: (Int32, Bool)? var bannedSendPhotos: (Int32, Bool)? var bannedSendVideos: (Int32, Bool)? var bannedSendFiles: (Int32, Bool)? - + var enableMultiselection = true if self.presentationInterfaceState.interfaceState.postSuggestionState != nil { enableMultiselection = false } - + var canSendPolls = true var canSendTodos = true if let peer = self.presentationInterfaceState.renderedPeer?.peer { @@ -117,37 +117,37 @@ extension ChatControllerImpl { } else { canSendPolls = false } - + var availableButtons: [AttachmentButtonType] = [.gallery, .file] if banSendText == nil { availableButtons.append(.location) availableButtons.append(.contact) } - + if canSendPolls { availableButtons.insert(.poll, at: max(0, availableButtons.count - 1)) } - + if canSendTodos { availableButtons.insert(.todo, at: max(0, availableButtons.count - 1)) } - + if "".isEmpty { availableButtons.insert(.audio, at: max(0, availableButtons.count - 1)) } - + let presentationData = self.presentationData - + var isScheduledMessages = false if case .scheduledMessages = self.presentationInterfaceState.subject { isScheduledMessages = true } - + var isPaidMessages = false if let _ = self.presentationInterfaceState.sendPaidMessageStars { isPaidMessages = true } - + var peerType: AttachMenuBots.Bot.PeerFlags = [] if let peer = self.presentationInterfaceState.renderedPeer?.peer { if let user = peer as? TelegramUser { @@ -166,7 +166,7 @@ extension ChatControllerImpl { } } } - + let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError> if let peer = self.presentationInterfaceState.renderedPeer?.peer, !isScheduledMessages, !peer.isDeleted { buttons = combineLatest( @@ -187,7 +187,7 @@ extension ChatControllerImpl { default: break } - + for bot in attachMenuBots.reversed() { var peerType = peerType if bot.peer.id == peer.id { @@ -197,7 +197,7 @@ extension ChatControllerImpl { let button: AttachmentButtonType = .app(bot) if !bot.peerTypes.intersection(peerType).isEmpty { buttons.insert(button, at: 1) - + if case let .bot(botId, _, _) = subject { if initialButton == nil && bot.peer.id == botId { initialButton = button @@ -206,7 +206,7 @@ extension ChatControllerImpl { } allButtons.insert(button, at: 1) } - + if !isPaidMessages { if context.isPremium, shortcutMessageList.items.count > 0, let user = peer as? TelegramUser, user.botInfo == nil { if let index = buttons.firstIndex(where: { $0 == .location }) { @@ -221,21 +221,21 @@ extension ChatControllerImpl { } } } - + return (buttons, allButtons, initialButton) } } else { buttons = .single((availableButtons, availableButtons, .gallery)) } - + let dataSettings = self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) return entry ?? GeneratedMediaStoreSettings.defaultSettings } - + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) let premiumGiftOptions: [CachedPremiumGiftOption] - + var showPremiumGift = false if !premiumConfiguration.isPremiumDisabled && self.presentationInterfaceState.disallowedGifts != TelegramDisallowedGifts.All { if self.presentationInterfaceState.alwaysShowGiftButton { @@ -246,23 +246,23 @@ extension ChatControllerImpl { showPremiumGift = true } } - + if let peer = self.presentationInterfaceState.renderedPeer?.peer, showPremiumGift, let user = peer as? TelegramUser, !user.isDeleted && user.botInfo == nil && !user.flags.contains(.isSupport) { premiumGiftOptions = self.presentationInterfaceState.premiumGiftOptions } else { premiumGiftOptions = [] } - + let _ = combineLatest(queue: Queue.mainQueue(), buttons, dataSettings).startStandalone(next: { [weak self] buttonsAndInitialButton, dataSettings in guard let strongSelf = self else { return } - + var (buttons, allButtons, initialButton) = buttonsAndInitialButton if !premiumGiftOptions.isEmpty { buttons.insert(.gift, at: 1) } - + guard let initialButton = initialButton else { if case let .bot(botId, botPayload, botJustInstalled) = subject { if let button = allButtons.first(where: { button in @@ -289,7 +289,7 @@ extension ChatControllerImpl { let controller = webAppTermsAlertController(context: context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { allowWrite in let _ = (context.engine.messages.addBotToAttachMenu(botId: botId, allowWrite: allowWrite) |> deliverOnMainQueue).startStandalone(error: { _ in - + }, completed: { strongSelf.presentAttachmentBot(botId: botId, payload: botPayload, justInstalled: true) }) @@ -302,22 +302,17 @@ extension ChatControllerImpl { } return } - + let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText - + let currentMediaController = Atomic(value: nil) let currentFilesController = Atomic(value: nil) let currentAudioController = Atomic(value: nil) let currentLocationController = Atomic(value: nil) - + strongSelf.canReadHistory.set(false) - - let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, style: .glass, chatLocation: strongSelf.chatLocation, isScheduledMessages: isScheduledMessages, buttons: buttons, initialButton: initialButton, makeEntityInputView: { [weak self] in - guard let strongSelf = self else { - return nil - } - return EntityInputView(context: strongSelf.context, isDark: false, areCustomEmojiEnabled: strongSelf.presentationInterfaceState.customEmojiAvailable) - }) + + let attachmentController = AttachmentController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, style: .glass, chatLocation: strongSelf.chatLocation, isScheduledMessages: isScheduledMessages, buttons: buttons, initialButton: initialButton, customEmojiAvailable: strongSelf.presentationInterfaceState.customEmojiAvailable) attachmentController.attachmentButton = strongSelf.chatDisplayNode.getAttachmentButton() attachmentController.shouldMinimizeOnSwipe = { [weak attachmentController] button in if case .app = button { @@ -390,7 +385,7 @@ extension ChatControllerImpl { if mediaReferences.count > 1 { groupingKey = Int64.random(in: .min ..< .max) } - + var attributes: [MessageAttribute] = [] var text = "" if let caption { @@ -400,13 +395,13 @@ extension ChatControllerImpl { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } } - + for mediaReference in mediaReferences { if messages.count == 10 { groupingKey = Int64.random(in: .min ..< .max) } let isLast = mediaReference == mediaReferences.last - + messages.append(.message(text: isLast ? text : "", attributes: isLast ? attributes : [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } messages = self.transformEnqueueMessages(messages, silentPosting: silentPosting, scheduleTime: scheduleTime, repeatPeriod: nil, postpone: false) @@ -440,7 +435,7 @@ extension ChatControllerImpl { if mediaReferences.count > 1 { groupingKey = Int64.random(in: .min ..< .max) } - + var attributes: [MessageAttribute] = [] var text = "" if let caption { @@ -450,13 +445,13 @@ extension ChatControllerImpl { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } } - + for mediaReference in mediaReferences { if messages.count == 10 { groupingKey = Int64.random(in: .min ..< .max) } let isLast = mediaReference == mediaReferences.last - + messages.append(.message(text: isLast ? text : "", attributes: isLast ? attributes : [], inlineStickers: [:], mediaReference: mediaReference, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: [])) } messages = self.transformEnqueueMessages(messages, silentPosting: silentPosting, scheduleTime: scheduleTime, repeatPeriod: nil, postpone: false) @@ -507,7 +502,7 @@ extension ChatControllerImpl { } let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: location), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) - + strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in guard let strongSelf = self else { return @@ -515,7 +510,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() - + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } }) @@ -525,7 +520,7 @@ extension ChatControllerImpl { }) }) completion(controller, controller.mediaPickerContext) - + let _ = currentLocationController.swap(controller) }) return true @@ -533,7 +528,9 @@ extension ChatControllerImpl { let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, style: .glass, updatedPresentationData: strongSelf.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: .always, requirePhoneNumbers: true)) contactsController.presentScheduleTimePicker = { [weak self] completion in if let strongSelf = self { - strongSelf.presentScheduleTimePicker(completion: completion) + strongSelf.presentScheduleTimePicker(completion: { result in + completion(result.time, result.repeatPeriod, result.silentPosting) + }) } } contactsController.navigationPresentation = .modal @@ -559,11 +556,11 @@ extension ChatControllerImpl { var media: TelegramMediaContact? switch peer { case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + guard case let .user(contact) = contact, let phoneNumber = contact.phone else { continue } let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - + let phone = contactData.basicData.phoneNumbers[0].value media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil) case let .deviceContact(_, basicData): @@ -571,17 +568,17 @@ extension ChatControllerImpl { continue } let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - + let phone = contactData.basicData.phoneNumbers[0].value media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil) } - + if let media = media { let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() - + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } }) @@ -612,7 +609,7 @@ extension ChatControllerImpl { let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> switch peer { case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + guard case let .user(contact) = contact, let phoneNumber = contact.phone else { return } let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") @@ -630,7 +627,7 @@ extension ChatControllerImpl { } } } - + if let stableId = stableId { return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) |> take(1) @@ -658,13 +655,13 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() - + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } }) } }, nil) - + var enqueueMessages: [EnqueueMessage] = [] if let textEnqueueMessage = textEnqueueMessage { enqueueMessages.append(textEnqueueMessage) @@ -683,7 +680,7 @@ extension ChatControllerImpl { strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime, postpone: postpone), postpone: postpone) }) } else { - let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: strongSelf.context), environment: ShareControllerAppEnvironment(sharedContext: strongSelf.context.sharedContext), subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in + let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: strongSelf.context), environment: ShareControllerAppEnvironment(sharedContext: strongSelf.context.sharedContext), subject: .filter(peer: peerAndContactData.0.flatMap(EnginePeer.init), contactId: nil, contactData: contactData, completion: { peer, contactData in guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { return } @@ -694,13 +691,13 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() - + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } }) } }, nil) - + var enqueueMessages: [EnqueueMessage] = [] if let textEnqueueMessage = textEnqueueMessage { enqueueMessages.append(textEnqueueMessage) @@ -763,7 +760,7 @@ extension ChatControllerImpl { }) completion(controller, controller.mediaPickerContext) strongSelf.controllerNavigationDisposable.set(nil) - + let _ = ApplicationSpecificNotice.incrementDismissedPremiumGiftSuggestion(accountManager: context.sharedContext.accountManager, peerId: peer.id, timestamp: Int32(Date().timeIntervalSince1970)).startStandalone() } } @@ -795,7 +792,7 @@ extension ChatControllerImpl { } completion(controller, controller.mediaPickerContext) strongSelf.controllerNavigationDisposable.set(nil) - + if bot.flags.contains(.notActivated) { let alertController = webAppTermsAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bot: bot, completion: { [weak self] allowWrite in guard let self else { @@ -824,7 +821,7 @@ extension ChatControllerImpl { guard let strongSelf else { return } - + let controller = QuickReplySetupScreen(context: strongSelf.context, initialData: initialData as! QuickReplySetupScreen.InitialData, mode: .select(completion: { [weak strongSelf] shortcutId in guard let strongSelf else { return @@ -845,7 +842,7 @@ extension ChatControllerImpl { attachmentController.navigationPresentation = .flatModal strongSelf.push(attachmentController) strongSelf.attachmentController = attachmentController - + if case let .bot(botId, _, botJustInstalled) = subject, botJustInstalled { if let button = allButtons.first(where: { button in if case let .app(bot) = button, bot.peer.id == botId { @@ -866,7 +863,7 @@ extension ChatControllerImpl { } } } - + if inputIsActive { Queue.mainQueue().after(0.15, { present() @@ -876,7 +873,7 @@ extension ChatControllerImpl { } }) } - + func presentEditingAttachmentMenu(editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?) { let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) @@ -907,7 +904,7 @@ extension ChatControllerImpl { } } } - + if editMediaOptions == nil, let (untilDate, personal) = bannedSendMedia { let banDescription: String if untilDate != 0 && untilDate != Int32.max { @@ -917,7 +914,7 @@ extension ChatControllerImpl { } else { banDescription = strongSelf.presentationInterfaceState.strings.Conversation_DefaultRestrictedMedia } - + let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] items.append(ActionSheetTextItem(title: banDescription)) @@ -943,10 +940,10 @@ extension ChatControllerImpl { }) ])]) strongSelf.present(actionSheet, in: .window(.root)) - + return } - + let legacyController = LegacyController(presentation: .custom, theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) legacyController.blocksBackgroundWhenInOverlay = true legacyController.acceptsFocusWhenInOverlay = true @@ -954,14 +951,14 @@ extension ChatControllerImpl { legacyController.controllerLoaded = { [weak legacyController] in legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true } - + let emptyController = LegacyEmptyController(context: legacyController.context)! let navigationController = makeLegacyNavigationController(rootController: emptyController) navigationController.setNavigationBarHidden(true, animated: false) legacyController.bind(controller: navigationController) - + legacyController.enableSizeClassSignal = true - + let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText let menuEditMediaOptions = editMediaOptions.flatMap { options -> LegacyAttachmentMenuMediaEditing in var result: LegacyAttachmentMenuMediaEditing = .none @@ -970,7 +967,7 @@ extension ChatControllerImpl { } return result } - + var slowModeEnabled = false var hasSchedule = false if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { @@ -979,7 +976,7 @@ extension ChatControllerImpl { } hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat && strongSelf.presentationInterfaceState.sendPaidMessageStars == nil } - + let controller = legacyAttachmentMenu( context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, @@ -1005,14 +1002,14 @@ extension ChatControllerImpl { if let strongSelf = self { var enablePhoto = true var enableVideo = true - + if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { enableVideo = false } - + var bannedSendPhotos: (Int32, Bool)? var bannedSendVideos: (Int32, Bool)? - + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { if let channel = peer as? TelegramChannel { if let value = channel.hasBannedPermission(.banSendPhotos) { @@ -1030,26 +1027,26 @@ extension ChatControllerImpl { } } } - + if bannedSendPhotos != nil { enablePhoto = false } if bannedSendVideos != nil { enableVideo = false } - + var storeCapturedPhotos = false var hasSchedule = false if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { storeCapturedPhotos = peer.id.namespace != Namespaces.Peer.SecretChat - + hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat && strongSelf.presentationInterfaceState.sendPaidMessageStars == nil } - + presentedLegacyCamera(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: menuController, parentController: strongSelf, editingMedia: editMediaOptions != nil, saveCapturedPhotos: storeCapturedPhotos, mediaGrouping: true, initialCaption: inputText, hasSchedule: hasSchedule, enablePhoto: enablePhoto, enableVideo: enableVideo, sendMessagesWithSignals: { [weak self] signals, _, _, _ in if let strongSelf = self { strongSelf.editMessageMediaWithLegacySignals(signals!) - + if !inputText.string.isEmpty { strongSelf.clearInputText() } @@ -1062,10 +1059,10 @@ extension ChatControllerImpl { } }, presentSchedulePicker: { [weak self] _, done in if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time, repeatPeriod in + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] result in if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + done(result.time, result.silentPosting) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { strongSelf.openScheduledMessages() } } @@ -1119,10 +1116,10 @@ extension ChatControllerImpl { })], actionLayout: .vertical), in: .window(.root)) }, presentSchedulePicker: { [weak self] _, done in if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time, repeatPeriod in + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] result in if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + done(result.time, result.silentPosting) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { strongSelf.openScheduledMessages() } } @@ -1164,11 +1161,11 @@ extension ChatControllerImpl { controller.customRemoveFromParentViewController = { [weak legacyController] in legacyController?.dismiss() } - + legacyController.blocksBackgroundWhenInOverlay = true strongSelf.present(legacyController, in: .window(.root)) controller.present(in: emptyController, sourceView: nil, animated: true) - + let presentationDisposable = strongSelf.updatedPresentationData.1.startStrict(next: { [weak controller] presentationData in if let controller = controller { controller.pallete = legacyMenuPaletteFromTheme(presentationData.theme, forceDark: false) @@ -1177,7 +1174,7 @@ extension ChatControllerImpl { legacyController.disposables.add(presentationDisposable) }) } - + func presentFileGallery(editingMessage: Bool = false) { self.presentOldMediaPicker(fileMode: true, editingMedia: editingMessage, completion: { [weak self] signals, silentPosting, scheduleTime in if editingMessage { @@ -1187,7 +1184,7 @@ extension ChatControllerImpl { } }) } - + func presentICloudFileGallery(editingMessage: Bool = false, documentTypes: [String] = ["public.item"]) { let _ = (self.context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId), @@ -1200,7 +1197,7 @@ extension ChatControllerImpl { } let (accountPeer, limits, premiumLimits) = result let isPremium = accountPeer?.isPremium ?? false - + strongSelf.present(legacyICloudFilePicker(theme: strongSelf.presentationData.theme, hasMultiselection: true, documentTypes: documentTypes, completion: { [weak self] urls in if let strongSelf = self, !urls.isEmpty { var signals: [Signal] = [] @@ -1211,7 +1208,7 @@ extension ChatControllerImpl { |> deliverOnMainQueue).startStrict(next: { results in if let strongSelf = self { let replyMessageSubject = strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject - + for item in results { if let item = item { if item.fileSize > Int64(premiumLimits.maxUploadFileParts) * 512 * 1024 { @@ -1235,7 +1232,7 @@ extension ChatControllerImpl { } } } - + var groupingKey: Int64? var fileTypes: (music: Bool, other: Bool) = (false, false) if results.count > 1 { @@ -1253,14 +1250,14 @@ extension ChatControllerImpl { if fileTypes.music != fileTypes.other { groupingKey = Int64.random(in: Int64.min ... Int64.max) } - + var messages: [EnqueueMessage] = [] for item in results { if let item = item { let fileId = Int64.random(in: Int64.min ... Int64.max) let mimeType = guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension) var previewRepresentations: [TelegramMediaImageRepresentation] = [] - if mimeType.hasPrefix("image/") || mimeType == "application/pdf" { + if mimeType.hasPrefix("image/") || mimeType == "application/pdf" || item.audioMetadata?.hasAudioArtwork == true { previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } var attributes: [TelegramMediaFileAttribute] = [] @@ -1268,7 +1265,7 @@ extension ChatControllerImpl { if let audioMetadata = item.audioMetadata { attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil)) } - + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(item.fileSize), attributes: attributes, alternativeRepresentations: []) let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: strongSelf.chatLocation.threadId, replyToMessageId: replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: groupingKey, correlationId: nil, bubbleUpEmojiOrStickersets: []) messages.append(message) @@ -1277,7 +1274,7 @@ extension ChatControllerImpl { groupingKey = Int64.random(in: Int64.min ... Int64.max) } } - + if !messages.isEmpty { if editingMessage { strongSelf.editMessageMediaWithMessages(messages) @@ -1285,7 +1282,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() - + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } }) @@ -1305,7 +1302,7 @@ extension ChatControllerImpl { }), in: .window(.root)) }) } - + func presentFileMediaPickerOptions(editingMessage: Bool) { let actionSheet = ActionSheetController(presentationData: self.presentationData) actionSheet.setItemGroups([ActionSheetItemGroup(items: [ @@ -1329,7 +1326,7 @@ extension ChatControllerImpl { self.chatDisplayNode.dismissInput() self.present(actionSheet, in: .window(.root)) } - + func presentMediaPicker(subject: MediaPickerScreenImpl.Subject = .assets(nil, .default), saveEditedPhotos: Bool, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, enableMultiselection: Bool, present: @escaping (MediaPickerScreenImpl, AttachmentMediaPickerContext?) -> Void, updateMediaPickerContext: @escaping (AttachmentMediaPickerContext?) -> Void, completion: @escaping (Bool, [Any], Bool, Int32?, ChatSendMessageActionSheetController.SendParameters?, @escaping (String) -> UIView?, @escaping () -> Void) -> Void) { var isScheduledMessages = false if case .scheduledMessages = self.presentationInterfaceState.subject { @@ -1346,7 +1343,7 @@ extension ChatControllerImpl { peer: (self.presentationInterfaceState.renderedPeer?.peer).flatMap(EnginePeer.init), threadTitle: self.contentData?.state.threadInfo?.title, chatLocation: self.chatLocation, - isScheduledMessages: isScheduledMessages, + isScheduledMessages: isScheduledMessages, bannedSendPhotos: bannedSendPhotos, bannedSendVideos: bannedSendVideos, enableMultiselection: enableMultiselection, @@ -1372,10 +1369,10 @@ extension ChatControllerImpl { } controller.presentSchedulePicker = { [weak self] media, done in if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time, repeatPeriod in + strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] result in if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + done(result.time, result.silentPosting) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { strongSelf.openScheduledMessages() } } @@ -1409,7 +1406,7 @@ extension ChatControllerImpl { } else { return } - + let editorController = MediaEditorScreenImpl( context: self.context, mode: .coverEditor(dimensions: dimensions), @@ -1443,7 +1440,7 @@ extension ChatControllerImpl { } self.push(editorController) }, dismissed: { - + } ) (self.navigationController as? NavigationController)?.pushViewController(mainController, animated: true) @@ -1460,12 +1457,12 @@ extension ChatControllerImpl { } present(controller, mediaPickerContext) } - + func presentOldMediaPicker(fileMode: Bool, editingMedia: Bool, completion: @escaping ([Any], Bool, Int32) -> Void) { let engine = self.context.engine let _ = (self.context.sharedContext.accountManager.transaction { transaction -> Signal<(GeneratedMediaStoreSettings, EngineConfiguration.SearchBots), NoError> in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) - + return engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) |> map { configuration -> (GeneratedMediaStoreSettings, EngineConfiguration.SearchBots) in return (entry ?? GeneratedMediaStoreSettings.defaultSettings, configuration) @@ -1483,7 +1480,7 @@ extension ChatControllerImpl { selectionLimit = 10 slowModeEnabled = true } - + let _ = legacyAssetPicker(context: strongSelf.context, presentationData: strongSelf.presentationData, editingMedia: editingMedia, fileMode: fileMode, peer: peer, threadTitle: strongSelf.contentData?.state.threadInfo?.title, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, selectionLimit: selectionLimit).startStandalone(next: { generator in if let strongSelf = self { let legacyController = LegacyController(presentation: fileMode ? .navigation : .custom, theme: strongSelf.presentationData.theme, initialLayout: strongSelf.validLayout) @@ -1494,26 +1491,26 @@ extension ChatControllerImpl { legacyController?.view.disablesInteractiveModalDismiss = true } let controller = generator(legacyController.context) - + legacyController.bind(controller: controller) legacyController.deferScreenEdgeGestures = [.top] - + configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak self, weak legacyController] in if let strongSelf = self { - let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting in + let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting, scheduleTime in if let legacyController = legacyController { legacyController.dismiss() } legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { result in if let strongSelf = self { - strongSelf.enqueueChatContextResult(results, result, hideVia: true) + strongSelf.enqueueChatContextResult(results, result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime) } }, enqueueMediaMessages: { signals in if let strongSelf = self { if editingMedia { strongSelf.editMessageMediaWithLegacySignals(signals) } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting) + strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) } } }) @@ -1527,21 +1524,21 @@ extension ChatControllerImpl { guard let strongSelf = self else { return } - + let text: String if slowModeEnabled { text = strongSelf.presentationData.strings.Chat_SlowmodeAttachmentLimitReached } else { text = strongSelf.presentationData.strings.Chat_AttachmentLimitReached } - + strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }, presentSchedulePicker: { [weak self] media, done in if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time, repeatPeriod in + strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] result in if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + done(result.time, result.silentPosting) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { strongSelf.openScheduledMessages() } } @@ -1574,27 +1571,27 @@ extension ChatControllerImpl { }) }) } - + func presentWebSearch(editingMessage: Bool, attachment: Bool, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return } - + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) |> deliverOnMainQueue).startStandalone(next: { [weak self] configuration in if let strongSelf = self { - let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(attachment: attachment, completion: { [weak self] results, selectionState, editingState, silentPosting in + let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(attachment: attachment, completion: { [weak self] results, selectionState, editingState, silentPosting, scheduleTime in self?.attachmentController?.dismiss(animated: true, completion: nil) legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in if let strongSelf = self { - strongSelf.enqueueChatContextResult(results, result, hideVia: true) + strongSelf.enqueueChatContextResult(results, result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime) } }, enqueueMediaMessages: { [weak self] signals in if let strongSelf = self, !signals.isEmpty { if editingMessage { strongSelf.editMessageMediaWithLegacySignals(signals) } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting) + strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) } } }) @@ -1603,13 +1600,13 @@ extension ChatControllerImpl { guard let strongSelf, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else { return false } - + enum ItemType { case gif case image case video } - + var itemType: ItemType? switch item { case let .internalReference(reference): @@ -1629,11 +1626,11 @@ extension ChatControllerImpl { itemType = .video } } - + var bannedSendPhotos: (Int32, Bool)? var bannedSendVideos: (Int32, Bool)? var bannedSendGifs: (Int32, Bool)? - + if let channel = peer as? TelegramChannel { if let value = channel.hasBannedPermission(.banSendPhotos) { bannedSendPhotos = value @@ -1655,30 +1652,30 @@ extension ChatControllerImpl { bannedSendGifs = (Int32.max, false) } } - + if let itemType { switch itemType { case .image: if bannedSendPhotos != nil { strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - + return false } case .video: if bannedSendVideos != nil { strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - + return false } case .gif: if bannedSendGifs != nil { strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: strongSelf.restrictedSendingContentsText(), actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - + return false } } } - + return true } controller.getCaptionPanelView = { [weak strongSelf] in @@ -1688,7 +1685,7 @@ extension ChatControllerImpl { } }) } - + func presentLocationPicker() { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return @@ -1720,7 +1717,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() - + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } }) @@ -1733,7 +1730,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.dismissInput() }) } - + func presentContactPicker() { let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: self.context, updatedPresentationData: self.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: .always)) contactsController.navigationPresentation = .modal @@ -1748,11 +1745,11 @@ extension ChatControllerImpl { var media: TelegramMediaContact? switch peer { case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + guard case let .user(contact) = contact, let phoneNumber = contact.phone else { continue } let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - + let phone = contactData.basicData.phoneNumbers[0].value media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: contact.id, vCardData: nil) case let .deviceContact(_, basicData): @@ -1760,7 +1757,7 @@ extension ChatControllerImpl { continue } let contactData = DeviceContactExtendedData(basicData: basicData, middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") - + let phone = contactData.basicData.phoneNumbers[0].value media = TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: nil) } @@ -1770,7 +1767,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() - + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } }) @@ -1790,7 +1787,7 @@ extension ChatControllerImpl { let dataSignal: Signal<(Peer?, DeviceContactExtendedData?), NoError> switch peer { case let .peer(contact, _, _): - guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else { + guard case let .user(contact) = contact, let phoneNumber = contact.phone else { return } let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") @@ -1808,7 +1805,7 @@ extension ChatControllerImpl { } } } - + if let stableId = stableId { return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil)) |> take(1) @@ -1836,7 +1833,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() - + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } }) @@ -1850,7 +1847,7 @@ extension ChatControllerImpl { strongSelf.sendMessages([message], postpone: postpone) }) } else { - let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: strongSelf.context), environment: ShareControllerAppEnvironment(sharedContext: strongSelf.context.sharedContext), subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in + let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: strongSelf.context), environment: ShareControllerAppEnvironment(sharedContext: strongSelf.context.sharedContext), subject: .filter(peer: peerAndContactData.0.flatMap(EnginePeer.init), contactId: nil, contactData: contactData, completion: { peer, contactData in guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { return } @@ -1861,7 +1858,7 @@ extension ChatControllerImpl { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.chatDisplayNode.collapseInput() - + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } }) @@ -1884,7 +1881,7 @@ extension ChatControllerImpl { } })) } - + func getCaptionPanelView(isFile: Bool, hasTimer: Bool = true) -> TGCaptionPanelView? { var isScheduledMessages = false if case .scheduledMessages = self.presentationInterfaceState.subject { @@ -1899,9 +1896,11 @@ extension ChatControllerImpl { return } self.presentInGlobalOverlay(c) + }, getNavigationController: { [weak self] in + return self?.navigationController as? NavigationController }) as? TGCaptionPanelView } - + func openCamera(cameraView: TGAttachmentCameraView? = nil) { let _ = (self.context.sharedContext.accountManager.transaction { transaction -> GeneratedMediaStoreSettings in let entry = transaction.getSharedData(ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings)?.get(GeneratedMediaStoreSettings.self) @@ -1911,17 +1910,17 @@ extension ChatControllerImpl { guard let strongSelf = self else { return } - + var enablePhoto = true var enableVideo = true - + if let callManager = strongSelf.context.sharedContext.callManager as? PresentationCallManagerImpl, callManager.hasActiveCall { enableVideo = false } - + var bannedSendPhotos: (Int32, Bool)? var bannedSendVideos: (Int32, Bool)? - + if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { if let channel = peer as? TelegramChannel { if let value = channel.hasBannedPermission(.banSendPhotos) { @@ -1939,14 +1938,14 @@ extension ChatControllerImpl { } } } - + if bannedSendPhotos != nil { enablePhoto = false } if bannedSendVideos != nil { enableVideo = false } - + var storeCapturedMedia = false var hasSchedule = false if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer { @@ -1954,7 +1953,7 @@ extension ChatControllerImpl { hasSchedule = strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat && strongSelf.presentationInterfaceState.sendPaidMessageStars == nil } let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText - + presentedLegacyCamera(context: strongSelf.context, peer: strongSelf.presentationInterfaceState.renderedPeer?.peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: nil, parentController: strongSelf, attachmentController: self?.attachmentController, editingMedia: false, saveCapturedPhotos: storeCapturedMedia, mediaGrouping: true, initialCaption: inputText, hasSchedule: hasSchedule, enablePhoto: enablePhoto, enableVideo: enableVideo, sendPaidMessageStars: strongSelf.presentationInterfaceState.sendPaidMessageStars?.value ?? 0, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime, parameters in if let strongSelf = self { strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil, parameters: parameters) @@ -1970,10 +1969,10 @@ extension ChatControllerImpl { } }, presentSchedulePicker: { [weak self] _, done in if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time, repeatPeriod in + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] result in if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + done(result.time, result.silentPosting) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { strongSelf.openScheduledMessages() } } @@ -1994,10 +1993,10 @@ extension ChatControllerImpl { }) }) } - + func openStickerEditor() { self.chatDisplayNode.dismissInput() - + var dismissImpl: (() -> Void)? let mainController = self.context.sharedContext.makeStickerMediaPickerScreen( context: self.context, @@ -2026,7 +2025,7 @@ extension ChatControllerImpl { } else { subject = .single(.empty(PixelDimensions(width: 1080, height: 1920))) } - + let editorController = MediaEditorScreenImpl( context: self.context, mode: .stickerEditor(mode: .generic(canSend: true)), @@ -2050,7 +2049,7 @@ extension ChatControllerImpl { }, completion: { [weak self] results, commit in dismissImpl?() self?.chatDisplayNode.dismissInput() - + Queue.mainQueue().after(0.1) { commit({}) if case let .sticker(file, _) = results.first?.media { @@ -2082,7 +2081,7 @@ extension ChatControllerImpl { mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.push(mainController) } - + func configurePollCreation(isQuiz: Bool? = nil) -> ViewController? { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return nil @@ -2104,18 +2103,18 @@ extension ChatControllerImpl { self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let self { self.chatDisplayNode.collapseInput() - + self.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } }) } }, nil) - + var attributes: [MessageAttribute] = [] if !poll.description.entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: poll.description.entities)) } - + let message: EnqueueMessage = .message( text: poll.description.string, attributes: attributes, @@ -2137,7 +2136,10 @@ extension ChatControllerImpl { revotingDisabled: poll.revotingDisabled, shuffleAnswers: poll.shuffleAnswers, hideResultsUntilClose: poll.hideResultsUntilClose, - attachedMedia: poll.media?.media + isCreator: true, + attachedMedia: poll.media?.media, + restrictToSubscribers: poll.restrictToSubscribers, + countries: poll.limitToCountries )), threadId: self.chatLocation.threadId, replyToMessageId: nil, @@ -2151,7 +2153,7 @@ extension ChatControllerImpl { } ) } - + func configureTodoCreation() -> ViewController? { guard let peer = self.presentationInterfaceState.renderedPeer?.peer else { return nil @@ -2174,7 +2176,7 @@ extension ChatControllerImpl { self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let self { self.chatDisplayNode.collapseInput() - + self.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil) } }) @@ -2197,7 +2199,7 @@ extension ChatControllerImpl { } ) } - + func openTodoEditing(messageId: EngineMessage.Id, itemId: Int32?, append: Bool) { guard let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId), let peer = self.presentationInterfaceState.renderedPeer?.peer else { return @@ -2205,7 +2207,7 @@ extension ChatControllerImpl { guard let existingTodo = message.media.first(where: { $0 is TelegramMediaTodo }) as? TelegramMediaTodo else { return } - + guard self.context.isPremium else { let context = self.context var replaceImpl: ((ViewController) -> Void)? @@ -2219,9 +2221,9 @@ extension ChatControllerImpl { self.push(demoController) return } - + let canEdit = canEditMessage(context: self.context, limitsConfiguration: self.context.currentLimitsConfiguration.with { EngineConfiguration.Limits($0) }, message: message) - + let controller = ComposeTodoScreen( context: self.context, initialData: ComposeTodoScreen.initialData( @@ -2247,7 +2249,7 @@ extension ChatControllerImpl { } return true } - + if canEdit && !areItemsOnlyAppended(existing: existingTodo.items, updated: todo.items) { let _ = self.context.engine.messages.requestEditMessage( messageId: messageId, 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/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index ee684b7ffc..2be170fc78 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -188,9 +188,29 @@ extension ChatControllerImpl { } var dismissController: ((@escaping () -> Void) -> Void)? + let canDeleteReactions: Bool + if let channel = message.peers[message.id.peerId] as? TelegramChannel { + canDeleteReactions = channel.hasPermission(.deleteAllMessages) + } else { + canDeleteReactions = false + } + let canViewReactions = canViewMessageReactionList(message: message) + let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)? + if canDeleteReactions && canViewReactions { + deleteReaction = { [weak self] peer, _ in + dismissController?({ [weak self] in + guard let self else { + return + } + self.presentReactionDeletionOptions(author: peer._asPeer(), messageId: message.id) + }) + } + } else { + deleteReaction = nil + } var items: ContextController.Items - if canViewMessageReactionList(message: message) { + if canViewReactions { items = ContextController.Items(content: .custom(ReactionListContextMenuContent( context: self.context, displayReadTimestamps: true, @@ -198,7 +218,10 @@ extension ChatControllerImpl { animationCache: self.controllerInteraction!.presentationContext.animationCache, animationRenderer: self.controllerInteraction!.presentationContext.animationRenderer, message: EngineMessage(message), - reaction: value, readStats: nil, back: nil, openPeer: { peer, hasReaction in + reaction: value, + readStats: nil, + back: nil, + openPeer: { peer, hasReaction in dismissController?({ [weak self] in guard let self else { return @@ -206,7 +229,8 @@ extension ChatControllerImpl { self.openPeer(peer: peer, navigation: .default, fromMessage: MessageReference(message), fromReactionMessageId: hasReaction ? message.id : nil) }) - } + }, + deleteReaction: deleteReaction ))) } else { items = ContextController.Items(content: .list([])) @@ -370,6 +394,10 @@ extension ChatControllerImpl { } func openMessageSendStarsScreen(message: Message) { + guard canSendReactionsToChat(self.presentationInterfaceState) else { + return + } + if let current = self.currentSendStarsUndoController { self.currentSendStarsUndoController = nil current.dismiss() diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageShareMenu.swift index db0483902e..992b1807ae 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 @@ -93,6 +92,59 @@ func chatShareToSavedMessagesAdditionalView(_ chatController: ChatControllerImpl } extension ChatControllerImpl { +// func openMessageShareMenu(id: EngineMessage.Id) { +// guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first else { +// return +// } +// +// let chatPresentationInterfaceState = self.presentationInterfaceState +// var warnAboutPrivate = false +// var canShareToStory = false +// if case .peer = chatPresentationInterfaceState.chatLocation, let channel = message.peers[message.id.peerId] as? TelegramChannel { +// if case .broadcast = channel.info { +// canShareToStory = true +// if let message = messages.first, message.media.contains(where: { media in +// if media is TelegramMediaContact || media is TelegramMediaPoll || media is TelegramMediaTodo { +// return true +// } else if let file = media as? TelegramMediaFile, file.isSticker || file.isAnimatedSticker || file.isVideoSticker { +// return true +// } else { +// return false +// } +// }) { +// canShareToStory = false +// } +// if message.text.containsOnlyEmoji { +// canShareToStory = false +// } +// } +// if channel.addressName == nil { +// warnAboutPrivate = true +// } +// } +// +// let _ = warnAboutPrivate +// +//// let shareScreen = self.context.sharedContext.makeShareController( +//// context: self.context, +//// subject: .messages(messages), +//// forceExternal: false, +//// shareStory: canShareToStory ? { [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) +//// } +//// } : nil, +//// enqueued: nil, +//// actionCompleted: nil +//// ) +// self.chatDisplayNode.dismissInput() +// self.push(shareScreen) +// } + func openMessageShareMenu(id: EngineMessage.Id) { guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(id), let message = messages.first else { return @@ -109,9 +161,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 +175,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 +197,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 +241,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 +272,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/ChatControllerRemoveAd.swift b/submodules/TelegramUI/Sources/ChatControllerRemoveAd.swift index 60fc23d926..086744b174 100644 --- a/submodules/TelegramUI/Sources/ChatControllerRemoveAd.swift +++ b/submodules/TelegramUI/Sources/ChatControllerRemoveAd.swift @@ -1,7 +1,6 @@ import Foundation import TelegramPresentationData import AccountContext -import Postbox import TelegramCore import SwiftSignalKit import Display diff --git a/submodules/TelegramUI/Sources/ChatControllerScrollToPointInHistory.swift b/submodules/TelegramUI/Sources/ChatControllerScrollToPointInHistory.swift index 31220d20c2..6318be7cdc 100644 --- a/submodules/TelegramUI/Sources/ChatControllerScrollToPointInHistory.swift +++ b/submodules/TelegramUI/Sources/ChatControllerScrollToPointInHistory.swift @@ -159,7 +159,7 @@ extension ChatControllerImpl { let peerInfoController = self.context.sharedContext.makePeerInfoController( context: self.context, updatedPresentationData: self.updatedPresentationData, - peer: peer._asPeer(), + peer: peer, mode: .media(kind: kind, messageIndex: message.index), avatarInitiallyExpanded: false, fromChat: true, diff --git a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift index 0322eba0b9..e8f2b42b67 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift @@ -59,7 +59,8 @@ func chatHistoryEntriesForView( cachedData: CachedPeerData?, adMessage: Message?, dynamicAdMessages: [Message], - isMusicPlaylist: Bool + isMusicPlaylist: Bool, + pinToTopStableId: EngineMessage.StableId? ) -> ([ChatHistoryEntry], ChatHistoryEntriesForViewState) { var currentState = currentState @@ -144,6 +145,11 @@ func chatHistoryEntriesForView( var message = entry.message var isRead = entry.isRead + var pinToTop = false + if message.stableId == pinToTopStableId { + pinToTop = true + } + if pendingRemovedMessages.contains(message.id) { continue } @@ -258,7 +264,7 @@ func chatHistoryEntriesForView( isCentered = link.isCentered } - let attributes = ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: isCentered, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false) + let attributes = ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: isCentered, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false, pinToTop: pinToTop) let groupStableId = currentState.messageGroupStableId(messageStableId: message.stableId, groupId: messageGroupingKey, isLocal: Namespaces.Message.allLocal.contains(message.id.namespace)) var found = false @@ -304,7 +310,7 @@ func chatHistoryEntriesForView( isCentered = link.isCentered } - entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: isCentered, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false))) + entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: isCentered, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false, pinToTop: pinToTop))) } } else { let selection: ChatHistoryMessageSelection @@ -314,7 +320,7 @@ func chatHistoryEntriesForView( selection = .none } - entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false))) + entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false, pinToTop: pinToTop))) } } @@ -388,7 +394,7 @@ func chatHistoryEntriesForView( associatedThreadInfo: nil, associatedStories: [:] ) - entries.insert(.MessageEntry(serviceMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false)), at: index) + entries.insert(.MessageEntry(serviceMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false)), at: index) } for i in (0 ..< entries.count).reversed() { @@ -428,7 +434,7 @@ func chatHistoryEntriesForView( } } if let insertAtPosition { - entries.insert(.MessageEntry(joinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false)), at: insertAtPosition) + entries.insert(.MessageEntry(joinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false)), at: insertAtPosition) } } } @@ -496,12 +502,12 @@ func chatHistoryEntriesForView( if messages.count > 1, let groupingKey = messages[0].groupingKey { var groupMessages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)] = [] for message in messages { - groupMessages.append((message, false, .none, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false), nil)) + groupMessages.append((message, false, .none, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false, pinToTop: false), nil)) } entries.insert(.MessageGroupEntry(groupingKey, groupMessages, presentationData), at: 0) } else { if !hasTopicCreated { - entries.insert(.MessageEntry(messages[0], presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[messages[0].id], isPlaying: false, isCentered: false, authorStoryStats: messages[0].author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false)), at: 0) + entries.insert(.MessageEntry(messages[0], presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[messages[0].id], isPlaying: false, isCentered: false, authorStoryStats: messages[0].author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false, pinToTop: false)), at: 0) } } @@ -582,7 +588,7 @@ func chatHistoryEntriesForView( associatedThreadInfo: nil, associatedStories: [:] ) - entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false)), at: 0) + entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false)), at: 0) } if let chatPeer, let nameChangeDate = peerStatusSettings.nameChangeDate, nameChangeDate > 0 { @@ -622,7 +628,7 @@ func chatHistoryEntriesForView( associatedThreadInfo: nil, associatedStories: [:] ) - entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false)), at: 0) + entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false)), at: 0) } if let peer = chatPeer.flatMap(EnginePeer.init) { @@ -675,7 +681,7 @@ func chatHistoryEntriesForView( if !dynamicAdMessages.isEmpty { assert(entries.sorted() == entries) for message in dynamicAdMessages { - entries.append(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false))) + entries.append(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false))) } entries.sort() } @@ -710,7 +716,7 @@ func chatHistoryEntriesForView( associatedStories: message.associatedStories ) nextAdMessageId += 1 - entries.append(.MessageEntry(updatedMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false))) + entries.append(.MessageEntry(updatedMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false))) } } } @@ -772,7 +778,7 @@ func chatHistoryEntriesForView( associatedThreadInfo: nil, associatedStories: [:] ) - entries.append(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false))) + entries.append(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false))) } } @@ -829,7 +835,7 @@ func chatHistoryEntriesForView( associatedThreadInfo: nil, associatedStories: [:] ) - entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false)), at: 0) + entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false)), at: 0) } } } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 1348dedfb8..2ee2daf1e4 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1,11 +1,9 @@ import Foundation import UIKit -import Postbox import SwiftSignalKit import Display import AsyncDisplayKit import TelegramCore -import Postbox import TelegramPresentationData import TelegramUIPreferences import MediaResources @@ -38,6 +36,8 @@ import DustEffect import UrlHandling import TextFormat import ChatNewThreadInfoItem +import PhoneNumberFormat +import Postbox struct ChatTopVisibleMessageRange: Equatable { var lowerBound: MessageIndex @@ -366,12 +366,15 @@ private func extractAssociatedData( deviceContactsNumbers: Set, isInline: Bool, showSensitiveContent: Bool, - isSuspiciousPeer: Bool + isSuspiciousPeer: Bool, + accountCountry: String? ) -> ChatMessageItemAssociatedData { var automaticDownloadPeerId: EnginePeer.Id? var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel var contactsPeerIds: Set = Set() var channelDiscussionGroup: ChatMessageItemAssociatedData.ChannelDiscussionGroupStatus = .unknown + var isParticipant = false + var invitedOn: Int32? if case let .peer(peerId) = chatLocation { automaticDownloadPeerId = peerId @@ -395,11 +398,15 @@ private func extractAssociatedData( } else if peerId.namespace == Namespaces.Peer.CloudChannel { for entry in view.additionalData { if case let .peer(_, value) = entry { - if let channel = value as? TelegramChannel, case .group = channel.info { - automaticMediaDownloadPeerType = .group + if let channel = value as? TelegramChannel { + if case .group = channel.info { + automaticMediaDownloadPeerType = .group + } + isParticipant = channel.participationStatus == .member } } else if case let .cachedPeerData(dataPeerId, cachedData) = entry, dataPeerId == peerId { if let cachedData = cachedData as? CachedChannelData { + invitedOn = cachedData.invitedOn switch cachedData.linkedDiscussionPeerId { case let .known(value): channelDiscussionGroup = .known(value) @@ -421,7 +428,7 @@ private func extractAssociatedData( automaticDownloadPeerId = message.peerId } - return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, areStarReactionsEnabled: areStarReactionsEnabled, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent, isSuspiciousPeer: isSuspiciousPeer) + return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, areStarReactionsEnabled: areStarReactionsEnabled, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent, isSuspiciousPeer: isSuspiciousPeer, accountCountry: accountCountry, isParticipant: isParticipant, invitedOn: invitedOn) } private extension ChatHistoryLocationInput { @@ -763,6 +770,8 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH private let initTimestamp: Double + var pinToTopStableId: EngineMessage.StableId? + public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal), @@ -1169,10 +1178,13 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH }).startStrict() self.beganInteractiveDragging = { [weak self] _ in - self?.isInteractivelyScrollingValue = true - self?.isInteractivelyScrollingPromise.set(true) - self?.beganDragging?() - //self?.updateHistoryScrollingArea(transition: .immediate) + guard let self else { + return + } + self.isInteractivelyScrollingValue = true + self.isInteractivelyScrollingPromise.set(true) + //self.pinToTopStableId = nil + self.beganDragging?() } self.endedInteractiveDragging = { [weak self] _ in @@ -1682,16 +1694,16 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) ), peerReactionSettings, - self.context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings]) + self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.reactionSettings)) ) |> map { peer, peerReactionSettings, preferencesView -> (MessageReaction.Reaction?, Bool) in var areStarsEnabled: Bool = false if let peerReactionSettings, let value = peerReactionSettings.knownValue?.starsAllowed { areStarsEnabled = value } - + let reactionSettings: ReactionSettings - if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) { + if let entry = preferencesView, let value = entry.get(ReactionSettings.self) { reactionSettings = value } else { reactionSettings = .default @@ -1711,6 +1723,33 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH return peer } |> distinctUntilChanged + + let accountCountry: Signal + if let data = context.currentAppConfiguration.with({ $0 }).data, let country = data["phone_country_iso2"] as? String { + accountCountry = .single(country) + } else { + accountCountry = .single(nil) + |> then( + combineLatest( + accountPeer + |> map { peer -> String? in + if case let .user(user) = peer { + return user.phone + } else { + return nil + } + } + |> distinctUntilChanged, + (context as! AccountContextImpl).countriesConfiguration + ) + |> map { phone, countriesConfiguration in + guard let phone, let (country, _) = lookupCountryIdByNumber(phone, configuration: countriesConfiguration) else { + return nil + } + return country.id + } + ) + } let topicAuthorId: Signal if let peerId = chatLocation.peerId, let threadId = chatLocation.threadId { @@ -1853,6 +1892,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH savedMessageTags |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_savedMessageTags"), defaultReaction |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_defaultReaction"), accountPeer |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_accountPeer"), + accountCountry |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_accountCountry"), audioTranscriptionSuggestion |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_audioTranscriptionSuggestion"), promises |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_promises"), topicAuthorId |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_topicAuthorId"), @@ -1863,7 +1903,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH chatThemes |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_chatThemes"), deviceContactsNumbers |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_deviceContactsNumbers"), contentSettings |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_contentSettings") - ) |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_firstChatHistoryTransition")).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings in + ) |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_firstChatHistoryTransition")).startStrict(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, accountCountry, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings in let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises if measure_isFirstTime { @@ -2107,12 +2147,35 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH isSuspiciousPeer = true } - let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction.0, areStarReactionsEnabled: defaultReaction.1, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage?.toLang, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"), isSuspiciousPeer: isSuspiciousPeer) + let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction.0, areStarReactionsEnabled: defaultReaction.1, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage?.toLang, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"), isSuspiciousPeer: isSuspiciousPeer, accountCountry: accountCountry) var includeEmbeddedSavedChatInfo = false if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId, !rotated { includeEmbeddedSavedChatInfo = true } + + var pinToTopStableId: EngineMessage.StableId? + var scrollToPinToTopStableId = false + if let strongSelf = self { + for i in (0 ..< view.entries.count).reversed() { + let message = view.entries[i].message + if message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) { + for j in (0 ..< i).reversed() { + let otherMessage = view.entries[j].message + if !otherMessage.effectivelyIncoming(context.account.peerId) && otherMessage.id.namespace == Namespaces.Message.Cloud { + if strongSelf.pinToTopStableId != otherMessage.stableId { + strongSelf.pinToTopStableId = otherMessage.stableId + scrollToPinToTopStableId = true + } + } + break + } + break + } + } + + pinToTopStableId = strongSelf.pinToTopStableId + } let previousChatHistoryEntriesForViewState = chatHistoryEntriesForViewState.with({ $0 }) let (filteredEntries, updatedChatHistoryEntriesForViewState) = chatHistoryEntriesForView( @@ -2141,7 +2204,8 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH cachedData: data.cachedData, adMessage: allAdMessages.fixed, dynamicAdMessages: allAdMessages.opportunistic, - isMusicPlaylist: isMusicPlaylist + isMusicPlaylist: isMusicPlaylist, + pinToTopStableId: pinToTopStableId ) let lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0 let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id, locationInput: update.2, ignoreMessagesInTimestampRange: update.3, ignoreMessageIds: update.4) @@ -2365,6 +2429,17 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH keyboardButtonsMessage = nil } + if scrollToPinToTopStableId, let strongSelf = self, let pinToTopStableId = strongSelf.pinToTopStableId { + for entry in processedView.filteredEntries.reversed() { + if case let .MessageEntry(message, _, _, _, _, _) = entry { + if message.stableId == pinToTopStableId { + updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(message.index), quote: nil), position: .top(0.0), directionHint: .Up, animated: true, highlight: false, displayLink: false, setupReply: false) + break + } + } + } + } + let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, source: source, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: !isSavedMusic || forceUpdateAll) var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, isSavedMusic: isSavedMusic, canReorder: processedView.filteredEntries.count > 1 && canReorder, animateFromPreviousFilter: resetScrolling, transition: rawTransition, systemStyle: systemStyle) @@ -2465,10 +2540,10 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH } private func beginPresentationDataManagement(updated: Signal) { - let appConfiguration = self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + let appConfiguration = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) |> take(1) |> map { view in - return view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue + return view?.get(AppConfiguration.self) ?? .defaultValue } var didSetPresentationData = false @@ -3337,6 +3412,11 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH if self.chatHistoryLocationValue?.content != locationInput { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: locationInput, id: self.takeNextHistoryLocationId()) } + } else if historyView.originalView.holeEarlier, case let .custom(_, _, _, _, _, loadMore) = self.source, let loadMore { + if self.chatHistoryLocationValue?.content != locationInput { + self.chatHistoryLocationValue = ChatHistoryLocationInput(content: locationInput, id: self.takeNextHistoryLocationId()) + loadMore() + } } else if case let .customChatContents(customChatContents) = self.subject, case .hashTagSearch = customChatContents.kind { if self.chatHistoryLocationValue?.content != locationInput { self.chatHistoryLocationValue = ChatHistoryLocationInput(content: locationInput, id: self.takeNextHistoryLocationId()) @@ -4324,16 +4404,16 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH if foundCorrelationMessage { self.layoutActionOnViewTransition = nil let (mappedTransition, updateSizeAndInsets) = layoutActionOnViewTransition(transition) - self.transaction(deleteIndices: mappedTransition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: mappedTransition.options, scrollToItem: mappedTransition.scrollToItem, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: mappedTransition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: { result in + self.transaction(deleteIndices: mappedTransition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: mappedTransition.options.union(.Synchronous), scrollToItem: mappedTransition.scrollToItem, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: mappedTransition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: { result in completion(true, result) }) } else { - self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: { result in + self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options.union(.Synchronous), scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: { result in completion(false, result) }) } } else { - self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: { result in + self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options.union(.Synchronous), scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: { result in completion(false, result) }) } @@ -4669,7 +4749,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH } } - func requestMessageUpdate(_ id: MessageId, andScrollToItem scroll: Bool = false) { + func requestMessageUpdate(_ id: MessageId, andScrollToItem scroll: Bool = false, customTransition: ControlledTransition? = nil) { if let historyView = self.historyView { var messageItem: ChatMessageItem? self.forEachItemNode({ itemNode in @@ -4715,7 +4795,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH scrollToItem = ListViewScrollToItem(index: index, position: .center(.top), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down, displayLink: true) } - self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion], scrollToItem: scrollToItem, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion, .Synchronous], scrollToItem: scrollToItem, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, customAnimationTransition: customTransition, updateOpaqueState: nil, completion: { _ in }) break loop } case let .MessageGroupEntry(_, messages, presentationData): @@ -4736,7 +4816,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH scrollToItem = ListViewScrollToItem(index: index, position: .center(.top), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down, displayLink: true) } - self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion], scrollToItem: scrollToItem, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion, .Synchronous], scrollToItem: scrollToItem, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, customAnimationTransition: customTransition, updateOpaqueState: nil, completion: { _ in }) break loop } default: @@ -4787,7 +4867,7 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH item = ListMessageItem(presentationData: presentationData, systemStyle: self.systemStyle, context: self.context, chatLocation: self.chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: self.controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch) } let updateItem = ListViewUpdateItem(index: index, previousIndex: index, item: item, directionHint: nil) - self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion], scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion, .Synchronous], scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) break loop } default: diff --git a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift index 959b23e6bc..059a2934cc 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryViewForLocation.swift @@ -445,6 +445,22 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea } if preload { + var channelMessage: Signal = .single(Void()) + if case .channelPost = subject, let channelMessageId = replyThreadMessage.channelMessageId { + channelMessage = context.engine.messages.getMessagesLoadIfNecessary([channelMessageId], strategy: .cloud(skipLocal: false)) + |> mapToSignal { result -> Signal in + switch result { + case .progress: + return .never() + case .result: + return .single(Void()) + } + } + |> `catch` { _ -> Signal in + return .single(Void()) + } + } + let preloadSignal = preloadedChatHistoryViewForLocation( input, context: context, @@ -455,8 +471,8 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea tag: nil, additionalData: [] ) - return preloadSignal - |> map { historyView -> Bool? in + return combineLatest(preloadSignal, channelMessage) + |> map { historyView, _ -> Bool? in switch historyView { case .Loading: return nil diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift index 0944f199b8..66ccd16977 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift @@ -27,6 +27,17 @@ private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult } } +private func hasBannedInlineContent(chatPresentationInterfaceState: ChatPresentationInterfaceState) -> Bool { + if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel { + let canBypass = canBypassRestrictions(chatPresentationInterfaceState: chatPresentationInterfaceState) + return channel.hasBannedPermission(.banSendInline, ignoreDefault: canBypass) != nil + } else if let group = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramGroup { + return group.hasBannedPermission(.banSendInline) + } else { + return false + } +} + func textInputContextPanel(context: AccountContext, chatPresentationInterfaceState: ChatPresentationInterfaceState, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, currentPanel: ChatInputContextPanelNode?) -> ChatInputContextPanelNode? { guard let controllerInteraction else { return nil @@ -45,15 +56,8 @@ func textInputContextPanel(context: AccountContext, chatPresentationInterfaceSta }).first else { return nil } - - var hasBannedInlineContent = false - if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banSendInline) != nil { - hasBannedInlineContent = true - } else if let group = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banSendInline) { - hasBannedInlineContent = true - } - - if hasBannedInlineContent { + + if hasBannedInlineContent(chatPresentationInterfaceState: chatPresentationInterfaceState) { switch inputQueryResult { case .stickers, .contextRequestResult: if let currentPanel = currentPanel as? DisabledContextResultsChatInputContextPanelNode { @@ -183,15 +187,8 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa }).first else { return nil } - - var hasBannedInlineContent = false - if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banSendInline) != nil { - hasBannedInlineContent = true - } else if let group = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banSendInline) { - hasBannedInlineContent = true - } - - if hasBannedInlineContent { + + if hasBannedInlineContent(chatPresentationInterfaceState: chatPresentationInterfaceState) { switch inputQueryResult { case .stickers, .contextRequestResult: if let currentPanel = currentPanel as? DisabledContextResultsChatInputContextPanelNode { @@ -305,4 +302,3 @@ func chatOverlayContextPanelForChatPresentationIntefaceState(_ chatPresentationI return nil } - diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 023a5d3e98..481efce7f5 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import TelegramCore -import Postbox import Display import AccountContext import Emoji @@ -221,7 +220,7 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte var stickersEnabled = true var stickersAreEmoji = !isTextEmpty if let peer = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel { - if isTextEmpty, case .broadcast = peer.info, canSendMessagesToPeer(peer) { + if isTextEmpty, case .broadcast = peer.info, canSendMessagesToPeer(EnginePeer(peer)) { accessoryItems.append(.silentPost(chatPresentationInterfaceState.interfaceState.silentPosting)) } if let boostsToUnrestrict = chatPresentationInterfaceState.boostsToUnrestrict, boostsToUnrestrict > 0 { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift index 68e8f2aa23..a7483d8662 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import AsyncDisplayKit import TelegramCore -import Postbox import AccountContext import ChatPresentationInterfaceState import ChatControllerInteraction diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift index a40d494a9c..2cdd37cd44 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateAccessoryPanels.swift @@ -230,6 +230,9 @@ func textInputAccessoryPanel( previousTapTimestamp = CFAbsoluteTimeGetCurrent() interfaceInteraction?.presentReplyOptions(sourceView) }, + longPressAction: { _ in + interfaceInteraction?.navigateToMessage(replyMessageSubject.messageId, false, true, ChatLoadingMessageSubject.generic) + }, dismiss: { _ in interfaceInteraction?.setupReplyMessage(nil, nil, { _, f in f() }) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 798844b9d7..ee3c2eeaee 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -44,7 +44,7 @@ private struct MessageContextMenuData { let canPin: Bool let canEdit: Bool let canSelect: Bool - let resourceStatus: MediaResourceStatus? + let resourceStatus: EngineMediaResource.FetchStatus? let messageActions: ChatAvailableMessageActions } @@ -484,8 +484,13 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else { return .single(ContextController.Items(content: .list([]))) } - if let message = messages.first, message.id.namespace < 0 { - return .single(ContextController.Items(content: .list([]))) + if let message = messages.first { + if message.id.namespace < 0 { + return .single(ContextController.Items(content: .list([]))) + } + if message.id.namespace == Namespaces.Message.Local && message.attributes.contains(where: { $0 is TypingDraftMessageAttribute }) { + return .single(ContextController.Items(content: .list([]))) + } } var isEmbeddedMode = false @@ -808,9 +813,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState |> map(Optional.init) } - var loadResourceStatusSignal: Signal = .single(nil) + var loadResourceStatusSignal: Signal = .single(nil) if let loadCopyMediaResource = loadCopyMediaResource { - loadResourceStatusSignal = context.account.postbox.mediaBox.resourceStatus(loadCopyMediaResource) + loadResourceStatusSignal = context.engine.resources.status(resource: EngineMediaResource(loadCopyMediaResource)) |> take(1) |> map(Optional.init) } @@ -1312,10 +1317,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if resourceAvailable { for media in message.media { if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { - let _ = (context.account.postbox.mediaBox.resourceData(largest.resource, option: .incremental(waitUntilFetchStatus: false)) + let _ = (context.engine.resources.data(resource: EngineMediaResource(largest.resource), incremental: true) |> take(1) |> deliverOnMainQueue).startStandalone(next: { data in - if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + if data.isComplete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { if let image = UIImage(data: imageData) { if !messageText.isEmpty { copyTextWithEntities() @@ -1354,7 +1359,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState showTranslateIfTopical = true } - let (canTranslate, _) = canTranslateText(context: context, text: messageText, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: showTranslateIfTopical, ignoredLanguages: translationSettings.ignoredLanguages) + var (canTranslate, _) = canTranslateText(context: context, text: messageText, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: showTranslateIfTopical, ignoredLanguages: translationSettings.ignoredLanguages) + if let peerId = chatPresentationInterfaceState.chatLocation.peerId, peerId.namespace == Namespaces.Peer.SecretChat { + canTranslate = false + } if canTranslate { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuTranslate, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Translate"), color: theme.actionSheet.primaryTextColor) @@ -1411,7 +1419,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.append(.action(ContextMenuActionItem(text: isVideo ? chatPresentationInterfaceState.strings.Gallery_SaveVideo : chatPresentationInterfaceState.strings.Gallery_SaveImage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in - let _ = (saveToCameraRoll(context: context, postbox: context.account.postbox, userLocation: .peer(message.id.peerId), mediaReference: mediaReference) + let _ = (saveToCameraRoll(context: context, userLocation: .peer(message.id.peerId), mediaReference: mediaReference) |> deliverOnMainQueue).startStandalone(completed: { Queue.mainQueue().after(0.2) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -1655,7 +1663,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } - if let activePoll = activePoll, messages[0].forwardInfo == nil { + if let activePoll, messages[0].forwardInfo == nil { var canStopPoll = false if !messages[0].flags.contains(.Incoming) { canStopPoll = true @@ -1862,6 +1870,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } var clearCacheAsDelete = false + var hasViewStats = false if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, !isMigrated { var views: Int = 0 var forwards: Int = 0 @@ -1882,11 +1891,28 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState controllerInteraction.openMessageStats(messages[0].id) }) }))) + hasViewStats = true } clearCacheAsDelete = true } + if !hasViewStats, messages[0].forwardInfo == nil { + for media in message.media { + if let poll = media as? TelegramMediaPoll, message.id.namespace == Namespaces.Message.Cloud, poll.pollId.namespace == Namespaces.Media.CloudPoll, poll.results.canViewStats { + actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ViewPollStats, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.actionSheet.primaryTextColor) + }, action: { c, _ in + c?.dismiss(completion: { + let controller = context.sharedContext.makePollStatsScreen(context: context, messageId: messages[0].id) + controllerInteraction.navigationController()?.pushViewController(controller) + }) + }))) + break + } + } + } + if message.id.namespace == Namespaces.Message.Cloud, let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, canEditFactCheck(appConfig: appConfig) { var canAddFactCheck = true if message.media.contains(where: { $0 is TelegramMediaAction || $0 is TelegramMediaGiveaway }) { @@ -2147,6 +2173,20 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState displayReadTimestamps = true } + let deleteReaction: ((EnginePeer, MessageReaction.Reaction) -> Void)? + if let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.hasPermission(.deleteAllMessages) { + deleteReaction = { [weak c] peer, _ in + c?.dismiss(completion: { + guard let chatController = interfaceInteraction.chatController() as? ChatController else { + return + } + chatController.presentReactionDeletionOptions(author: peer._asPeer(), messageId: message.id) + }) + } + } else { + deleteReaction = nil + } + c.pushItems(items: .single(ContextController.Items(content: .custom(ReactionListContextMenuContent( context: context, displayReadTimestamps: displayReadTimestamps, @@ -2163,7 +2203,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState c?.dismiss(completion: { controllerInteraction.openPeer(peer, .default, MessageReference(message), hasReaction ? .reaction : .default) }) - } + }, + deleteReaction: deleteReaction )), tip: tip))) } else { f(.default) @@ -2261,6 +2302,55 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } + for media in message.media { + if let poll = media as? TelegramMediaPoll, message.id.namespace == Namespaces.Message.Cloud, poll.pollId.namespace == Namespaces.Media.CloudPoll { + var restrictionText: String = "" + let peerName: String = chatPresentationInterfaceState.renderedPeer?.peer.flatMap(EnginePeer.init)?.compactDisplayTitle ?? "" + + if !poll.countries.isEmpty { + let locale = localeWithStrings(chatPresentationInterfaceState.strings) + let countryNames = poll.countries.map { id in + if id == "FT" { + return "Fragment" + } else if let countryName = locale.localizedString(forRegionCode: id) { + return countryName + } else { + return id + } + } + var countries: String = "" + if countryNames.count == 1, let country = countryNames.first { + countries = "**\(country)**" + } else { + for i in 0 ..< countryNames.count { + countries.append("**\(countryNames[i])**") + if i == countryNames.count - 2 { + countries.append(chatPresentationInterfaceState.strings.Chat_Poll_Restriction_Country_CountriesLastDelimiter) + } else if i < countryNames.count - 2 { + countries.append(chatPresentationInterfaceState.strings.Chat_Poll_Restriction_Country_CountriesDelimiter) + } + } + } + if poll.restrictToSubscribers { + restrictionText = chatPresentationInterfaceState.strings.Chat_Poll_Restriction_SubscribersCountry(peerName, countries).string + } else { + restrictionText = chatPresentationInterfaceState.strings.Chat_Poll_Restriction_Country(countries).string + } + } else if poll.restrictToSubscribers { + restrictionText = chatPresentationInterfaceState.strings.Chat_Poll_Restriction_Subscribers(peerName).string + } + + if !restrictionText.isEmpty { + actions.append(.separator) + let noAction: ((ContextMenuActionItem.Action) -> Void)? = nil + actions.append( + .action(ContextMenuActionItem(text: restrictionText, textLayout: .multiline, textFont: .small, parseMarkdown: true, icon: { _ in return nil }, action: noAction)) + ) + } + break + } + } + return ContextController.Items(content: .list(actions), tip: nil) } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift index 06271060c2..2c880d7c8e 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextQueries.swift @@ -69,9 +69,9 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee signal = .single({ _ in return .stickers([]) }) } - let stickerConfiguration = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + let stickerConfiguration = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) |> map { preferencesView -> StickersSearchConfiguration in - let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue + let appConfiguration: AppConfiguration = preferencesView?.get(AppConfiguration.self) ?? .defaultValue return StickersSearchConfiguration.with(appConfiguration: appConfiguration) } let stickerSettings = context.sharedContext.accountManager.transaction { transaction -> StickerSettings in 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 diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index 82c58604fa..c74d28f32b 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import AsyncDisplayKit -import Postbox import TelegramCore import TelegramPresentationData import AccountContext @@ -47,10 +46,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: strings.Conversation_ClearAll, 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: strings.Conversation_ClearCache, style: .plain, target: target, action: selector) + buttonItem.accessibilityLabel = title + return ChatNavigationButton(action: .clearCache, buttonItem: buttonItem) } } } @@ -74,6 +77,9 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha } func rightNavigationButtonForChatInterfaceState(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, strings: PresentationStrings, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?, chatInfoNavigationButton: ChatNavigationButton?, moreInfoNavigationButton: ChatNavigationButton?) -> ChatNavigationButton? { + if case .standard(.previewing) = presentationInterfaceState.mode { + return nil + } var hasMessages = false if let chatHistoryState = presentationInterfaceState.chatHistoryState { if case .loaded(false, _) = chatHistoryState { @@ -88,7 +94,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/TelegramUI/Sources/ChatLinkPreview.swift b/submodules/TelegramUI/Sources/ChatLinkPreview.swift index 7dd4ac4884..643451dd21 100644 --- a/submodules/TelegramUI/Sources/ChatLinkPreview.swift +++ b/submodules/TelegramUI/Sources/ChatLinkPreview.swift @@ -1,5 +1,4 @@ import Foundation -import Postbox import TelegramCore import SwiftSignalKit import TelegramNotices diff --git a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift index c9dae58747..ee3f739586 100644 --- a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift @@ -353,7 +353,7 @@ final class ChatManagingBotTitlePanelNode: ChatTitleAccessoryPanelNode { guard let navigationController = chatController.navigationController as? NavigationController else { return } - if let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { navigationController.pushViewController(controller) } }) diff --git a/submodules/TelegramUI/Sources/ChatMessageReportInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatMessageReportInputPanelNode.swift index 48f96157ca..e9642778f8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReportInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReportInputPanelNode.swift @@ -72,7 +72,7 @@ final class ChatMessageReportInputPanelNode: ChatInputPanelNode { } } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState diff --git a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift index 82217835b6..f9673954be 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift @@ -31,6 +31,64 @@ private func convertAnimatingSourceRect(_ rect: CGRect, fromView: UIView, toView } } +private func pendingAdditiveSublayerTranslation(_ layer: CALayer) -> CGPoint { + guard let keys = layer.animationKeys() else { return .zero } + var result = CGPoint.zero + for key in keys { + guard let anim = layer.animation(forKey: key) as? CABasicAnimation, + anim.keyPath == "sublayerTransform", + anim.isAdditive, + let fromValue = anim.fromValue as? NSValue else { + continue + } + let t = fromValue.caTransform3DValue + result.x += t.m41 + result.y += t.m42 + } + return result +} + +/// Convert a rect expressed in window coordinates to `toView`'s local coordinates, +/// accounting for pending additive `sublayerTransform` animations on any ancestor +/// of `toView`. Returns the position in `toView.bounds` that will render at +/// `windowRect` visually at t=0 of the pending animations (once CA commits them). +/// +/// Standard `toView.layer.convert(windowRect, from: nil)` reads model transforms +/// only, so it yields the position that will render at `windowRect` *at t=end* of +/// any pending additive animations (since additive animations don't change model +/// values). For source-side morph calibration, where the snapshot was captured at +/// pre-animation state, the t=0 position is what we want. +/// +/// Walks the layer chain top-down from the root to `toView`. At each parent→child +/// step it subtracts the parent's pending additive `sublayerTransform` translation +/// *in the parent's own bounds coord space*, then does the standard one-step +/// `convert(_:to:)` into the child. Applying the correction at the right level +/// (rather than flat-summing the translations in the destination space) lets +/// `CALayer.convert` propagate each correction through any remaining +/// transforms — child `transform`, further ancestors' own model +/// `sublayerTransform`, etc. — so the result is correct even when the chain +/// contains non-translation transforms (rotations, scales). +private func convertAnimatingSourceRectFromWindow(_ windowRect: CGRect, toView: UIView) -> CGRect { + var chain: [CALayer] = [] + var layer: CALayer? = toView.layer + while let cur = layer { + chain.append(cur) + layer = cur.superlayer + } + chain.reverse() + + var r = windowRect + for i in 0..<(chain.count - 1) { + let parent = chain[i] + let child = chain[i + 1] + + let pending = pendingAdditiveSublayerTranslation(parent) + let adjustedR = r.offsetBy(dx: -pending.x, dy: -pending.y) + r = parent.convert(adjustedR, to: child) + } + return r +} + private final class OverlayTransitionContainerNode: ViewControllerTracingNode { override init() { super.init() @@ -388,9 +446,11 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran func beginAnimation() { if let portalTargetView = self.portalTargetView { portalTargetView.view.alpha = 0.0 - portalTargetView.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15) + portalTargetView.view.layer.allowsGroupOpacity = true + portalTargetView.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.14, delay: 0.14) - self.portalSourceView.layer.animateAlpha(from: 0.01, to: 1.0, duration: 0.12) + self.portalSourceView.layer.allowsGroupOpacity = true + self.portalSourceView.layer.animateAlpha(from: 0.01, to: 1.0, duration: 0.1, delay: 0.12) } let verticalDuration: Double = ChatMessageTransitionNodeImpl.animationDuration @@ -428,12 +488,11 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view) - let sourceRect = self.view.convert(initialTextInput.sourceRect, from: nil) + let sourceRect = convertAnimatingSourceRectFromWindow(initialTextInput.sourceRect, toView: self.view) let sourceBackgroundAbsoluteRect = initialTextInput.backgroundView.frame.offsetBy(dx: sourceRect.minX, dy: sourceRect.minY) let sourceAbsoluteRect = CGRect(origin: CGPoint(x: sourceBackgroundAbsoluteRect.minX, y: sourceBackgroundAbsoluteRect.maxY - self.contextSourceNode.contentRect.height), size: self.contextSourceNode.contentRect.size) let textInput = ChatMessageTransitionNodeImpl.Source.TextInput(backgroundView: initialTextInput.backgroundView, contentView: initialTextInput.contentView, sourceRect: sourceRect, scrollOffset: initialTextInput.scrollOffset) - textInput.backgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: sourceAbsoluteRect.height - sourceBackgroundAbsoluteRect.height), size: textInput.backgroundView.bounds.size) textInput.contentView.frame = textInput.contentView.frame.offsetBy(dx: 0.0, dy: sourceAbsoluteRect.height - sourceBackgroundAbsoluteRect.height) @@ -443,7 +502,7 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran var replySourceAbsoluteFrame: CGRect if let storedFrameBeforeDismissed = replyPanel.storedFrameBeforeDismissed { - replySourceAbsoluteFrame = self.view.convert(storedFrameBeforeDismissed, from: nil) + replySourceAbsoluteFrame = convertAnimatingSourceRectFromWindow(storedFrameBeforeDismissed, toView: self.view) } else { replySourceAbsoluteFrame = replyPanelParentView.convert(replyPanelFrame, to: self.view) } @@ -477,6 +536,7 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY) self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) + self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: verticalCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in guard let strongSelf = self else { return @@ -484,6 +544,7 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran strongSelf.endAnimation() }) self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: horizontalCurve.mediaTimingFunction, additive: true) + self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), horizontalCurve, horizontalDuration) self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), verticalCurve, verticalDuration) @@ -1098,8 +1159,10 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran overlayController.displayNode.addSubnode(animatingItemNode) animatingItemNode.overlayController = overlayController self.listNode.context.sharedContext.mainWindow?.presentInGlobalOverlay(overlayController) + animatingItemNode.frame = self.bounds default: - self.addSubnode(animatingItemNode) + animatingItemNode.frame = CGRect() + itemNode.addSubnode(animatingItemNode) } animatingItemNode.animationEnded = { [weak self, weak animatingItemNode] in @@ -1120,7 +1183,6 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran } } - animatingItemNode.frame = self.bounds animatingItemNode.beginAnimation() self.onTransitionEvent(.animated(duration: ChatMessageTransitionNodeImpl.animationDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve)) @@ -1182,9 +1244,9 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran } func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?, isRotated: Bool) { - for animatingItemNode in self.animatingItemNodes { + /*for animatingItemNode in self.animatingItemNodes { animatingItemNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) - } + }*/ if itemNode == nil { for decorationItemNode in self.decorationItemNodes { decorationItemNode.addExternalOffset(offset: offset, transition: transition) @@ -1205,9 +1267,9 @@ public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTran } func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { - for animatingItemNode in self.animatingItemNodes { + /*for animatingItemNode in self.animatingItemNodes { animatingItemNode.addContentOffset(offset: offset, itemNode: itemNode) - } + }*/ if itemNode == nil { for decorationItemNode in self.decorationItemNodes { decorationItemNode.addContentOffset(offset: offset) diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 87181921ff..324fd0e7e6 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 } @@ -735,7 +735,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { if fileReference.media.isAnimatedSticker { let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512) updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0))) - updatedFetchMediaSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource)) + updatedFetchMediaSignal = context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media)) } else if fileReference.media.isVideo || fileReference.media.isAnimated { updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, blurred: hasSpoiler) } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { @@ -797,7 +797,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { messageText = NSAttributedString(string: foldLineBreaks(textString.string), font: textFont, textColor: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? theme.chat.inputPanel.primaryTextColor : theme.chat.inputPanel.secondaryTextColor) } - let textConstrainedSize = CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude) + let textConstrainedSize = CGSize(width: width - textLineInset - contentLeftInset - rightInset - textRightInset - 10.0, height: CGFloat.greatestFiniteMagnitude) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 0.0, bottom: 2.0, right: 0.0))) let spoilerTextLayoutAndApply: (TextNodeLayout, (TextNodeWithEntities.Arguments?) -> TextNodeWithEntities)? @@ -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/ChatPremiumRequiredInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatPremiumRequiredInputPanelNode.swift index 9cd52cba92..c4568f583a 100644 --- a/submodules/TelegramUI/Sources/ChatPremiumRequiredInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPremiumRequiredInputPanelNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import TelegramCore -import Postbox import SwiftSignalKit import TelegramNotices import TelegramPresentationData @@ -30,9 +29,10 @@ final class ChatPremiumRequiredInputPanelNode: ChatInputPanelNode { var isSecondary: Bool var interfaceState: ChatPresentationInterfaceState var metrics: LayoutMetrics + var deviceMetrics: DeviceMetrics var isMediaInputExpanded: Bool - init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) { + init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) { self.width = width self.leftInset = leftInset self.rightInset = rightInset @@ -43,6 +43,7 @@ final class ChatPremiumRequiredInputPanelNode: ChatInputPanelNode { self.isSecondary = isSecondary self.interfaceState = interfaceState self.metrics = metrics + self.deviceMetrics = deviceMetrics self.isMediaInputExpanded = isMediaInputExpanded } } @@ -74,8 +75,8 @@ final class ChatPremiumRequiredInputPanelNode: ChatInputPanelNode { deinit { } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { - let params = Params(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) -> CGFloat { + let params = Params(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, interfaceState: interfaceState, metrics: metrics, deviceMetrics: deviceMetrics, isMediaInputExpanded: isMediaInputExpanded) if let currentLayout = self.currentLayout, currentLayout.params == params { return currentLayout.height } diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 9f76e4bc68..22e43345cf 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import TelegramCore -import Postbox import SwiftSignalKit import TelegramStringFormatting import ChatPresentationInterfaceState @@ -81,7 +80,7 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.openBoostToUnrestrict() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState } diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsController.swift b/submodules/TelegramUI/Sources/ChatSearchResultsController.swift index c686bf1d6d..9e21477601 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsController.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsController.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import SwiftSignalKit import TelegramCore import TelegramPresentationData diff --git a/submodules/TelegramUI/Sources/ChatSearchState.swift b/submodules/TelegramUI/Sources/ChatSearchState.swift index bf886b8000..5b402de82e 100644 --- a/submodules/TelegramUI/Sources/ChatSearchState.swift +++ b/submodules/TelegramUI/Sources/ChatSearchState.swift @@ -1,5 +1,4 @@ import Foundation -import Postbox import TelegramCore struct ChatSearchState: Equatable { diff --git a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift index 3aa6cd9d48..7ad89bff3d 100644 --- a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift @@ -32,9 +32,10 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { var isSecondary: Bool var interfaceState: ChatPresentationInterfaceState var metrics: LayoutMetrics + var deviceMetrics: DeviceMetrics var isMediaInputExpanded: Bool - init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) { + init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) { self.width = width self.leftInset = leftInset self.rightInset = rightInset @@ -45,6 +46,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { self.isSecondary = isSecondary self.interfaceState = interfaceState self.metrics = metrics + self.deviceMetrics = deviceMetrics self.isMediaInputExpanded = isMediaInputExpanded } } @@ -110,16 +112,15 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { self.totalMessageCountDisposable?.dispose() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) -> CGFloat { var leftInset = leftInset + 8.0 var rightInset = rightInset + 8.0 - if bottomInset <= 32.0 { - leftInset += 18.0 - rightInset += 18.0 - } + let compactBottomSideInset = self.compactBottomSideInset(bottomInset: bottomInset, deviceMetrics: deviceMetrics) + leftInset += compactBottomSideInset + rightInset += compactBottomSideInset - let params = Params(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded) + let params = Params(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, maxOverlayHeight: maxOverlayHeight, isSecondary: isSecondary, interfaceState: interfaceState, metrics: metrics, deviceMetrics: deviceMetrics, isMediaInputExpanded: isMediaInputExpanded) if let currentLayout = self.currentLayout, currentLayout.params == params { return currentLayout.height } diff --git a/submodules/TelegramUI/Sources/ChatUnblockInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatUnblockInputPanelNode.swift index a344369297..8cce468833 100644 --- a/submodules/TelegramUI/Sources/ChatUnblockInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatUnblockInputPanelNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import TelegramCore -import Postbox import SwiftSignalKit import TelegramPresentationData import ChatPresentationInterfaceState @@ -91,7 +90,7 @@ final class ChatUnblockInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.unblockPeer() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState } diff --git a/submodules/TelegramUI/Sources/ChatVerifiedPeerTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatVerifiedPeerTitlePanelNode.swift index 5cd2fb706d..0a6a56d831 100644 --- a/submodules/TelegramUI/Sources/ChatVerifiedPeerTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatVerifiedPeerTitlePanelNode.swift @@ -2,7 +2,6 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import Postbox import TelegramCore import TelegramPresentationData import LocalizedPeerData @@ -78,7 +77,7 @@ final class ChatVerifiedPeerTitlePanelNode: ChatTitleAccessoryPanelNode { self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor } - var panelHeight: CGFloat = 8.0 + var panelHeight: CGFloat = 12.0 if let peer = interfaceState.renderedPeer?.peer, let verification = interfaceState.peerVerification { if isFirstTime { @@ -94,6 +93,7 @@ final class ChatVerifiedPeerTitlePanelNode: ChatTitleAccessoryPanelNode { let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: plainText, font: Font.regular(12.0), textColor: interfaceState.theme.rootController.navigationBar.secondaryTextColor, paragraphAlignment: .center)) attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: emojiStatus.fileId, file: nil), range: NSMakeRange(0, 1)) + attributedText.addAttribute(.baselineOffset, value: 1.0, range: NSMakeRange(0, 1)) if let entity = entities.first { let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) attributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: interfaceState.theme.rootController.navigationBar.accentTextColor, range: range) @@ -125,8 +125,8 @@ final class ChatVerifiedPeerTitlePanelNode: ChatTitleAccessoryPanelNode { placeholderColor: interfaceState.theme.list.mediaPlaceholderColor, attemptSynchronous: false )) - transition.updateFrame(node: emojiStatusTextNode.textNode, frame: CGRect(origin: CGPoint(x: floor((width - emojiStatusLayout.size.width) / 2.0), y: panelHeight), size: emojiStatusLayout.size)) - panelHeight += emojiStatusLayout.size.height + 8.0 + transition.updateFrame(node: emojiStatusTextNode.textNode, frame: CGRect(origin: CGPoint(x: floor((width - emojiStatusLayout.size.width) / 2.0), y: panelHeight + 1.0), size: emojiStatusLayout.size)) + panelHeight += emojiStatusLayout.size.height + 12.0 emojiStatusTextNode.visibilityRect = .infinite } diff --git a/submodules/TelegramUI/Sources/CommandChatInputPanelItem.swift b/submodules/TelegramUI/Sources/CommandChatInputPanelItem.swift index 8d27ec66e3..cc50e28b30 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputPanelItem.swift @@ -4,7 +4,6 @@ import AsyncDisplayKit import Display import TelegramCore import SwiftSignalKit -import Postbox import TelegramPresentationData import TelegramUIPreferences import AvatarNode diff --git a/submodules/TelegramUI/Sources/CommandMenuChatInputPanelItem.swift b/submodules/TelegramUI/Sources/CommandMenuChatInputPanelItem.swift index e27f05e592..1e823f781d 100644 --- a/submodules/TelegramUI/Sources/CommandMenuChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/CommandMenuChatInputPanelItem.swift @@ -4,7 +4,6 @@ import AsyncDisplayKit import Display import TelegramCore import SwiftSignalKit -import Postbox import TelegramPresentationData import TelegramUIPreferences import AccountContext diff --git a/submodules/TelegramUI/Sources/ComposeController.swift b/submodules/TelegramUI/Sources/ComposeController.swift index d4825816b6..cf07ca09e6 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 @@ -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 @@ -224,7 +226,7 @@ public class ComposeControllerImpl: ViewController, ComposeController { DeviceAccess.authorizeAccess(to: .contacts) default: let presentationData = strongSelf.presentationData - strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { self?.context.sharedContext.applicationBindings.openSettings() })]), in: .window(.root)) } diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index 3f2a25e270..c7965a6879 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -18,13 +18,13 @@ import PremiumUI import UndoUI import ContextUI -private func peerTokenTitle(accountPeerId: PeerId, peer: Peer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) -> String { +private func peerTokenTitle(accountPeerId: PeerId, peer: EnginePeer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) -> String { if peer.id == accountPeerId { return strings.DialogList_SavedMessages } else if peer.id.isReplies { return strings.DialogList_Replies } else { - return EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder) + return peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder) } } @@ -168,7 +168,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } } strongSelf.contactsNode.editableTokens.append(contentsOf: peers.map { peer -> EditableTokenListToken in - return EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: params.context.account.peerId, peer: peer._asPeer(), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)) + return EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: params.context.account.peerId, peer: peer, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)) }) strongSelf._peersReady.set(.single(true)) if strongSelf.isNodeLoaded { @@ -198,7 +198,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } let peers = peerList.compactMap { $0 } strongSelf.contactsNode.editableTokens.append(contentsOf: peers.map { peer -> EditableTokenListToken in - return EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: params.context.account.peerId, peer: peer._asPeer(), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)) + return EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: params.context.account.peerId, peer: peer, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)) }) strongSelf._peersReady.set(.single(true)) if strongSelf.isNodeLoaded { @@ -383,7 +383,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection displayCountAlert = true updatedState = updatedState.withToggledPeerId(.peer(peer.id)) } else { - addedToken = EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: accountPeerId, peer: peer, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(EnginePeer(peer))) + addedToken = EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: accountPeerId, peer: peer, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)) } } updatedCount = updatedState.selectedPeerIndices.count @@ -400,7 +400,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection state.selectedPeerIds.remove(peer.id) removedTokenId = peer.id } else { - addedToken = EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: accountPeerId, peer: peer, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(EnginePeer(peer))) + addedToken = EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: accountPeerId, peer: peer, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)) state.selectedPeerIds.insert(peer.id) } updatedCount = state.selectedPeerIds.count @@ -448,7 +448,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection if !self.params.initialSelectedPeers.isEmpty { for peer in self.params.initialSelectedPeers { - self.contactsNode.openPeer?(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil)) + self.contactsNode.openPeer?(.peer(peer: peer, isGlobal: false, participantCount: nil)) } /*if case let .contacts(contactsNode) = self.contactsNode.contentNode { contactsNode.updateSelectionState { state in @@ -478,7 +478,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection a(.default) if let self { - self.params.sendMessage?(EnginePeer(peer)) + self.params.sendMessage?(peer) } }))) @@ -488,7 +488,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection a(.default) if let self { - self.params.openProfile?(EnginePeer(peer)) + self.params.openProfile?(peer) } }))) @@ -745,7 +745,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection } for peer in peers { if !existingPeerIds.contains(peer.id) { - tokens.append(EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: self.context.account.peerId, peer: peer._asPeer(), strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer))) + tokens.append(EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: self.context.account.peerId, peer: peer, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer))) } } } else { diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 08c8d27e4d..5ff26d31e7 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) @@ -314,7 +314,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } case let .chats(chatsNode): chatsNode.peerSelected = { [weak self] peer, _, _, _, _ in - self?.openPeer?(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil)) + self?.openPeer?(.peer(peer: peer, isGlobal: false, participantCount: nil)) } chatsNode.additionalCategorySelected = { [weak self] id in guard let strongSelf = self else { @@ -489,7 +489,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } else if count <= 1 { let callTitle: String if case let .contacts(contactListNode) = self.contentNode, let peer = contactListNode.selectedPeers.first, case let .peer(peer, _, _) = peer { - callTitle = self.presentationData.strings.NewCall_ActionCallSingle(EnginePeer(peer).compactDisplayTitle).string + callTitle = self.presentationData.strings.NewCall_ActionCallSingle(peer.compactDisplayTitle).string } else { callTitle = self.presentationData.strings.NewCall_ActionCallMultiple } diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index 047253a186..b9a6e0977a 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -70,7 +70,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController private let isPeerEnabled: (ContactListPeer) -> Bool var dismissed: (() -> Void)? - var presentScheduleTimePicker: (@escaping (Int32, Int32?) -> Void) -> Void = { _ in } + var presentScheduleTimePicker: (@escaping (Int32, Int32?, Bool) -> Void) -> Void = { _ in } private let createActionDisposable = MetaDisposable() private let confirmationDisposable = MetaDisposable() @@ -514,7 +514,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController a(.default) if let self { - self.sendMessage?(EnginePeer(peer)) + self.sendMessage?(peer) } }))) @@ -524,7 +524,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController a(.default) if let self { - self.openProfile?(EnginePeer(peer)) + self.openProfile?(peer) } }))) @@ -646,8 +646,8 @@ final class ContactsPickerContext: AttachmentMediaPickerContext { } func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { - self.controller?.presentScheduleTimePicker ({ time, repeatPeriod in - self.controller?.contactsNode.requestMultipleAction?(false, time, parameters) + self.controller?.presentScheduleTimePicker ({ time, repeatPeriod, silentPosting in + self.controller?.contactsNode.requestMultipleAction?(silentPosting, time, parameters) }) } diff --git a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift index 8b22952450..d272c478ae 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionControllerNode.swift @@ -1,7 +1,6 @@ import Display import UIKit import AsyncDisplayKit -import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData @@ -158,7 +157,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { onlyWriteable: false, isGroupInvitation: false, isPeerEnabled: { peer in - return isPeerEnabled(.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil)) + return isPeerEnabled(.peer(peer: peer, isGlobal: false, participantCount: nil)) }, displayCallIcons: displayCallIcons, contextAction: multipleSelection ? { peer, node, gesture, _, _ in @@ -228,7 +227,7 @@ final class ContactSelectionControllerNode: ASDisplayNode { strongSelf.contactListNode.updateSelectionState { state in let peerId = ContactListPeerId.peer(peer.id) let state = state ?? ContactListNodeGroupSelectionState() - return state.withToggledPeerId(peerId).withSelectedPeerMap([peerId: ContactListPeer.peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil)]) + return state.withToggledPeerId(peerId).withSelectedPeerMap([peerId: ContactListPeer.peer(peer: peer, isGlobal: false, participantCount: nil)]) } } } diff --git a/submodules/TelegramUI/Sources/CreateChannelController.swift b/submodules/TelegramUI/Sources/CreateChannelController.swift index b855091607..eddc7f5297 100644 --- a/submodules/TelegramUI/Sources/CreateChannelController.swift +++ b/submodules/TelegramUI/Sources/CreateChannelController.swift @@ -19,8 +19,12 @@ import PeerInfoUI import MapResourceToAvatarSizes import LegacyMediaPickerUI import TextFormat +import MediaEditor +import MediaEditorScreen +import CameraScreen import AvatarEditorScreen import OldChannelsController +import Photos import AVFoundation private struct CreateChannelArguments { @@ -363,10 +367,29 @@ public func createChannelController(context: AccountContext, mode: CreateChannel let actionsDisposable = DisposableSet() - let currentAvatarMixin = Atomic(value: nil) - - let uploadedAvatar = Promise() - var uploadedVideoAvatar: (Promise, Double?)? = nil + var avatarPickerHolder: Any? + var pendingAvatar: CreatePendingPeerAvatar? + let applyPendingAvatar: (CreatePendingPeerAvatar) -> Void = { avatar in + pendingAvatar = avatar + updateState { current in + var current = current + current.avatar = avatar.updatingAvatar + return current + } + } + let updatePendingAvatarIfCurrent: (CreatePendingPeerAvatar) -> Void = { avatar in + if pendingAvatar?.previewRepresentation.resource.id == avatar.previewRepresentation.resource.id { + applyPendingAvatar(avatar) + } + } + let clearPendingAvatar: () -> Void = { + pendingAvatar = nil + updateState { current in + var current = current + current.avatar = nil + return current + } + } let checkAddressNameDisposable = MetaDisposable() actionsDisposable.add(checkAddressNameDisposable) @@ -434,12 +457,9 @@ public func createChannelController(context: AccountContext, mode: CreateChannel } } }).start(next: { peerId in - let updatingAvatar = stateValue.with { - return $0.avatar - } - if let _ = updatingAvatar { - let _ = context.engine.peers.updatePeerPhoto(peerId: peerId, photo: uploadedAvatar.get(), video: uploadedVideoAvatar?.0.get(), videoStartTimestamp: uploadedVideoAvatar?.1, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) + if let pendingAvatar { + let _ = context.engine.peers.updatePeerPhoto(peerId: peerId, photo: pendingAvatar.uploadedPhoto, video: pendingAvatar.uploadedVideo, videoStartTimestamp: pendingAvatar.videoStartTimestamp, markup: pendingAvatar.markup, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }).start() } @@ -474,216 +494,127 @@ public func createChannelController(context: AccountContext, mode: CreateChannel }, changeProfilePhoto: { endEditingImpl?() - let title = stateValue.with { state -> String in - return state.editingName.composedTitle - } + let keyboardInputData = Promise() + keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) - let _ = (context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), - TelegramEngine.EngineData.Item.Configuration.SearchBots() - ) - |> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var dismissPickerImpl: (() -> Void)? + let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: { + clearPendingAvatar() + }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, _, cancelled in + avatarPickerHolder = nil - let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) - legacyController.statusBar.statusBarStyle = .Ignore - - let emptyController = LegacyEmptyController(context: legacyController.context)! - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - - legacyController.bind(controller: navigationController) - - endEditingImpl?() - presentControllerImpl?(legacyController, nil) - - let completedChannelPhotoImpl: (UIImage) -> Void = { image in - if let data = image.jpegData(compressionQuality: 0.6) { - let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) - uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: resource)) - uploadedVideoAvatar = nil - updateState { current in - var current = current - current.avatar = .image(representation, false) - return current - } + let applyPhoto: (UIImage) -> Void = { image in + if let avatar = CreatePeerAvatarSetup.photo(context: context, image: image) { + applyPendingAvatar(avatar) + } + } + let applyVideo: (UIImage, MediaEditorScreenImpl.MediaResult.VideoResult?, MediaEditorValues?, UploadPeerPhotoMarkup?) -> Void = { image, video, values, markup in + if let avatar = CreatePeerAvatarSetup.video(context: context, image: image, video: video, values: values, markup: markup, didCompleteLoadingPreview: { avatar in + updatePendingAvatarIfCurrent(avatar) + }) { + applyPendingAvatar(avatar) } } - let completedChannelVideoImpl: (UIImage, Any?, TGVideoEditAdjustments?) -> Void = { image, asset, adjustments in - if let data = image.jpegData(compressionQuality: 0.6) { - let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) - updateState { state in - var state = state - state.avatar = .image(representation, true) - return state + let subject: Signal + if let asset = result as? PHAsset { + subject = .single(.asset(asset)) + } else if let image = result as? UIImage { + subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false)) + } else if let result = result as? Signal { + subject = result + |> map { value -> MediaEditorScreenImpl.Subject? in + switch value { + case .pendingImage: + return nil + case let .image(image): + return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft, fromCamera: false) + case let .video(video): + return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: [], additionalVideoPosition: .topLeft, fromCamera: false) + default: + return nil } - - var videoStartTimestamp: Double? = nil - if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { - videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue - } - - let signal = Signal { subscriber in - let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in - if let paintingData = adjustments.paintingData, paintingData.hasAnimation { - return LegacyPaintEntityRenderer(postbox: context.account.postbox, adjustments: adjustments) - } else { - return nil - } - } - - let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") - let uploadInterface = LegacyLiveUploadInterface(context: context) - let signal: SSignal - if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { - let durationSignal: SSignal = SSignal(generator: { subscriber in - let disposable = (entityRenderer.duration()).start(next: { duration in - subscriber.putNext(duration) - subscriber.putCompletion() - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - signal = durationSignal.map(toSignal: { duration -> SSignal in - if let duration = duration as? Double { - return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)! - } else { - return SSignal.single(nil) - } - }) - - } else if let asset = asset as? AVAsset { - signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)! - } else { - signal = SSignal.complete() - } - - let signalDisposable = signal.start(next: { next in - if let result = next as? TGMediaVideoConversionResult { - if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { - context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - } - - if let timestamp = videoStartTimestamp { - videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) - } - - var value = stat() - if stat(result.fileURL.path, &value) == 0 { - if let data = try? Data(contentsOf: result.fileURL) { - let resource: TelegramMediaResource - if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { - resource = LocalFileMediaResource(fileId: liveUploadData.id) - } else { - resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - } - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - subscriber.putNext(resource) - - EngineTempBox.shared.dispose(tempFile) - } - } - subscriber.putCompletion() - } - }, error: { _ in - }, completed: nil) - - let disposable = ActionDisposable { - signalDisposable?.dispose() - } - - return ActionDisposable { - disposable.dispose() - } - } - - uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: photoResource)) - - let promise = Promise() - promise.set(signal - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { resource -> Signal in - if let resource = resource { - return context.engine.peers.uploadedPeerVideo(resource: resource) |> map(Optional.init) - } else { - return .single(nil) - } - } |> afterNext { next in - if let next = next, next.isCompleted { - updateState { state in - var state = state - state.avatar = .image(representation, false) - return state - } - } - }) - uploadedVideoAvatar = (promise, videoStartTimestamp) } + } else { + let controller = AvatarEditorScreen(context: context, inputData: keyboardInputData.get(), peerType: .channel, markup: nil) + controller.imageCompletion = { image, commit in + applyPhoto(image) + commit() + } + controller.videoCompletion = { image, _, _, markup, commit in + applyVideo(image, nil, nil, markup) + commit() + } + pushControllerImpl?(controller) + return } - let keyboardInputData = Promise() - keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) - - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)! - mixin.stickersContext = LegacyPaintStickersContext(context: context) - let _ = currentAvatarMixin.swap(mixin) - mixin.requestSearchController = { assetsController in - let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in - assetsController?.dismiss() - completedChannelPhotoImpl(result) - })) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } -// mixin.requestAvatarEditor = { imageCompletion, videoCompletion in -// guard let imageCompletion, let videoCompletion else { -// return -// } -// let controller = AvatarEditorScreen(context: context, inputData: keyboardInputData.get(), peerType: .channel, markup: nil) -// controller.imageCompletion = imageCompletion -// controller.videoCompletion = videoCompletion -// pushControllerImpl?(controller) -// } - mixin.didFinishWithImage = { image in - if let image = image { - completedChannelPhotoImpl(image) - } - } - mixin.didFinishWithVideo = { image, asset, adjustments in - if let image = image, let asset = asset { - completedChannelVideoImpl(image, asset, adjustments) - } - } - if stateValue.with({ $0.avatar }) != nil { - mixin.didFinishWithDelete = { - updateState { current in - var current = current - current.avatar = nil - return current + let editorController = MediaEditorScreenImpl( + context: context, + mode: .avatarEditor, + subject: subject, + transitionIn: fromCamera ? .camera : transitionView.flatMap({ .gallery( + MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn( + sourceView: $0, + sourceRect: transitionRect, + sourceImage: transitionImage + ) + ) }), + transitionOut: { finished, _ in + if !finished, let transitionView { + return MediaEditorScreenImpl.TransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: 0.0 + ) } - uploadedAvatar.set(.never()) - } - } - mixin.didDismiss = { [weak legacyController] in - let _ = currentAvatarMixin.swap(nil) - legacyController?.dismiss() - } - let menuController = mixin.present() - if let menuController = menuController { - menuController.customRemoveFromParentViewController = { [weak legacyController] in - legacyController?.dismiss() - } + return nil + }, + completion: { results, commit in + guard let result = results.first else { + return + } + switch result.media { + case let .image(image, _): + applyPhoto(image) + commit({}) + case let .video(video, coverImage, values, _, _): + if let coverImage { + applyVideo(coverImage, video, values, nil) + } + commit({}) + default: + break + } + dismissPickerImpl?() + } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void + ) + editorController.cancelled = { _ in + cancelled() } + pushControllerImpl?(editorController) + }, dismissed: { + avatarPickerHolder = nil }) + avatarPickerHolder = pickerHolder + if let mainController { + dismissPickerImpl = { [weak mainController] in + if let mainController, let navigationController = mainController.navigationController { + var viewControllers = navigationController.viewControllers + viewControllers = viewControllers.filter { controller in + return !(controller is CameraScreen) && controller !== mainController + } + navigationController.setViewControllers(viewControllers, animated: false) + } + } + if mainController is ActionSheetController { + presentControllerImpl?(mainController, nil) + } else { + mainController.navigationPresentation = .flatModal + mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + pushControllerImpl?(mainController) + } + } }, focusOnDescription: { focusOnDescriptionImpl?() }, updatePublicLinkText: { text in @@ -741,6 +672,8 @@ public func createChannelController(context: AccountContext, mode: CreateChannel return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() + + let _ = avatarPickerHolder } let controller = ItemListController(context: context, state: signal) diff --git a/submodules/TelegramUI/Sources/CreateGroupController.swift b/submodules/TelegramUI/Sources/CreateGroupController.swift index 12295436ea..14ecb0c14b 100644 --- a/submodules/TelegramUI/Sources/CreateGroupController.swift +++ b/submodules/TelegramUI/Sources/CreateGroupController.swift @@ -34,6 +34,10 @@ import TextFormat import AvatarEditorScreen import SendInviteLinkScreen import OldChannelsController +import MediaEditor +import MediaEditorScreen +import CameraScreen +import Photos import AVFoundation private struct CreateGroupArguments { @@ -563,10 +567,29 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] let checkAddressNameDisposable = MetaDisposable() actionsDisposable.add(checkAddressNameDisposable) - let currentAvatarMixin = Atomic(value: nil) - - let uploadedAvatar = Promise() - var uploadedVideoAvatar: (Promise, Double?)? = nil + var avatarPickerHolder: Any? + var pendingAvatar: CreatePendingPeerAvatar? + let applyPendingAvatar: (CreatePendingPeerAvatar) -> Void = { avatar in + pendingAvatar = avatar + updateState { current in + var current = current + current.avatar = avatar.updatingAvatar + return current + } + } + let updatePendingAvatarIfCurrent: (CreatePendingPeerAvatar) -> Void = { avatar in + if pendingAvatar?.previewRepresentation.resource.id == avatar.previewRepresentation.resource.id { + applyPendingAvatar(avatar) + } + } + let clearPendingAvatar: () -> Void = { + pendingAvatar = nil + updateState { current in + var current = current + current.avatar = nil + return current + } + } if initialTitle == nil && peerIds.count > 0 && peerIds.count < 5 { let _ = (context.engine.data.get( @@ -793,19 +816,15 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] let _ = createSignal let _ = replaceControllerImpl let _ = dismissImpl - let _ = uploadedVideoAvatar actionsDisposable.add((createSignal |> mapToSignal { result -> Signal in guard let result = result else { return .single(nil) } - let updatingAvatar = stateValue.with { - return $0.avatar - } - if let _ = updatingAvatar { - return context.engine.peers.updatePeerPhoto(peerId: result.peerId, photo: uploadedAvatar.get(), video: uploadedVideoAvatar?.0.get(), videoStartTimestamp: uploadedVideoAvatar?.1, mapResourceToAvatarSizes: { resource, representations in - return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) + if let pendingAvatar { + return context.engine.peers.updatePeerPhoto(peerId: result.peerId, photo: pendingAvatar.uploadedPhoto, video: pendingAvatar.uploadedVideo, videoStartTimestamp: pendingAvatar.videoStartTimestamp, markup: pendingAvatar.markup, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) |> ignoreValues |> `catch` { _ -> Signal in @@ -893,216 +912,134 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] }, changeProfilePhoto: { endEditingImpl?() - let title = stateValue.with { state -> String in - return state.editingName.composedTitle + let peerType: AvatarEditorScreen.PeerType + if case let .requestPeer(group) = mode, group.isForum == true { + peerType = .forum + } else { + peerType = .group } - let _ = (context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), - TelegramEngine.EngineData.Item.Configuration.SearchBots() - ) - |> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let keyboardInputData = Promise() + keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) + + var dismissPickerImpl: (() -> Void)? + let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: { + clearPendingAvatar() + }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, _, cancelled in + avatarPickerHolder = nil - let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) - legacyController.statusBar.statusBarStyle = .Ignore - - let emptyController = LegacyEmptyController(context: legacyController.context)! - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - - legacyController.bind(controller: navigationController) - - endEditingImpl?() - presentControllerImpl?(legacyController, nil) - - let completedGroupPhotoImpl: (UIImage) -> Void = { image in - if let data = image.jpegData(compressionQuality: 0.6) { - let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) - uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: resource)) - uploadedVideoAvatar = nil - updateState { current in - var current = current - current.avatar = .image(representation, false) - return current - } + let applyPhoto: (UIImage) -> Void = { image in + if let avatar = CreatePeerAvatarSetup.photo(context: context, image: image) { + applyPendingAvatar(avatar) + } + } + let applyVideo: (UIImage, MediaEditorScreenImpl.MediaResult.VideoResult?, MediaEditorValues?, UploadPeerPhotoMarkup?) -> Void = { image, video, values, markup in + if let avatar = CreatePeerAvatarSetup.video(context: context, image: image, video: video, values: values, markup: markup, didCompleteLoadingPreview: { avatar in + updatePendingAvatarIfCurrent(avatar) + }) { + applyPendingAvatar(avatar) } } - let completedGroupVideoImpl: (UIImage, Any?, TGVideoEditAdjustments?) -> Void = { image, asset, adjustments in - if let data = image.jpegData(compressionQuality: 0.6) { - let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) - updateState { state in - var state = state - state.avatar = .image(representation, true) - return state + let subject: Signal + if let asset = result as? PHAsset { + subject = .single(.asset(asset)) + } else if let image = result as? UIImage { + subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false)) + } else if let result = result as? Signal { + subject = result + |> map { value -> MediaEditorScreenImpl.Subject? in + switch value { + case .pendingImage: + return nil + case let .image(image): + return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft, fromCamera: false) + case let .video(video): + return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: [], additionalVideoPosition: .topLeft, fromCamera: false) + default: + return nil } - - var videoStartTimestamp: Double? = nil - if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { - videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue - } - - let signal = Signal { subscriber in - let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in - if let paintingData = adjustments.paintingData, paintingData.hasAnimation { - return LegacyPaintEntityRenderer(postbox: context.account.postbox, adjustments: adjustments) - } else { - return nil - } - } - - let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") - let uploadInterface = LegacyLiveUploadInterface(context: context) - let signal: SSignal - if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { - let durationSignal: SSignal = SSignal(generator: { subscriber in - let disposable = (entityRenderer.duration()).start(next: { duration in - subscriber.putNext(duration) - subscriber.putCompletion() - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - signal = durationSignal.map(toSignal: { duration -> SSignal in - if let duration = duration as? Double { - return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)! - } else { - return SSignal.single(nil) - } - }) - - } else if let asset = asset as? AVAsset { - signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)! - } else { - signal = SSignal.complete() - } - - let signalDisposable = signal.start(next: { next in - if let result = next as? TGMediaVideoConversionResult { - if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { - context.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data) - } - - if let timestamp = videoStartTimestamp { - videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) - } - - var value = stat() - if stat(result.fileURL.path, &value) == 0 { - if let data = try? Data(contentsOf: result.fileURL) { - let resource: TelegramMediaResource - if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { - resource = LocalFileMediaResource(fileId: liveUploadData.id) - } else { - resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - } - context.account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) - subscriber.putNext(resource) - - EngineTempBox.shared.dispose(tempFile) - } - } - subscriber.putCompletion() - } - }, error: { _ in - }, completed: nil) - - let disposable = ActionDisposable { - signalDisposable?.dispose() - } - - return ActionDisposable { - disposable.dispose() - } - } - - uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: photoResource)) - - let promise = Promise() - promise.set(signal - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { resource -> Signal in - if let resource = resource { - return context.engine.peers.uploadedPeerVideo(resource: resource) |> map(Optional.init) - } else { - return .single(nil) - } - } |> afterNext { next in - if let next = next, next.isCompleted { - updateState { state in - var state = state - state.avatar = .image(representation, false) - return state - } - } - }) - uploadedVideoAvatar = (promise, videoStartTimestamp) } + } else { + let controller = AvatarEditorScreen(context: context, inputData: keyboardInputData.get(), peerType: peerType, markup: nil) + controller.imageCompletion = { image, commit in + applyPhoto(image) + commit() + } + controller.videoCompletion = { image, _, _, markup, commit in + applyVideo(image, nil, nil, markup) + commit() + } + pushImpl?(controller) + return } - let keyboardInputData = Promise() - keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) - - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)! - mixin.stickersContext = LegacyPaintStickersContext(context: context) - let _ = currentAvatarMixin.swap(mixin) - mixin.requestSearchController = { assetsController in - let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in - assetsController?.dismiss() - completedGroupPhotoImpl(result) - })) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } -// mixin.requestAvatarEditor = { imageCompletion, videoCompletion in -// guard let imageCompletion, let videoCompletion else { -// return -// } -// let controller = AvatarEditorScreen(context: context, inputData: keyboardInputData.get(), peerType: .group, markup: nil) -// controller.imageCompletion = imageCompletion -// controller.videoCompletion = videoCompletion -// pushImpl?(controller) -// } - mixin.didFinishWithImage = { image in - if let image = image { - completedGroupPhotoImpl(image) - } - } - mixin.didFinishWithVideo = { image, asset, adjustments in - if let image = image, let asset = asset { - completedGroupVideoImpl(image, asset, adjustments) - } - } - if stateValue.with({ $0.avatar }) != nil { - mixin.didFinishWithDelete = { - updateState { current in - var current = current - current.avatar = nil - return current + let editorController = MediaEditorScreenImpl( + context: context, + mode: .avatarEditor, + subject: subject, + transitionIn: fromCamera ? .camera : transitionView.flatMap({ .gallery( + MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn( + sourceView: $0, + sourceRect: transitionRect, + sourceImage: transitionImage + ) + ) }), + transitionOut: { finished, _ in + if !finished, let transitionView { + return MediaEditorScreenImpl.TransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: 0.0 + ) } - uploadedAvatar.set(.never()) - } - } - mixin.didDismiss = { [weak legacyController] in - let _ = currentAvatarMixin.swap(nil) - legacyController?.dismiss() - } - let menuController = mixin.present() - if let menuController = menuController { - menuController.customRemoveFromParentViewController = { [weak legacyController] in - legacyController?.dismiss() - } + return nil + }, + completion: { results, commit in + guard let result = results.first else { + return + } + switch result.media { + case let .image(image, _): + applyPhoto(image) + commit({}) + case let .video(video, coverImage, values, _, _): + if let coverImage { + applyVideo(coverImage, video, values, nil) + } + commit({}) + default: + break + } + dismissPickerImpl?() + } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void + ) + editorController.cancelled = { _ in + cancelled() } + pushImpl?(editorController) + }, dismissed: { + avatarPickerHolder = nil }) + avatarPickerHolder = pickerHolder + if let mainController { + dismissPickerImpl = { [weak mainController] in + if let mainController, let navigationController = mainController.navigationController { + var viewControllers = navigationController.viewControllers + viewControllers = viewControllers.filter { controller in + return !(controller is CameraScreen) && controller !== mainController + } + navigationController.setViewControllers(viewControllers, animated: false) + } + } + if mainController is ActionSheetController { + presentControllerImpl?(mainController, nil) + } else { + mainController.navigationPresentation = .flatModal + mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + pushImpl?(mainController) + } + } }, changeLocation: { endEditingImpl?() @@ -1221,7 +1158,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] }, action: { _, f in f(.default) - let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .autoremove, currentTime: currentValue == 0 ? nil : currentValue, dismissByTapOutside: true, completion: { value in + let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .autoremove, currentTime: currentValue == 0 ? nil : currentValue, completion: { value in applyValue(value) }) endEditingImpl?() @@ -1313,6 +1250,8 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] } |> afterDisposed { actionsDisposable.dispose() + + let _ = avatarPickerHolder } let controller = ItemListController(context: context, state: signal) diff --git a/submodules/TelegramUI/Sources/CreatePeerAvatarSetup.swift b/submodules/TelegramUI/Sources/CreatePeerAvatarSetup.swift new file mode 100644 index 0000000000..603e188909 --- /dev/null +++ b/submodules/TelegramUI/Sources/CreatePeerAvatarSetup.swift @@ -0,0 +1,216 @@ +import Foundation +import UIKit +import SwiftSignalKit +import TelegramCore +import AccountContext +import MediaEditor +import MediaEditorScreen +import ItemListAvatarAndNameInfoItem +import Photos +import AVFoundation + +struct CreatePendingPeerAvatar { + let previewRepresentation: TelegramMediaImageRepresentation + let isLoadingPreview: Bool + let uploadedPhoto: Signal + let uploadedVideo: Signal? + let videoStartTimestamp: Double? + let markup: UploadPeerPhotoMarkup? + + var updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar { + return .image(self.previewRepresentation, self.isLoadingPreview) + } +} + +enum CreatePeerAvatarSetup { + private static func makePhotoRepresentation(context: AccountContext, image: UIImage) -> (LocalFileMediaResource, TelegramMediaImageRepresentation)? { + guard let data = image.jpegData(compressionQuality: 0.6) else { + return nil + } + + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) + let representation = TelegramMediaImageRepresentation( + dimensions: PixelDimensions(width: 640, height: 640), + resource: resource, + progressiveSizes: [], + immediateThumbnailData: nil, + hasVideo: false, + isPersonal: false + ) + return (resource, representation) + } + + static func photo(context: AccountContext, image: UIImage) -> CreatePendingPeerAvatar? { + guard let (resource, representation) = self.makePhotoRepresentation(context: context, image: image) else { + return nil + } + + return CreatePendingPeerAvatar( + previewRepresentation: representation, + isLoadingPreview: false, + uploadedPhoto: context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(resource)), + uploadedVideo: nil, + videoStartTimestamp: nil, + markup: nil + ) + } + + static func video( + context: AccountContext, + image: UIImage, + video: MediaEditorScreenImpl.MediaResult.VideoResult?, + values: MediaEditorValues?, + markup: UploadPeerPhotoMarkup?, + didCompleteLoadingPreview: @escaping (CreatePendingPeerAvatar) -> Void = { _ in } + ) -> CreatePendingPeerAvatar? { + var shouldUploadVideo = true + if markup != nil { + if let data = context.currentAppConfiguration.with({ $0 }).data, let uploadVideoValue = data["upload_markup_video"] as? Bool, uploadVideoValue { + shouldUploadVideo = true + } else { + shouldUploadVideo = false + } + } + + guard let (photoResource, representation) = self.makePhotoRepresentation(context: context, image: image) else { + return nil + } + + let uploadedPhoto = context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(photoResource)) + + var videoStartTimestamp: Double? = nil + if let values, let coverImageTimestamp = values.coverImageTimestamp, coverImageTimestamp > 0.0 { + videoStartTimestamp = coverImageTimestamp - (values.videoTrimRange?.lowerBound ?? 0.0) + } + + let hasVideoUpload = shouldUploadVideo && video != nil && values != nil + guard hasVideoUpload, let video, let values else { + return CreatePendingPeerAvatar( + previewRepresentation: representation, + isLoadingPreview: false, + uploadedPhoto: uploadedPhoto, + uploadedVideo: nil, + videoStartTimestamp: videoStartTimestamp, + markup: markup + ) + } + + let account = context.account + let videoResource: Signal + + var exportSubject: Signal<(MediaEditorVideoExport.Subject, Double), NoError>? + switch video { + case let .imageFile(path): + if let image = UIImage(contentsOfFile: path) { + exportSubject = .single((.image(image: image), 3.0)) + } + case let .videoFile(path): + let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL) + exportSubject = .single((.video(asset: asset, isStory: false), asset.duration.seconds)) + case let .asset(localIdentifier): + exportSubject = Signal { subscriber in + let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil) + if fetchResult.count != 0 { + let asset = fetchResult.object(at: 0) + if asset.mediaType == .video { + PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in + if let avAsset { + subscriber.putNext((.video(asset: avAsset, isStory: true), avAsset.duration.seconds)) + subscriber.putCompletion() + } + } + } else { + let options = PHImageRequestOptions() + options.deliveryMode = .highQualityFormat + PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in + if let image { + subscriber.putNext((.image(image: image), 3.0)) + subscriber.putCompletion() + } + } + } + } + return EmptyDisposable + } + } + + guard let exportSubject else { + return CreatePendingPeerAvatar( + previewRepresentation: representation, + isLoadingPreview: false, + uploadedPhoto: uploadedPhoto, + uploadedVideo: nil, + videoStartTimestamp: videoStartTimestamp, + markup: markup + ) + } + + videoResource = exportSubject + |> castError(UploadPeerPhotoError.self) + |> mapToSignal { exportSubject, duration in + return Signal { subscriber in + let configuration = recommendedVideoExportConfiguration(values: values, duration: duration, forceFullHd: true, frameRate: 60.0, isAvatar: true) + let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") + let videoExport = MediaEditorVideoExport(postbox: context.account.postbox, subject: exportSubject, configuration: configuration, outputPath: tempFile.path, textScale: 2.0) + let _ = (videoExport.status + |> deliverOnMainQueue).startStandalone(next: { status in + switch status { + case .completed: + if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path), options: .mappedIfSafe) { + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + subscriber.putNext(resource) + subscriber.putCompletion() + } + EngineTempBox.shared.dispose(tempFile) + case .progress: + break + default: + break + } + }) + + return EmptyDisposable + } + } + + var completedAvatar: CreatePendingPeerAvatar? + let uploadedVideo = (videoResource + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { resource -> Signal in + if let resource { + return context.engine.peers.uploadedPeerVideo(resource: EngineMediaResource(resource)) + |> map(Optional.init) + } else { + return .single(nil) + } + } + |> afterNext { next in + if let next, next.isCompleted, let completedAvatar { + didCompleteLoadingPreview(completedAvatar) + } + }) + + let pendingAvatar = CreatePendingPeerAvatar( + previewRepresentation: representation, + isLoadingPreview: true, + uploadedPhoto: uploadedPhoto, + uploadedVideo: uploadedVideo, + videoStartTimestamp: videoStartTimestamp, + markup: markup + ) + completedAvatar = CreatePendingPeerAvatar( + previewRepresentation: representation, + isLoadingPreview: false, + uploadedPhoto: uploadedPhoto, + uploadedVideo: uploadedVideo, + videoStartTimestamp: videoStartTimestamp, + markup: markup + ) + + return pendingAvatar + } +} diff --git a/submodules/TelegramUI/Sources/DeleteChatInputPanelNode.swift b/submodules/TelegramUI/Sources/DeleteChatInputPanelNode.swift index f40a2c3a50..a4d0f4a610 100644 --- a/submodules/TelegramUI/Sources/DeleteChatInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/DeleteChatInputPanelNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import TelegramCore -import Postbox import SwiftSignalKit import ChatPresentationInterfaceState import ChatInputPanelNode @@ -36,7 +35,7 @@ final class DeleteChatInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.deleteChat() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) -> CGFloat { if self.presentationInterfaceState != interfaceState { self.presentationInterfaceState = interfaceState diff --git a/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift index 69025bf1fe..f86bc5defd 100644 --- a/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift @@ -43,7 +43,8 @@ final class DisabledContextResultsChatInputContextPanelNode: ChatInputContextPan self.containerNode.backgroundColor = interfaceState.theme.list.plainBackgroundColor self.separatorNode.backgroundColor = interfaceState.theme.list.itemPlainSeparatorColor - guard let (untilDate, personal) = (interfaceState.renderedPeer?.peer as? TelegramChannel)?.hasBannedPermission(.banSendInline) else { + let canBypass = canBypassRestrictions(chatPresentationInterfaceState: interfaceState) + guard let (untilDate, personal) = (interfaceState.renderedPeer?.peer as? TelegramChannel)?.hasBannedPermission(.banSendInline, ignoreDefault: canBypass) else { return } let banDescription: String diff --git a/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift b/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift index 99908fef41..5b3c76031d 100644 --- a/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/EmojisChatInputPanelItem.swift @@ -4,7 +4,6 @@ import AsyncDisplayKit import Display import TelegramCore import SwiftSignalKit -import Postbox import TelegramPresentationData import AnimationCache import MultiAnimationRenderer diff --git a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift index 08ad7c9271..0e6be30087 100644 --- a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift +++ b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift @@ -18,7 +18,7 @@ import WallpaperResources import GZip import TelegramUniversalVideoContent import GradientBackground -import Svg +import LegacyImpl import UniversalMediaPlayer import RangeSet diff --git a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift index 5a57356c20..fdd72a5cd6 100644 --- a/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HashtagChatInputContextPanelNode.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import AsyncDisplayKit -import Postbox import TelegramCore import Display import TelegramPresentationData diff --git a/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift index 9dfef707b0..579bf2383d 100644 --- a/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HashtagChatInputPanelItem.swift @@ -4,7 +4,6 @@ import AsyncDisplayKit import Display import TelegramCore import SwiftSignalKit -import Postbox import TelegramPresentationData import TelegramUIPreferences import ItemListUI diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift index c76495b3ec..76fcf7bc4b 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import AsyncDisplayKit -import Postbox import TelegramCore import Display import SwiftSignalKit @@ -19,6 +18,9 @@ import ChatControllerInteraction import ChatContextResultPeekContent import ChatInputContextPanelNode import BatchVideoRendering +import GlassBackgroundComponent +import ComponentFlow +import ComponentDisplayAdapters private struct ChatContextResultStableId: Hashable { let result: ChatContextResult @@ -83,6 +85,9 @@ private func preparedTransition(from fromEntries: [HorizontalListContextResultsC } final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputContextPanelNode { + private let backgroundContainerView: GlassBackgroundContainerView + private let backgroundView: GlassBackgroundView + private let listClippingView: UIView private let listView: ListView private var currentExternalResults: ChatContextResultCollection? private var currentProcessedResults: ChatContextResultCollection? @@ -96,9 +101,14 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont private let batchVideoContext: QueueLocalObject override init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, chatPresentationContext: ChatPresentationContext) { + self.backgroundContainerView = GlassBackgroundContainerView() + self.backgroundView = GlassBackgroundView() + self.backgroundContainerView.contentView.addSubview(self.backgroundView) + self.listClippingView = UIView() + self.listClippingView.clipsToBounds = true + self.listView = ListViewImpl() - self.listView.isOpaque = true - self.listView.backgroundColor = theme.list.plainBackgroundColor + self.listView.isOpaque = false self.listView.transform = CATransform3DMakeRotation(-CGFloat(CGFloat.pi / 2.0), 0.0, 0.0, 1.0) self.listView.isHidden = true self.listView.accessibilityPageScrolledString = { row, count in @@ -112,9 +122,12 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont super.init(context: context, theme: theme, strings: strings, fontSize: fontSize, chatPresentationContext: chatPresentationContext) self.isOpaque = false - self.clipsToBounds = true + self.clipsToBounds = false + self.layer.allowsGroupOpacity = true - self.addSubnode(self.listView) + self.view.addSubview(self.backgroundContainerView) + self.listClippingView.addSubview(self.listView.view) + self.backgroundView.contentView.addSubview(self.listClippingView) self.listView.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in if let strongSelf = self, let state = opaqueTransactionState as? HorizontalListContextResultsOpaqueState { @@ -362,12 +375,26 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) { let listHeight: CGFloat = 105.0 + let sideInset: CGFloat = 8.0 + let innerInset: CGFloat = 4.0 + let cornerRadius: CGFloat = 8.0 + let innerRadius: CGFloat = cornerRadius - innerInset - self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: listHeight, height: size.width) + let listFrame = CGRect(x: sideInset, y: size.height - bottomInset - 8.0 - listHeight, width: size.width - sideInset * 2.0, height: listHeight) + let transformedListFrame = CGSize(width: listFrame.height, height: listFrame.width).centered(in: listFrame) + self.listView.bounds = CGRect(origin: CGPoint(), size: transformedListFrame.size) + transition.updatePosition(node: self.listView, position: CGRect(origin: CGPoint(x: -innerInset, y: -innerInset), size: listFrame.size).center) - //transition.updateFrame(node: self.listView, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + transition.updateFrame(view: self.listClippingView, frame: CGRect(origin: CGPoint(), size: listFrame.size).insetBy(dx: innerInset, dy: innerInset)) + self.listClippingView.layer.cornerRadius = innerRadius - transition.updatePosition(node: self.listView, position: CGPoint(x: size.width / 2.0, y: size.height - bottomInset - 8.0 - listHeight / 2.0)) + let backgroundContainerInset: CGFloat = 32.0 + let backgroundContainerFrame = listFrame.insetBy(dx: -backgroundContainerInset, dy: -backgroundContainerInset) + transition.updateFrame(view: self.backgroundContainerView, frame: backgroundContainerFrame) + self.backgroundContainerView.update(size: backgroundContainerFrame.size, isDark: interfaceState.theme.overallDarkAppearance, transition: ComponentTransition(transition)) + + transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: listFrame.size).offsetBy(dx: backgroundContainerInset, dy: backgroundContainerInset)) + self.backgroundView.update(size: listFrame.size, cornerRadius: cornerRadius, isDark: interfaceState.theme.overallDarkAppearance, tintColor: .init(kind: .panel), transition: ComponentTransition(transition)) var insets = UIEdgeInsets() insets.top = leftInset @@ -392,22 +419,18 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont } override func animateOut(completion: @escaping () -> Void) { - /*let position = self.listView.layer.position - self.listView.layer.animatePosition(from: position, to: CGPoint(x: position.x, y: position.y + self.listView.bounds.size.width), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in - completion() - })*/ - self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + ComponentTransition.easeInOut(duration: 0.3).setAlpha(view: self.backgroundContainerView, alpha: 0.01, completion: { _ in completion() }) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let listViewBounds = self.listView.bounds - let listViewPosition = self.listView.position - let listViewFrame = CGRect(origin: CGPoint(x: listViewPosition.x - listViewBounds.height / 2.0, y: listViewPosition.y - listViewBounds.width / 2.0), size: CGSize(width: listViewBounds.height, height: listViewBounds.width)) - if !listViewFrame.contains(point) { + guard let result = super.hitTest(point, with: event) else { return nil } - return super.hitTest(point, with: event) + if result === self.view { + return nil + } + return result } } diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift index ed79bfdbf4..a71bd7b345 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputPanelItem.swift @@ -97,7 +97,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode private var currentImageResource: TelegramMediaResource? private var currentVideoFile: TelegramMediaFile? private var currentAnimatedStickerFile: TelegramMediaFile? - private var resourceStatus: MediaResourceStatus? + private var resourceStatus: EngineMediaResource.FetchStatus? private(set) var item: HorizontalListContextResultsChatInputPanelItem? private var statusDisposable = MetaDisposable() private let statusNode: RadialStatusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) @@ -202,7 +202,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode let sideInset: CGFloat = 4.0 var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - var updatedStatusSignal: Signal? + var updatedStatusSignal: Signal? var imageResource: TelegramMediaResource? var stickerFile: TelegramMediaFile? @@ -223,9 +223,9 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode } if let file = videoFile { - updatedStatusSignal = item.context.account.postbox.mediaBox.resourceStatus(file.resource) + updatedStatusSignal = item.context.engine.resources.status(resource: EngineMediaResource(file.resource)) } else if let imageResource = imageResource { - updatedStatusSignal = item.context.account.postbox.mediaBox.resourceStatus(imageResource) + updatedStatusSignal = item.context.engine.resources.status(resource: EngineMediaResource(imageResource)) } case let .internalReference(internalReference): if let image = internalReference.image { @@ -254,12 +254,12 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode if file.isVideo && file.isAnimated { videoFile = file imageResource = nil - updatedStatusSignal = item.context.account.postbox.mediaBox.resourceStatus(file.resource) + updatedStatusSignal = item.context.engine.resources.status(resource: EngineMediaResource(file.resource)) } else if let imageResource = imageResource { - updatedStatusSignal = item.context.account.postbox.mediaBox.resourceStatus(imageResource) + updatedStatusSignal = item.context.engine.resources.status(resource: EngineMediaResource(imageResource)) } } else if let imageResource = imageResource { - updatedStatusSignal = item.context.account.postbox.mediaBox.resourceStatus(imageResource) + updatedStatusSignal = item.context.engine.resources.status(resource: EngineMediaResource(imageResource)) } } diff --git a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift index 3d15ecc22a..9faa2ba98a 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift @@ -256,7 +256,7 @@ final class HorizontalStickerGridItemNode: GridItemNode { placeholderNode.frame = bounds if let context = self.currentState?.0, let theme = self.currentState?.1.theme, let file = self.currentState?.1.file { - placeholderNode.update(backgroundColor: theme.list.plainBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor.mixedWith(theme.list.plainBackgroundColor, alpha: 0.4), shimmeringColor: theme.list.mediaPlaceholderColor.withAlphaComponent(0.3), data: file.immediateThumbnailData, size: bounds.size, enableEffect: context.sharedContext.energyUsageSettings.fullTranslucency) + placeholderNode.update(backgroundColor: nil, foregroundColor: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), shimmeringColor: theme.list.mediaPlaceholderColor.withAlphaComponent(0.2), data: file.immediateThumbnailData, size: bounds.size, enableEffect: context.sharedContext.energyUsageSettings.fullTranslucency) } } diff --git a/submodules/TelegramUI/Sources/ID3ArtworkReader.swift b/submodules/TelegramUI/Sources/ID3ArtworkReader.swift index 0fdc8aef04..29ea8dc742 100644 --- a/submodules/TelegramUI/Sources/ID3ArtworkReader.swift +++ b/submodules/TelegramUI/Sources/ID3ArtworkReader.swift @@ -4,35 +4,37 @@ private enum ID3Tag: CaseIterable { case v2 case v3 - static var headerLength = 5 + static let headerLength = 10 + static let versionHeaderLength = 5 + var header: Data { switch self { case .v2: - return Data([ 0x49, 0x44, 0x33, 0x02, 0x00 ]) + return Data([0x49, 0x44, 0x33, 0x02, 0x00]) case .v3: - return Data([ 0x49, 0x44, 0x33, 0x03, 0x00 ]) + return Data([0x49, 0x44, 0x33, 0x03, 0x00]) } } var artworkHeader: Data { switch self { case .v2: - return Data([ 0x50, 0x49, 0x43 ]) + return Data([0x50, 0x49, 0x43]) case .v3: - return Data([ 0x41, 0x50, 0x49, 0x43 ]) + return Data([0x41, 0x50, 0x49, 0x43]) } } - var frameSizeOffset: Int { + var frameIdentifierLength: Int { switch self { case .v2: - return 2 + return 3 case .v3: return 4 } } - var frameOffset: Int { + var frameHeaderLength: Int { switch self { case .v2: return 6 @@ -41,25 +43,30 @@ private enum ID3Tag: CaseIterable { } } - func frameSize(_ value: Int32) -> Int { + var unsupportedFlagsMask: UInt8 { + switch self { + case .v2, .v3: + return 0xc0 + } + } + + func frameDataSize(in data: Data, at frameOffset: Int) -> Int? { switch self { case .v2: - return Int(value & 0x00ffffff) + self.frameOffset + guard let range = makeRange(start: frameOffset + self.frameIdentifierLength, length: 3, upperBound: data.count) else { + return nil + } + let bytes = data[range] + return Int(UInt32(bytes[bytes.startIndex]) << 16 | UInt32(bytes[bytes.startIndex + 1]) << 8 | UInt32(bytes[bytes.startIndex + 2])) case .v3: - return Int(value) + self.frameOffset + guard let value = readBigEndianUInt32(in: data, at: frameOffset + self.frameIdentifierLength) else { + return nil + } + return Int(value) } } } -private let sizeOffset = 6 -private let tagOffset = 10 -private let artOffset = 10 - -private let v2FrameOffset: UInt32 = 6 -private let v3FrameOffset: UInt32 = 10 - -private let tagEnding = Data([ 0x00, 0x00, 0x00 ]) - private enum ID3ArtworkFormat: CaseIterable { case jpg case png @@ -67,77 +74,103 @@ private enum ID3ArtworkFormat: CaseIterable { var magic: Data { switch self { case .jpg: - return Data([ 0xff, 0xd8, 0xff ]) + return Data([0xff, 0xd8, 0xff]) case .png: - return Data([ 0x89, 0x50, 0x4e, 0x47 ]) + return Data([0x89, 0x50, 0x4e, 0x47]) } } } -private class DataStream { - private let data: Data - private(set) var position = 0 - var reachedEnd: Bool { - return self.position >= self.data.count +private enum ID3ArtworkReaderLimits { + static let maximumTagSize = 16 * 1024 * 1024 + static let maximumFrameSize = 16 * 1024 * 1024 + static let maximumArtworkSize = 16 * 1024 * 1024 +} + +private let id3Prefix = Data([0x49, 0x44, 0x33]) +private let tagEnding = Data([0x00, 0x00, 0x00]) +private let jpegEndMarker = Data([0xff, 0xd9]) +private let pngEndMarker = Data([0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82]) + +private func makeRange(start: Int, length: Int, upperBound: Int) -> Range? { + guard start >= 0, length >= 0 else { + return nil } - - init(_ data: Data) { - self.data = data + let (end, overflow) = start.addingReportingOverflow(length) + guard !overflow, end <= upperBound else { + return nil } - - func next() -> UInt8? { - guard !self.reachedEnd else { - return nil - } - let byte = self.data[self.position] - self.position += 1 - return byte + return start ..< end +} + +private func checkedAdd(_ lhs: Int, _ rhs: Int) -> Int? { + let (result, overflow) = lhs.addingReportingOverflow(rhs) + return overflow ? nil : result +} + +private func readBigEndianUInt32(in data: Data, at offset: Int) -> UInt32? { + guard let range = makeRange(start: offset, length: 4, upperBound: data.count) else { + return nil } - - func nextValue() -> T? { - let count = MemoryLayout.size - guard self.position + count <= self.data.count else { - return nil - } - let value = self.data.subdata(in: self.position ..< self.position + count).withUnsafeBytes { (pointer: UnsafeRawBufferPointer) -> T in - return pointer.baseAddress!.assumingMemoryBound(to: T.self).pointee - } - self.position += count - return value + let bytes = data[range] + return UInt32(bytes[bytes.startIndex]) << 24 + | UInt32(bytes[bytes.startIndex + 1]) << 16 + | UInt32(bytes[bytes.startIndex + 2]) << 8 + | UInt32(bytes[bytes.startIndex + 3]) +} + +private func decodeSynchsafeUInt32(_ value: UInt32) -> Int? { + guard value & 0x80808080 == 0 else { + return nil } + let b1 = Int((value >> 24) & 0x7f) + let b2 = Int((value >> 16) & 0x7f) + let b3 = Int((value >> 8) & 0x7f) + let b4 = Int(value & 0x7f) + return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4 +} + +private func extractArtworkData(from data: Data, frameDataRange: Range) -> Data? { + let frameData = data.subdata(in: frameDataRange) - func next(_ count: Int, offset: Int = 0, peek: Bool = false) -> Data? { - guard self.position + offset + count <= self.data.count else { - return nil + var bestMatch: (format: ID3ArtworkFormat, range: Range)? + for format in ID3ArtworkFormat.allCases { + guard let range = frameData.range(of: format.magic) else { + continue } - let subdata = self.data.subdata(in: self.position + offset ..< self.position + offset + count) - if !peek { - self.position += count + offset - } - return subdata - } - - func upTo(_ marker: UInt8) -> Data? { - if let end = (self.position ..< self.data.count).firstIndex( where: { self.data[$0] == marker } ) { - let upTo = self.next(end - self.position) - self.skip() - return upTo + if let current = bestMatch { + if range.lowerBound < current.range.lowerBound { + bestMatch = (format, range) + } } else { - return nil + bestMatch = (format, range) } } - func skip(_ count: Int = 1) { - self.position += count + guard let match = bestMatch else { + return nil } - func skipThrough(_ marker: UInt8) { - if let end = (self.position ..< self.data.count).firstIndex( where: { self.data[$0] == marker } ) { - self.position = end + 1 - } else { - self.position = self.data.count - } + let payload: Data + switch match.format { + case .jpg: + if let endMarkerRange = frameData[match.range.lowerBound ..< frameData.endIndex].range(of: jpegEndMarker) { + payload = frameData.subdata(in: match.range.lowerBound ..< endMarkerRange.upperBound) + } else { + payload = frameData.subdata(in: match.range.lowerBound ..< frameData.endIndex) + } + case .png: + if let endMarkerRange = frameData[match.range.lowerBound ..< frameData.endIndex].range(of: pngEndMarker) { + payload = frameData.subdata(in: match.range.lowerBound ..< endMarkerRange.upperBound) + } else { + payload = frameData.subdata(in: match.range.lowerBound ..< frameData.endIndex) + } } + + guard payload.count <= ID3ArtworkReaderLimits.maximumArtworkSize else { + return nil + } + return payload } enum ID3ArtworkResult { @@ -147,12 +180,17 @@ enum ID3ArtworkResult { } func readAlbumArtworkData(_ data: Data) -> ID3ArtworkResult { - guard data.count >= 4 else { - return .notFound + if data.count < id3Prefix.count { + return id3Prefix.starts(with: data) ? .moreDataNeeded(ID3Tag.headerLength) : .notFound + } + if data.count < ID3Tag.headerLength { + return data.starts(with: id3Prefix) ? .moreDataNeeded(ID3Tag.headerLength) : .notFound } - let stream = DataStream(data) - let versionHeader = stream.next(ID3Tag.headerLength) + guard let versionHeaderRange = makeRange(start: 0, length: ID3Tag.versionHeaderLength, upperBound: data.count) else { + return .notFound + } + let versionHeader = data.subdata(in: versionHeaderRange) var version: ID3Tag? for tag in ID3Tag.allCases { @@ -165,79 +203,57 @@ func readAlbumArtworkData(_ data: Data) -> ID3ArtworkResult { return .notFound } - stream.skip() - guard let value: UInt32 = stream.nextValue() else { + let flags = data[5] + guard flags & id3Tag.unsupportedFlagsMask == 0 else { return .notFound } - let size = CFSwapInt32HostToBig(value) - let b1 = (size & 0x7f000000) >> 3 - let b2 = (size & 0x007f0000) >> 2 - let b3 = (size & 0x00007f00) >> 1 - let b4 = size & 0x0000007f - let tagSize = Int(b1 + b2 + b3 + b4) - while !stream.reachedEnd { - guard let frameHeader = stream.next(4, peek: true) else { - return .moreDataNeeded(tagSize) - } - - stream.skip(id3Tag.frameSizeOffset) - guard let value: UInt32 = stream.nextValue() else { - return .moreDataNeeded(tagSize) - } - let val = CFSwapInt32HostToBig(value) - if val > Int32.max { - return .notFound - } - let frameSize = id3Tag.frameSize(Int32(val)) - let bytesLeft = frameSize - id3Tag.frameSizeOffset - 4 - - if frameHeader == id3Tag.artworkHeader { - var image: (ID3ArtworkFormat, Int)? - outer: for i in 0 ..< frameSize - 4 { - if let head = stream.next(4, offset: i, peek: true) { - for format in ID3ArtworkFormat.allCases { - if head.prefix(format.magic.count) == format.magic { - image = (format, i) - break outer - } - } - } - } - - if let (format, offset) = image { - stream.skip(offset) - - switch format { - case .jpg: - var data = Data(capacity: frameSize + 1024) - var previousByte: UInt8 = 0xff - - let limit = Int(Double(frameSize - offset) * 0.8) - for _ in 0 ..< frameSize - offset { - if let byte: UInt8 = stream.nextValue() { - data.append(byte) - if byte == 0xd9 && previousByte == 0xff && data.count > limit { - break - } - previousByte = byte - } else { - return .moreDataNeeded(tagSize) - } - } - return .artworkData(data) - case .png: - if let data = stream.next(frameSize - offset) { - return .artworkData(data) - } else { - return .moreDataNeeded(tagSize) - } - } - } - } else if frameHeader.prefix(3) == tagEnding { - return .notFound - } - stream.skip(bytesLeft) + guard let rawTagSize = readBigEndianUInt32(in: data, at: 6), let tagPayloadSize = decodeSynchsafeUInt32(rawTagSize) else { + return .notFound } + guard let totalTagSize = checkedAdd(ID3Tag.headerLength, tagPayloadSize), totalTagSize <= ID3ArtworkReaderLimits.maximumTagSize else { + return .notFound + } + + let availableTagEnd = min(totalTagSize, data.count) + var frameOffset = ID3Tag.headerLength + + while frameOffset < availableTagEnd { + let remainingTagBytes = availableTagEnd - frameOffset + if remainingTagBytes < id3Tag.frameHeaderLength { + return totalTagSize > data.count ? .moreDataNeeded(totalTagSize) : .notFound + } + + guard let frameIdentifierRange = makeRange(start: frameOffset, length: id3Tag.frameIdentifierLength, upperBound: data.count) else { + return totalTagSize > data.count ? .moreDataNeeded(totalTagSize) : .notFound + } + let frameIdentifier = data.subdata(in: frameIdentifierRange) + if frameIdentifier.prefix(3) == tagEnding { + return .notFound + } + + guard let framePayloadSize = id3Tag.frameDataSize(in: data, at: frameOffset), framePayloadSize <= ID3ArtworkReaderLimits.maximumFrameSize else { + return .notFound + } + guard let frameSize = checkedAdd(id3Tag.frameHeaderLength, framePayloadSize), let frameEnd = checkedAdd(frameOffset, frameSize) else { + return .notFound + } + guard frameEnd <= totalTagSize else { + return .notFound + } + if frameEnd > data.count { + return .moreDataNeeded(totalTagSize) + } + + if frameIdentifier == id3Tag.artworkHeader { + let frameDataStart = frameOffset + id3Tag.frameHeaderLength + if let artworkData = extractArtworkData(from: data, frameDataRange: frameDataStart ..< frameEnd) { + return .artworkData(artworkData) + } + } + + frameOffset = frameEnd + } + return .notFound } diff --git a/submodules/TelegramUI/Sources/LargeEmojiActionSheetItem.swift b/submodules/TelegramUI/Sources/LargeEmojiActionSheetItem.swift index d16fd2d030..de88ceab55 100644 --- a/submodules/TelegramUI/Sources/LargeEmojiActionSheetItem.swift +++ b/submodules/TelegramUI/Sources/LargeEmojiActionSheetItem.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import Display -import Postbox import SwiftSignalKit import TelegramCore import TelegramPresentationData diff --git a/submodules/TelegramUI/Sources/MediaManager.swift b/submodules/TelegramUI/Sources/MediaManager.swift index f838410fdf..7163222876 100644 --- a/submodules/TelegramUI/Sources/MediaManager.swift +++ b/submodules/TelegramUI/Sources/MediaManager.swift @@ -3,7 +3,6 @@ import SwiftSignalKit import AVFoundation import MobileCoreServices import Display -import Postbox import TelegramCore import MediaPlayer import TelegramAudio @@ -78,7 +77,7 @@ public final class MediaManagerImpl: NSObject, MediaManager { didSet { if self.voiceMediaPlayer !== oldValue { if let voiceMediaPlayer = self.voiceMediaPlayer { - let account = voiceMediaPlayer.account + let account = voiceMediaPlayer.engine.account self.voiceMediaPlayerStateDisposable.set((voiceMediaPlayer.playbackState |> deliverOnMainQueue).startStrict(next: { [weak self, weak voiceMediaPlayer] state in guard let strongSelf = self else { @@ -117,7 +116,7 @@ public final class MediaManagerImpl: NSObject, MediaManager { if self.musicMediaPlayer !== oldValue { if let musicMediaPlayer = self.musicMediaPlayer { let type = musicMediaPlayer.type - let account = musicMediaPlayer.account + let account = musicMediaPlayer.engine.account self.musicMediaPlayerStateValue.set(musicMediaPlayer.playbackState |> map { state -> (Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)? in guard let state = state else { @@ -524,7 +523,7 @@ public final class MediaManagerImpl: NSObject, MediaManager { controlPlaybackWithProximity = playlist.context.sharedContext.currentMediaInputSettings.with({ $0.enableRaiseToSpeak }) } - let voiceMediaPlayer = SharedMediaPlayer(context: context, mediaManager: strongSelf, inForeground: strongSelf.inForeground, account: context.account, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: .reversed, initialLooping: .none, initialPlaybackRate: settings.voicePlaybackRate, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: controlPlaybackWithProximity, type: type, continueInstantVideoLoopAfterFinish: continueInstantVideoLoopAfterFinish) + let voiceMediaPlayer = SharedMediaPlayer(context: context, mediaManager: strongSelf, inForeground: strongSelf.inForeground, engine: context.engine, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: .reversed, initialLooping: .none, initialPlaybackRate: settings.voicePlaybackRate, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: controlPlaybackWithProximity, type: type, continueInstantVideoLoopAfterFinish: continueInstantVideoLoopAfterFinish) strongSelf.voiceMediaPlayer = voiceMediaPlayer voiceMediaPlayer.playedToEnd = { [weak voiceMediaPlayer] in if let strongSelf = self, let voiceMediaPlayer = voiceMediaPlayer, voiceMediaPlayer === strongSelf.voiceMediaPlayer { @@ -558,7 +557,7 @@ public final class MediaManagerImpl: NSObject, MediaManager { strongSelf.musicMediaPlayer?.control(control) } else { strongSelf.musicMediaPlayer?.stop() - let musicMediaPlayer = SharedMediaPlayer(context: context, mediaManager: strongSelf, inForeground: strongSelf.inForeground, account: context.account, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: settings.order, initialLooping: settings.looping, initialPlaybackRate: storedState?.playbackRate ?? .x1, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: false, type: type, continueInstantVideoLoopAfterFinish: true) + let musicMediaPlayer = SharedMediaPlayer(context: context, mediaManager: strongSelf, inForeground: strongSelf.inForeground, engine: context.engine, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: settings.order, initialLooping: settings.looping, initialPlaybackRate: storedState?.playbackRate ?? .x1, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: false, type: type, continueInstantVideoLoopAfterFinish: true) strongSelf.musicMediaPlayer = musicMediaPlayer strongSelf.musicListenTracker = MusicListenTracker(engine: context.engine) musicMediaPlayer.cancelled = { [weak musicMediaPlayer] in diff --git a/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift index c0d7a4c6ca..41ee283032 100644 --- a/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/MentionChatInputContextPanelNode.swift @@ -36,7 +36,7 @@ private struct MentionChatInputContextPanelEntry: Comparable, Identifiable { } func item(context: AccountContext, presentationData: PresentationData, inverted: Bool, setPeerIdRevealed: @escaping (EnginePeer.Id?) -> Void, peerSelected: @escaping (EnginePeer) -> Void, removeRequested: @escaping (EnginePeer.Id) -> Void) -> ListViewItem { - return MentionChatInputPanelItem(context: context, presentationData: ItemListPresentationData(presentationData), inverted: inverted, peer: self.peer._asPeer(), revealed: self.revealed, setPeerIdRevealed: setPeerIdRevealed, peerSelected: peerSelected, removeRequested: removeRequested) + return MentionChatInputPanelItem(context: context, presentationData: ItemListPresentationData(presentationData), inverted: inverted, peer: self.peer, revealed: self.revealed, setPeerIdRevealed: setPeerIdRevealed, peerSelected: peerSelected, removeRequested: removeRequested) } } diff --git a/submodules/TelegramUI/Sources/MentionChatInputPanelItem.swift b/submodules/TelegramUI/Sources/MentionChatInputPanelItem.swift index 928dba8aa7..4ab767c87d 100644 --- a/submodules/TelegramUI/Sources/MentionChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/MentionChatInputPanelItem.swift @@ -16,14 +16,14 @@ final class MentionChatInputPanelItem: ListViewItem { fileprivate let presentationData: ItemListPresentationData fileprivate let revealed: Bool fileprivate let inverted: Bool - fileprivate let peer: Peer + fileprivate let peer: EnginePeer private let peerSelected: (EnginePeer) -> Void fileprivate let setPeerIdRevealed: (EnginePeer.Id?) -> Void fileprivate let removeRequested: (EnginePeer.Id) -> Void - + let selectable: Bool = true - - public init(context: AccountContext, presentationData: ItemListPresentationData, inverted: Bool, peer: Peer, revealed: Bool, setPeerIdRevealed: @escaping (PeerId?) -> Void, peerSelected: @escaping (EnginePeer) -> Void, removeRequested: @escaping (PeerId) -> Void) { + + public init(context: AccountContext, presentationData: ItemListPresentationData, inverted: Bool, peer: EnginePeer, revealed: Bool, setPeerIdRevealed: @escaping (PeerId?) -> Void, peerSelected: @escaping (EnginePeer) -> Void, removeRequested: @escaping (PeerId) -> Void) { self.context = context self.presentationData = presentationData self.inverted = inverted @@ -85,7 +85,7 @@ final class MentionChatInputPanelItem: ListViewItem { if self.revealed { self.setPeerIdRevealed(nil) } else { - self.peerSelected(EnginePeer(self.peer)) + self.peerSelected(self.peer) } } } @@ -174,7 +174,7 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { } - let title = EnginePeer(item.peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + let title = item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) var username: String? let string = NSMutableAttributedString() string.append(NSAttributedString(string: title, font: primaryFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)) @@ -204,7 +204,7 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor - strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: EnginePeer(item.peer), emptyColor: item.presentationData.theme.list.mediaPlaceholderColor) + strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor) let _ = textApply() @@ -221,7 +221,7 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { strongSelf.activateAreaNode.accessibilityValue = username strongSelf.activateAreaNode.frame = CGRect(origin: .zero, size: nodeLayout.size) - if let peer = item.peer as? TelegramUser, let _ = peer.botInfo { + if case let .user(peer) = item.peer, let _ = peer.botInfo { strongSelf.setRevealOptions([ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]) strongSelf.setRevealOptionsOpened(item.revealed, animated: animation.isAnimated) } else { diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index 74ca2b1aa1..4f9e8b5f2b 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -140,7 +140,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam } if !params.forceOpenChat, !viewForumAsMessages, params.subject == nil, case let .peer(peer) = params.chatLocation, peer.id == params.context.account.peerId { - if let controller = params.context.sharedContext.makePeerInfoController(context: params.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let controller = params.context.sharedContext.makePeerInfoController(context: params.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { params.navigationController.pushViewController(controller, animated: params.animated, completion: { }) return @@ -182,6 +182,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam controller.navigateToMessage(messageLocation: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: (highlight?.quote).flatMap { quote in NavigateToMessageParams.Quote(string: quote.string, offset: quote.offset) }, subject: highlight?.subject, setupReply: setupReply)), animated: isFirst || params.forceAnimatedScroll, completion: { [weak navigationController, weak controller] in if let navigationController = navigationController, let controller = controller { let _ = navigationController.popToViewController(controller, animated: animated) + params.completion(controller) } }, customPresentProgress: { [weak navigationController] c, a in (navigationController?.viewControllers.last as? ViewController)?.present(c, in: .window(.root), with: a) @@ -252,8 +253,14 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload, mode: botAppStart.mode) } } + + if controller.chatLocation.peerId == params.chatLocation.asChatLocation.peerId && controller.chatLocation.threadId == params.chatLocation.asChatLocation.threadId && (controller.subject != .scheduledMessages || controller.subject == params.subject) { + if let updateTextInputState = params.updateTextInputState { + controller.updateTextInputState(updateTextInputState) + } + } } else { - controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation.asChatLocation, chatLocationContextHolder: params.chatLocationContextHolder, subject: params.subject, botStart: params.botStart, attachBotStart: params.attachBotStart, botAppStart: params.botAppStart, peekData: params.peekData, peerNearbyData: params.peerNearbyData, chatListFilter: params.chatListFilter, chatNavigationStack: params.chatNavigationStack, customChatNavigationStack: params.customChatNavigationStack) + controller = ChatControllerImpl(context: params.context, chatLocation: params.chatLocation.asChatLocation, chatLocationContextHolder: params.chatLocationContextHolder, subject: params.subject, botStart: params.botStart, attachBotStart: params.attachBotStart, botAppStart: params.botAppStart, peekData: params.peekData, chatListFilter: params.chatListFilter, chatNavigationStack: params.chatNavigationStack, customChatNavigationStack: params.customChatNavigationStack, initialTextInputState: params.updateTextInputState) if let botAppStart = params.botAppStart, case let .peer(peer) = params.chatLocation { Queue.mainQueue().after(0.1) { @@ -262,14 +269,6 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam } } - if controller.chatLocation.peerId == params.chatLocation.asChatLocation.peerId && controller.chatLocation.threadId == params.chatLocation.asChatLocation.threadId && (controller.subject != .scheduledMessages || controller.subject == params.subject) { - if let updateTextInputState = params.updateTextInputState { - Queue.mainQueue().after(0.1) { - controller.updateTextInputState(updateTextInputState) - } - } - } - controller.purposefulAction = params.purposefulAction if let search = params.activateMessageSearch { controller.activateSearch(domain: search.0, query: search.1) @@ -448,7 +447,7 @@ public func navigateToForumThreadImpl(context: AccountContext, peerId: EnginePee } } -public func chatControllerForForumThreadImpl(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64) -> Signal { +public func chatControllerForForumThreadImpl(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, initialTextInputState: ChatTextInputState? = nil) -> Signal { return context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) ) @@ -476,7 +475,8 @@ public func chatControllerForForumThreadImpl(context: AccountContext, peerId: En initialAnchor: .automatic, isNotAvailable: false )), - chatLocationContextHolder: Atomic(value: nil) + chatLocationContextHolder: Atomic(value: nil), + initialTextInputState: initialTextInputState )) } else { return fetchAndPreloadReplyThreadInfo(context: context, subject: .groupMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))), atMessageId: nil, preload: false) @@ -488,7 +488,8 @@ public func chatControllerForForumThreadImpl(context: AccountContext, peerId: En return ChatControllerImpl( context: context, chatLocation: .replyThread(message: result.message), - chatLocationContextHolder: result.contextHolder + chatLocationContextHolder: result.contextHolder, + initialTextInputState: initialTextInputState ) } } diff --git a/submodules/TelegramUI/Sources/OpenAddContact.swift b/submodules/TelegramUI/Sources/OpenAddContact.swift index fabba71aaf..859791713b 100644 --- a/submodules/TelegramUI/Sources/OpenAddContact.swift +++ b/submodules/TelegramUI/Sources/OpenAddContact.swift @@ -4,12 +4,13 @@ import TelegramCore import Display import DeviceAccess import AccountContext +import ShareController import AlertUI import PresentationDataUtils import PeerInfoUI -import ShareController +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,12 +18,14 @@ 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 { - if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { pushController(infoController) } } else if let stableId, let contactData { @@ -36,7 +39,7 @@ func openAddContactImpl(context: AccountContext, firstName: String = "", lastNam DeviceAccess.authorizeAccess(to: .contacts) default: let presentationData = context.sharedContext.currentPresentationData.with { $0 } - present(textAlertController(context: context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(textAlertController(context: context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { context.sharedContext.applicationBindings.openSettings() })]), nil) } diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index a8442864c2..6d916852c5 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 @@ -9,6 +11,7 @@ import Lottie import TelegramUIPreferences import TelegramPresentationData import AccountContext +import ShareController import GalleryUI import InstantPageUI import LocationUI @@ -18,7 +21,6 @@ import PeerInfoUI import SettingsUI import AlertUI import PresentationDataUtils -import ShareController import UndoUI import WebsiteType import GalleryData @@ -135,13 +137,13 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { params.openUrl(url) return true case let .pass(file): - let _ = (params.context.account.postbox.mediaBox.resourceData(file.resource, option: .complete(waitUntilFetchStatus: true)) + let _ = (params.context.engine.resources.data(resource: EngineMediaResource(file.resource), waitUntilFetchStatus: true) |> take(1) |> deliverOnMainQueue).startStandalone(next: { data in guard let navigationController = params.navigationController else { return } - if data.complete, let content = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + if data.isComplete, let content = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { if let pass = try? PKPass(data: content), let controller = PKAddPassesViewController(pass: pass) { if let window = navigationController.view.window { controller.popoverPresentationController?.sourceView = window @@ -233,7 +235,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 = { @@ -254,11 +256,14 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { } } + let fileReference: FileMediaReference = .message(message: MessageReference(params.message), media: file) let subject: BrowserScreen.Subject - if file.mimeType == "application/pdf" { - subject = .pdfDocument(file: .message(message: MessageReference(params.message), media: file), canShare: canShare) + if file.mimeType.contains("markdown") { + subject = .markdownDocument(file: fileReference, canShare: canShare) + } else if file.mimeType.contains("pdf") { + subject = .pdfDocument(file: fileReference, canShare: canShare) } else { - subject = .document(file: .message(message: MessageReference(params.message), media: file), canShare: canShare) + subject = .document(file: fileReference, canShare: canShare) } let controller = BrowserScreen(context: params.context, subject: subject) controller.openDocument = { [weak controller] file, canShare in @@ -318,6 +323,44 @@ 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 } + let toastController = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Chat_ToastVoiceMessageDeviceMuted, 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() @@ -396,7 +439,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { } else { contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName, lastName: contact.lastName, phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: contact.phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") } - let controller = deviceContactInfoController(context: ShareControllerAppAccountContext(context: params.context), environment: ShareControllerAppEnvironment(sharedContext: params.context.sharedContext), updatedPresentationData: params.updatedPresentationData, subject: .vcard(peer?._asPeer(), nil, contactData), completed: nil, cancelled: nil) + let controller = deviceContactInfoController(context: ShareControllerAppAccountContext(context: params.context), environment: ShareControllerAppEnvironment(sharedContext: params.context.sharedContext), updatedPresentationData: params.updatedPresentationData, subject: .vcard(peer, nil, contactData), completed: nil, cancelled: nil) params.navigationController?.pushViewController(controller) }) return true @@ -419,7 +462,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { }), .window(.root)) case let .theme(media): params.dismissInput() - let path = params.context.account.postbox.mediaBox.completedResourcePath(media.resource) + let path = params.context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(media.resource.id)) var previewTheme: PresentationTheme? if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) { previewTheme = makePresentationTheme(data: data) @@ -499,7 +542,7 @@ func openChatTheme(context: AccountContext, message: Message, pushController: @e } if case let .theme(slug) = resolvedUrl { if let file = file { - if let path = context.sharedContext.accountManager.mediaBox.completedResourcePath(file.resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { + if let path = context.sharedContext.accountManager.resources.completedResourcePath(resource: EngineMediaResource(file.resource)), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) { if let theme = makePresentationTheme(data: data) { let controller = ThemePreviewController(context: context, previewTheme: theme, source: .slug(slug, file)) pushController(controller) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 696fc1daa7..d448a26b79 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -20,8 +20,6 @@ import JoinLinkPreviewUI import LanguageLinkPreviewUI import SettingsUI import UrlHandling -import ShareController -import ChatInterfaceState import TelegramCallsUI import UndoUI import ImportStickerPackUI @@ -121,6 +119,15 @@ func openResolvedUrlImpl( } ) } + let isStartGroup: Bool + switch peerType { + case .group?: + isStartGroup = true + default: + isStartGroup = false + } + let shouldForceStartGroupStartFlow = isStartGroup && !payload.isEmpty && adminRights == nil + let shouldCheckExistingGroupAdmin = isStartGroup && !payload.isEmpty && adminRights != nil var filter: ChatListNodePeersFilter = [.onlyGroupsAndChannels, .onlyManageable, .excludeDisabled, .excludeRecent, .doNotSearchMessages] var title: String = presentationData.strings.Bot_AddToChat_Title @@ -199,53 +206,108 @@ func openResolvedUrlImpl( } }), TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: {}) - ] + ], + actionLayout: .vertical ) present(alertController, nil) } + let openAdminControllerImpl: (TelegramChatAdminRightsFlags?) -> Void = { initialAdminRights in + let adminController = channelAdminController(context: context, peerId: peerId, adminId: botPeerId, initialParticipant: nil, invite: true, initialAdminRights: initialAdminRights, updated: { _ in + if shouldCheckExistingGroupAdmin { + Queue.mainQueue().after(0.1) { + addMemberImpl() + } + } else { + controller?.dismiss() + } + }, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in }) + navigationController?.pushViewController(adminController) + } + let openAdminControllerWithResolvedRightsImpl: (Bool) -> Void = { isGroup in + if adminRights == nil { + let _ = (defaultAdminRights.get() + |> take(1) + |> deliverOnMainQueue).start(next: { defaultAdminRights in + let initialAdminRights = isGroup ? defaultAdminRights?.group?.rights : defaultAdminRights?.channel?.rights + openAdminControllerImpl(initialAdminRights) + }) + } else { + openAdminControllerImpl(adminRights?.chatAdminRights) + } + } if case let .channel(peer) = peer { var isGroup = false if case .group = peer.info { isGroup = true } - if peer.flags.contains(.isCreator) || peer.adminRights?.rights.contains(.canAddAdmins) == true { - let _ = (defaultAdminRights.get() - |> take(1) - |> deliverOnMainQueue).start(next: { defaultAdminRights in - let initialAdminRights = adminRights?.chatAdminRights ?? (isGroup ? defaultAdminRights?.group?.rights : defaultAdminRights?.channel?.rights) - let controller = channelAdminController(context: context, peerId: peerId, adminId: botPeerId, initialParticipant: nil, invite: true, initialAdminRights: initialAdminRights, updated: { _ in - controller?.dismiss() - }, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in }) - navigationController?.pushViewController(controller) - }) + if shouldForceStartGroupStartFlow { + addMemberImpl() + } else if peer.flags.contains(.isCreator) || peer.adminRights?.rights.contains(.canAddAdmins) == true { + if shouldCheckExistingGroupAdmin && isGroup { + let _ = (context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: botPeerId) + |> deliverOnMainQueue).start(next: { participant in + let isBotAlreadyAdmin: Bool + if let participant = participant { + switch participant { + case .creator: + isBotAlreadyAdmin = true + case let .member(_, _, adminInfo, _, _, _): + isBotAlreadyAdmin = adminInfo != nil + } + } else { + isBotAlreadyAdmin = false + } + if isBotAlreadyAdmin { + addMemberImpl() + } else { + openAdminControllerWithResolvedRightsImpl(isGroup) + } + }) + } else { + openAdminControllerWithResolvedRightsImpl(isGroup) + } } else { addMemberImpl() } } else if case let .legacyGroup(peer) = peer { - if case .member = peer.role { + if shouldForceStartGroupStartFlow { addMemberImpl() - } else { - let _ = (defaultAdminRights.get() - |> take(1) - |> deliverOnMainQueue).start(next: { defaultAdminRights in - let initialAdminRights = adminRights?.chatAdminRights ?? defaultAdminRights?.group?.rights - let controller = channelAdminController(context: context, peerId: peerId, adminId: botPeerId, initialParticipant: nil, invite: true, initialAdminRights: initialAdminRights, updated: { _ in - controller?.dismiss() - }, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in }) - navigationController?.pushViewController(controller) + } else if case .member = peer.role { + addMemberImpl() + } else if shouldCheckExistingGroupAdmin { + let _ = (context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peerId) + |> mapToSignal { _ in + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.LegacyGroupParticipants(id: peerId)) + } + |> deliverOnMainQueue).start(next: { participants in + let isBotAlreadyAdmin: Bool + if let participant = participants.knownValue?.first(where: { $0.peerId == botPeerId }) { + switch participant { + case .creator, .admin: + isBotAlreadyAdmin = true + case .member: + isBotAlreadyAdmin = false + } + } else { + isBotAlreadyAdmin = false + } + if isBotAlreadyAdmin { + addMemberImpl() + } else { + openAdminControllerWithResolvedRightsImpl(true) + } }) + } else { + openAdminControllerWithResolvedRightsImpl(true) } } } dismissInput() navigationController?.pushViewController(controller) case let .gameStart(botPeerId, game): - let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyManageable, .excludeDisabled, .excludeRecent, .doNotSearchMessages], hasContactSelector: false, title: presentationData.strings.Bot_AddToChat_Title, selectForumThreads: true)) - controller.peerSelected = { [weak controller] peer, _ in - let _ = peer.id - let _ = botPeerId - let _ = game + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled, .doNotSearchMessages], hasContactSelector: false, title: presentationData.strings.ShareMenu_SelectChats, selectForumThreads: true)) + controller.peerSelected = { [weak controller] peer, threadId in let presentationData = context.sharedContext.currentPresentationData.with { $0 } let text: String if case .user = peer { @@ -255,6 +317,20 @@ func openResolvedUrlImpl( } let alertController = textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.RequestPeer_SelectionConfirmationSend, action: { + controller?.inProgress = true + let _ = (context.engine.messages.sendBotGame(botPeerId: botPeerId, game: game, to: peer.id, threadId: threadId) + |> deliverOnMainQueue).startStandalone(error: { _ in + controller?.inProgress = false + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + }) + if let navigationController { + if let threadId { + let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peer.id, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: true, keepStack: .always, animated: true).startStandalone() + } else { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), keepStack: .always, useExisting: true, scrollToEndIfExists: true)) + } + } controller?.dismiss() }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { })]) @@ -554,16 +630,16 @@ func openResolvedUrlImpl( } let chatController: Signal if let threadId { - chatController = chatControllerForForumThreadImpl(context: context, peerId: peerId, threadId: threadId) + chatController = chatControllerForForumThreadImpl(context: context, peerId: peerId, threadId: threadId, initialTextInputState: textInputState) } else { - chatController = .single(ChatControllerImpl(context: context, chatLocation: .peer(id: peerId))) + chatController = .single(ChatControllerImpl(context: context, chatLocation: .peer(id: peerId), initialTextInputState: textInputState)) } - + let _ = (chatController |> deliverOnMainQueue).start(next: { [weak navigationController] chatController in guard let navigationController else { return - } + } var controllers = navigationController.viewControllers.filter { controller in if controller is PeerSelectionController { return false @@ -574,17 +650,8 @@ func openResolvedUrlImpl( navigationController.setViewControllers(controllers, animated: true) }) } - - if let textInputState = textInputState { - let _ = (ChatInterfaceState.update(engine: context.engine, peerId: peerId, threadId: threadId, { currentState in - return currentState.withUpdatedComposeInputState(textInputState) - }) - |> deliverOnMainQueue).startStandalone(completed: { - updateControllers() - }) - } else { - updateControllers() - } + + updateControllers() } if let to = to { @@ -624,14 +691,13 @@ 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 { - let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], selectForumThreads: true)) + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyWriteable, .excludeDisabled], hasFilters: true, selectForumThreads: true)) controller.peerSelected = { peer, threadId in continueWithPeer(peer.id, threadId) } @@ -680,18 +746,18 @@ func openResolvedUrlImpl( subscriber.putNext((nil, settings, themeInfo)) subscriber.putCompletion() } else if let resource = themeInfo.file?.resource { - disposables.add(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .standalone(resource: resource)).start()) + disposables.add(context.engine.resources.fetch(reference: .standalone(resource: resource), userLocation: .other, userContentType: .other).start()) - let maybeFetched = context.sharedContext.accountManager.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: false) + let maybeFetched = context.sharedContext.accountManager.resources.data(resource: EngineMediaResource(resource)) |> mapToSignal { maybeData -> Signal in - if maybeData.complete { + if maybeData.isComplete { let loadedData = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(loadedData) } else { - return context.account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: false) + return context.engine.resources.data(resource: EngineMediaResource(resource)) |> map { next -> Data? in - if next.size > 0, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) { - context.sharedContext.accountManager.mediaBox.storeResourceData(resource.id, data: data) + if next.availableSize > 0, let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []) { + context.sharedContext.accountManager.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) return data } else { return nil @@ -767,6 +833,7 @@ func openResolvedUrlImpl( if case .new = section { context.sharedContext.openAddContact( context: context, + peer: nil, firstName: "", lastName: "", phoneNumber: "", @@ -789,7 +856,7 @@ func openResolvedUrlImpl( navigationController?.pushViewController(controller) default: let presentationData = context.sharedContext.currentPresentationData.with { $0 } - present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.AccessDenied_Settings, action: { context.sharedContext.applicationBindings.openSettings() })]), nil) } @@ -843,6 +910,7 @@ func openResolvedUrlImpl( case .contact: context.sharedContext.openAddContact( context: context, + peer: nil, firstName: "", lastName: "", phoneNumber: "", @@ -1740,7 +1808,7 @@ func openResolvedUrlImpl( guard let controller = context.sharedContext.makePeerInfoController( context: context, updatedPresentationData: updatedPresentationData, - peer: peer._asPeer(), + peer: peer, mode: .storyAlbum(id: id), avatarInitiallyExpanded: false, fromChat: false, @@ -1761,7 +1829,7 @@ func openResolvedUrlImpl( guard let controller = context.sharedContext.makePeerInfoController( context: context, updatedPresentationData: updatedPresentationData, - peer: peer._asPeer(), + peer: peer, mode: .giftCollection(id: id), avatarInitiallyExpanded: false, fromChat: false, @@ -1971,5 +2039,27 @@ func openResolvedUrlImpl( } navigationController?.pushViewController(controller) } + case let .textStyle(style, initialPreview): + Task { @MainActor in + var authorPeer: EnginePeer? + if let authorId = style.authorId { + authorPeer = await context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: authorId) + ).get() + } + let isAlreadyAdded = await context.engine.messages.composeAIMessageStyles().get().contains(where: { $0.id == .style(.custom(style.id)) }) + + let controller = await context.sharedContext.makeTextProcessingScreen(context: context, theme: nil, mode: .preview(style: style, authorPeer: authorPeer, initialPreview: initialPreview, isAlreadyAdded: isAlreadyAdded, added: { + Task { @MainActor in + guard let emojiFileId = style.emojiFileId, let file = await context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId]).get().first?.value else { + return + } + present(UndoOverlayController(presentationData: presentationData, content: .customEmoji(context: context, file: file, loop: false, title: presentationData.strings.TextProcessing_ToastStyleAdded_Title, text: presentationData.strings.TextProcessing_ToastStyleAdded_Text(style.title).string, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in + return true + }), nil) + } + }), inputText: TextWithEntities(text: "", entities: []), copyResult: nil, translateChat: nil) + navigationController?.pushViewController(controller) + } } } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index 0ab56ceb80..561932edd8 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -172,7 +172,7 @@ private func makeResolvedUrlHandler( openPeer: { peer, navigation in switch navigation { case .info: - if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { context.sharedContext.applicationBindings.dismissNativeController() navigationController?.pushViewController(infoController) } @@ -532,7 +532,7 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur if let peer = peer, let controller = context.sharedContext.makePeerInfoController( context: context, updatedPresentationData: nil, - peer: peer._asPeer(), + peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, @@ -693,6 +693,10 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur handleResolvedUrl(.createBot(parentBot: peer.id, username: params["username"], title: params["name"])) }) } + case "textStyle": + if let slug = params["slug"] { + convertedUrl = makeTelegramUrl("/addstyle/\(slug)") + } default: break } diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerController.swift index bce9e5b293..ed034ebced 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)) } @@ -182,7 +180,7 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer let fileId = Int64.random(in: Int64.min ... Int64.max) let mimeType = guessMimeTypeByFileExtension((item.fileName as NSString).pathExtension) var previewRepresentations: [TelegramMediaImageRepresentation] = [] - if mimeType.hasPrefix("image/") || mimeType == "application/pdf" { + if mimeType.hasPrefix("image/") || mimeType == "application/pdf" || item.audioMetadata?.hasAudioArtwork == true { previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) } var attributes: [TelegramMediaFileAttribute] = [] @@ -213,7 +211,7 @@ final class OverlayAudioPlayerControllerImpl: ViewController, OverlayAudioPlayer switch result { case let .media(resultMedia): if let resultFile = resultMedia.media as? TelegramMediaFile { - self.context.account.postbox.mediaBox.moveResourceData(from: file.resource.id, to: resultFile.resource.id, synchronous: true) + self.context.engine.resources.moveResourceData(from: EngineMediaResource.Id(file.resource.id), to: EngineMediaResource.Id(resultFile.resource.id), synchronous: true) self.controllerNode.addToSavedMusic(file: .standalone(media: file)) } } diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 1324d32411..9fa74bbce6 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? @@ -240,7 +245,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, sendGift: { _ in }, openUniqueGift: { _ in }, openMessageFeeException: { - }, requestMessageUpdate: { _, _ in + }, requestMessageUpdate: { _, _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { }, scrollToMessageId: { _ in @@ -253,7 +258,10 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, requestToggleTodoMessageItem: { _, _, _ in }, displayTodoToggleUnavailable: { _ in }, openStarsPurchase: { _ in - }, openRankInfo: { _, _, _ in }, openSetPeerAvatar: {}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) + }, openRankInfo: { _, _, _ in + }, openSetPeerAvatar: { + }, displayPollRestrictedToast: { _ in + }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(), presentationContext: ChatPresentationContext(context: context, backgroundNode: nil)) self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) @@ -293,7 +301,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 @@ -305,12 +313,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, @@ -324,7 +327,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 } @@ -335,6 +338,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 +383,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 +562,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu ) self.setupReordering() + + self.albumArtBackground.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.albumArtTapped(_:)))) } deinit { @@ -564,10 +586,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 } @@ -580,7 +603,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: savedMusicContext.peerId)) |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer = peer.flatMap({ PeerReference($0._asPeer()) }) else { + guard let self, let peer = peer.flatMap({ PeerReference($0) }) else { return } @@ -633,7 +656,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: []) @@ -716,7 +739,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu if let controller = self.context.sharedContext.makePeerInfoController( context: self.context, updatedPresentationData: nil, - peer: peer._asPeer(), + peer: peer, mode: .myProfile, avatarInitiallyExpanded: false, fromChat: false, @@ -820,7 +843,9 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu containerTransform = CATransform3DScale(containerTransform, scale, scale, scale) transition.updateTransform(layer: self.containerContainingNode.layer, transform: containerTransform) - transition.updateCornerRadius(node: self.containerContainingNode, cornerRadius: layout.deviceMetrics.screenCornerRadius) + + let containerCornerRadius = max(22.0, layout.deviceMetrics.screenCornerRadius) + transition.updateCornerRadius(node: self.containerContainingNode, cornerRadius: containerCornerRadius) } private var effectiveHeaderHeight: CGFloat { @@ -841,7 +866,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 +988,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) @@ -988,7 +1057,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 @@ -999,6 +1068,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 { @@ -1098,7 +1174,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 @@ -1128,11 +1204,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, @@ -1146,7 +1217,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 } @@ -1562,3 +1633,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.modalBlocksBackgroundColor.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) +} diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift index 546ddc9884..5ba432e116 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControlsNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import ComponentFlow -import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData @@ -148,8 +147,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 +191,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 +221,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 +354,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, @@ -480,7 +452,7 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode { case let .telegramFile(fileReference, _, _): strongSelf.currentFileReference = fileReference if let size = fileReference.media.size { - strongSelf.scrubberNode.bufferingStatus = strongSelf.account.postbox.mediaBox.resourceRangesStatus(fileReference.media.resource) + strongSelf.scrubberNode.bufferingStatus = strongSelf.engine.resources.resourceRangesStatus(resource: EngineMediaResource(fileReference.media.resource)) |> map { ranges -> (RangeSet, Int64) in return (ranges, size) } @@ -592,7 +564,6 @@ final class OverlayAudioPlayerControlsNode: ASDisplayNode { deinit { self.statusDisposable?.dispose() self.chapterDisposable?.dispose() - self.scrubbingDisposable?.dispose() } override func didLoad() { @@ -788,9 +759,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 +849,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 @@ -1047,6 +952,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 @@ -1283,15 +1193,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)) + } } } diff --git a/submodules/TelegramUI/Sources/OverlayMediaController.swift b/submodules/TelegramUI/Sources/OverlayMediaController.swift index cbed0ab07e..1dddfaadc4 100644 --- a/submodules/TelegramUI/Sources/OverlayMediaController.swift +++ b/submodules/TelegramUI/Sources/OverlayMediaController.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import AccountContext import AVKit diff --git a/submodules/TelegramUI/Sources/OverlayMediaControllerNode.swift b/submodules/TelegramUI/Sources/OverlayMediaControllerNode.swift index 0e2cc2d901..e5b60ac8fc 100644 --- a/submodules/TelegramUI/Sources/OverlayMediaControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayMediaControllerNode.swift @@ -3,7 +3,6 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit -import Postbox import AccountContext private final class OverlayMediaControllerNodeView: UITracingLayerView { diff --git a/submodules/TelegramUI/Sources/PollResultsController.swift b/submodules/TelegramUI/Sources/PollResultsController.swift index fb9234290f..4dd03bc17f 100644 --- a/submodules/TelegramUI/Sources/PollResultsController.swift +++ b/submodules/TelegramUI/Sources/PollResultsController.swift @@ -448,7 +448,7 @@ public func pollResultsController(context: AccountContext, messageId: EngineMess }) }, openPeer: { peer in if let peer = peer.peers[peer.peerId] { - if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { pushControllerImpl?(controller) } } diff --git a/submodules/TelegramUI/Sources/PrefetchManager.swift b/submodules/TelegramUI/Sources/PrefetchManager.swift index 1ab0ba75e7..9e45dce177 100644 --- a/submodules/TelegramUI/Sources/PrefetchManager.swift +++ b/submodules/TelegramUI/Sources/PrefetchManager.swift @@ -52,10 +52,10 @@ private final class PrefetchManagerInnerImpl { } |> distinctUntilChanged - let appConfiguration = account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + let appConfiguration = engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) |> take(1) |> map { view in - return view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue + return view?.get(AppConfiguration.self) ?? .defaultValue } let orderedPreloadMedia = combineLatest(account.viewTracker.orderedPreloadMedia, TelegramEngine(account: account).stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false), appConfiguration) diff --git a/submodules/TelegramUI/Sources/SaveMediaToFiles.swift b/submodules/TelegramUI/Sources/SaveMediaToFiles.swift index 7ad5919923..af8c4a35b4 100644 --- a/submodules/TelegramUI/Sources/SaveMediaToFiles.swift +++ b/submodules/TelegramUI/Sources/SaveMediaToFiles.swift @@ -24,7 +24,7 @@ func saveMediaToFiles(context: AccountContext, fileReference: FileMediaReference } } - var signal = fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: fileReference.abstract) + var signal = fetchMediaData(context: context, userLocation: .other, mediaReference: fileReference.abstract) var cancelImpl: (() -> Void)? let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -60,7 +60,7 @@ func saveMediaToFiles(context: AccountContext, fileReference: FileMediaReference case .progress: break case let .data(data): - if data.complete { + if data.isComplete { var symlinkPath = data.path + ".mp3" if fileSize(symlinkPath) != nil { try? FileManager.default.removeItem(atPath: symlinkPath) diff --git a/submodules/TelegramUI/Sources/SecretChatHandshakeStatusInputPanelNode.swift b/submodules/TelegramUI/Sources/SecretChatHandshakeStatusInputPanelNode.swift index b8ae461975..be6ae06001 100644 --- a/submodules/TelegramUI/Sources/SecretChatHandshakeStatusInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/SecretChatHandshakeStatusInputPanelNode.swift @@ -3,7 +3,6 @@ import UIKit import AsyncDisplayKit import Display import TelegramCore -import Postbox import SwiftSignalKit import LocalizedPeerData import ChatPresentationInterfaceState @@ -53,7 +52,7 @@ final class SecretChatHandshakeStatusInputPanelNode: ChatInputPanelNode { self.interfaceInteraction?.unblockPeer() } - override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat { + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, maxOverlayHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, deviceMetrics: DeviceMetrics, isMediaInputExpanded: Bool) -> CGFloat { self.presentationInterfaceState = interfaceState var text: String? diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 450fe8033b..223c0af0a8 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -99,14 +99,17 @@ import GiftCraftScreen import ChatParticipantRightsScreen import PeerCopyProtectionInfoScreen import ChatRankInfoScreen +import PollStatsScreen import RankChatPreviewItem import TextProcessingScreen import CreateBotScreen +import EmojiStatusSelectionComponent +import EntityKeyboard private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() let tokens = Bag() - + var isEmpty: Bool { return self.tokens.isEmpty && self.subscribers.isEmpty } @@ -292,7 +295,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?, Bool) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?, testingEnvironment: Bool = false) { assert(Queue.mainQueue().isCurrent()) - + precondition(!testHasInstance) testHasInstance = true @@ -805,7 +808,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { guard let peer = peer else { return nil } - return AccountWithInfo(account: context.account, peer: peer._asPeer()) + return AccountWithInfo(account: context.account, peer: peer) } |> distinctUntilChanged }) @@ -1931,7 +1934,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { handleTextLinkActionImpl(context: context, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink) } - public func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController? { + public func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peer: EnginePeer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController? { let controller = peerInfoControllerImpl(context: context, updatedPresentationData: updatedPresentationData, peer: peer, mode: mode, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: fromChat) controller?.navigationPresentation = .modalInLargeLayout return controller @@ -2295,7 +2298,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return nil } - public func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController { + public func makeChatRecentActionsController(context: AccountContext, peer: EnginePeer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController { return ChatRecentActionsController(context: context, peer: peer, adminPeerId: adminPeerId, starsState: starsState) } @@ -2323,8 +2326,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) { @@ -2343,7 +2346,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return PeerSelectionControllerImpl(params) } - public func openAddPeerMembers(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, parentController: ViewController, groupPeer: Peer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable) { + public func openAddPeerMembers(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, parentController: ViewController, groupPeer: EnginePeer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable) { return presentAddMembersImpl(context: context, updatedPresentationData: updatedPresentationData, parentController: parentController, groupPeer: groupPeer, selectAddMemberDisposable: selectAddMemberDisposable, addMemberDisposable: addMemberDisposable) } @@ -2551,7 +2554,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, openMessageFeeException: { }, - requestMessageUpdate: { _, _ in + requestMessageUpdate: { _, _, _ in }, cancelInteractiveKeyboardGestures: { }, @@ -2578,7 +2581,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { openStarsPurchase: { _ in }, openRankInfo: { _, _, _ in - }, openSetPeerAvatar: { + }, + openSetPeerAvatar: { + }, + displayPollRestrictedToast: { _ in }, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), @@ -2723,7 +2729,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: context.engine.privacy.webSessions(), websitesOnly: false) } - public func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController { + public func makeChatQrCodeScreen(context: AccountContext, peer: EnginePeer, threadId: Int64?, temporary: Bool) -> ViewController { return ChatQrCodeScreenImpl(context: context, subject: .peer(peer: peer, threadId: threadId, temporary: temporary)) } @@ -2756,19 +2762,18 @@ public final class SharedAccountContextImpl: SharedAccountContext { return makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, mode: audio ? .audio(.chat) : .recent, bannedSendMedia: bannedSendMedia, presentGallery: presentGallery, presentFiles: presentFiles, presentDocumentScanner: presentDocumentScanner, send: send) } - public func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, hasTimer: Bool, customEmojiAvailable: Bool, pushViewController: @escaping (ViewController) -> Void, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? { + public func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, hasTimer: Bool, customEmojiAvailable: Bool, pushViewController: @escaping (ViewController) -> Void, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void, getNavigationController: @escaping () -> NavigationController?) -> NSObject? { let inputPanelNode = LegacyMessageInputPanelNode( context: context, chatLocation: chatLocation, isScheduledMessages: isScheduledMessages, isFile: isFile, hasTimer: hasTimer, + customEmojiAvailable: customEmojiAvailable, pushViewController: pushViewController, present: present, presentInGlobalOverlay: presentInGlobalOverlay, - makeEntityInputView: { - return EntityInputView(context: context, isDark: true, areCustomEmojiEnabled: customEmojiAvailable) - } + getNavigationController: getNavigationController ) return inputPanelNode } @@ -2973,8 +2978,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .copyProtection case .aiTools: mappedSource = .aiTools - case let .auth(price): - mappedSource = .auth(price) + case let .auth(price, days): + mappedSource = .auth(price, days) case let .premiumGift(file): mappedSource = .premiumGift(file) } @@ -3292,12 +3297,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { )).startStandalone(next: { [weak controller] result, options in if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext { if case .starGiftTransfer = source { - presentTransferAlertImpl?(EnginePeer(peer)) + presentTransferAlertImpl?(peer) } else { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.DisallowedGifts(id: peer.id)) |> deliverOnMainQueue).start(next: { disallowedGifts in if let disallowedGifts, disallowedGifts == TelegramDisallowedGifts.All && peer.id != context.account.peerId { - let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Gift_Send_GiftsDisallowed(EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Gift_Send_GiftsDisallowed(peer.compactDisplayTitle).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) controller?.present(alertController, in: .window(.root)) return } @@ -3330,7 +3335,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { if let infoController = self.makePeerInfoController( context: context, updatedPresentationData: nil, - peer: peer._asPeer(), + peer: peer, mode: .generic, avatarInitiallyExpanded: peer.smallProfileImage != nil, fromChat: false, @@ -3478,7 +3483,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { if let controller = context.sharedContext.makePeerInfoController( context: context, updatedPresentationData: nil, - peer: peer._asPeer(), + peer: peer, mode: .gifts, avatarInitiallyExpanded: false, fromChat: false, @@ -3988,6 +3993,9 @@ public final class SharedAccountContextImpl: SharedAccountContext { return messageStatsController(context: context, updatedPresentationData: updatedPresentationData, subject: .message(id: messageId)) } + public func makePollStatsScreen(context: AccountContext, messageId: EngineMessage.Id) -> ViewController { + return PollStatsScreen(context: context, messageId: messageId) + } public func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController { return messageStatsController(context: context, updatedPresentationData: updatedPresentationData, subject: .story(peerId: peerId, id: storyId, item: storyItem, fromStory: fromStory)) } @@ -4008,7 +4016,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { if let infoController = self.makePeerInfoController( context: context, updatedPresentationData: nil, - peer: peer._asPeer(), + peer: peer, mode: .generic, avatarInitiallyExpanded: peer.smallProfileImage != nil, fromChat: false, @@ -4223,11 +4231,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 } @@ -4335,8 +4349,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 { @@ -4406,9 +4420,21 @@ public final class SharedAccountContextImpl: SharedAccountContext { completion: completion ) } + + public func makeEmojiStatusSelectionController(context: AccountContext, mode: EmojiStatusSelectionControllerMode, sourceView: UIView, emojiContent: Signal, currentSelection: Int64?, color: UIColor?, destinationItemView: @escaping () -> UIView?) -> ViewController { + return EmojiStatusSelectionController( + context: context, + mode: mode, + sourceView: sourceView, + emojiContent: emojiContent |> map { $0 as! EmojiPagerContentComponent }, + currentSelection: currentSelection, + color: color, + destinationItemView: destinationItemView + ) + } } -private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? { +private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: EnginePeer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? { var switchToMediaTarget: PeerInfoSwitchToMediaTarget? switch mode { case let .media(kind, index): @@ -4424,15 +4450,18 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation break } - if let _ = peer as? TelegramGroup { - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], switchToMediaTarget: switchToMediaTarget) - } else if let _ = peer as? TelegramChannel { + if case .legacyGroup = peer { + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, reactionSourceMessageId: nil, callMessages: [], switchToMediaTarget: switchToMediaTarget) + } else if case .channel = peer { + var sourceMessageId: MessageId? var forumTopicThread: ChatReplyThreadMessage? var switchToRecommendedChannels = false var switchToGiftsTarget: PeerInfoSwitchToGiftsTarget? var switchToGroupsInCommon = false var switchToStoryFolder: Int64? switch mode { + case let .group(messageId): + sourceMessageId = messageId case let .forumTopic(thread): forumTopicThread = thread case .recommendedChannels: @@ -4448,10 +4477,10 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation default: break } - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], forumTopicThread: forumTopicThread, switchToRecommendedChannels: switchToRecommendedChannels, switchToGiftsTarget: switchToGiftsTarget, switchToGroupsInCommon: switchToGroupsInCommon, switchToStoryFolder: switchToStoryFolder, switchToMediaTarget: switchToMediaTarget) - } else if peer is TelegramUser { - var nearbyPeerDistance: Int32? + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, reactionSourceMessageId: nil, sourceMessageId: sourceMessageId, callMessages: [], forumTopicThread: forumTopicThread, switchToRecommendedChannels: switchToRecommendedChannels, switchToGiftsTarget: switchToGiftsTarget, switchToGroupsInCommon: switchToGroupsInCommon, switchToStoryFolder: switchToStoryFolder, switchToMediaTarget: switchToMediaTarget) + } else if case .user = peer { var reactionSourceMessageId: MessageId? + var sourceMessageId: MessageId? var callMessages: [Message] = [] var hintGroupInCommon: PeerId? var forumTopicThread: ChatReplyThreadMessage? @@ -4462,16 +4491,16 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation var switchToStoryFolder: Int64? switch mode { - case let .nearbyPeer(distance): - nearbyPeerDistance = distance case let .calls(messages): callMessages = messages case .generic: break - case let .group(id): - hintGroupInCommon = id + case let .group(messageId): + hintGroupInCommon = messageId.peerId + sourceMessageId = messageId case let .reaction(messageId): reactionSourceMessageId = messageId + sourceMessageId = messageId case let .forumTopic(thread): forumTopicThread = thread case .myProfile: @@ -4501,9 +4530,9 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation default: break } - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, isMyProfile: isMyProfile, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread, sharedMediaFromForumTopic: sharedMediaFromForumTopic, switchToGiftsTarget: switchToGiftsTarget, switchToGroupsInCommon: switchToGroupsInCommon, switchToStoryFolder: switchToStoryFolder, switchToMediaTarget: switchToMediaTarget) - } else if peer is TelegramSecretChat { - return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], switchToMediaTarget: switchToMediaTarget) + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, reactionSourceMessageId: reactionSourceMessageId, sourceMessageId: sourceMessageId, callMessages: callMessages, isMyProfile: isMyProfile, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread, sharedMediaFromForumTopic: sharedMediaFromForumTopic, switchToGiftsTarget: switchToGiftsTarget, switchToGroupsInCommon: switchToGroupsInCommon, switchToStoryFolder: switchToStoryFolder, switchToMediaTarget: switchToMediaTarget) + } else if case .secretChat = peer { + return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, reactionSourceMessageId: nil, callMessages: [], switchToMediaTarget: switchToMediaTarget) } return nil } diff --git a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift index 7d48f091fa..b53bcaab96 100644 --- a/submodules/TelegramUI/Sources/SharedMediaPlayer.swift +++ b/submodules/TelegramUI/Sources/SharedMediaPlayer.swift @@ -1,7 +1,6 @@ import Foundation import UIKit import SwiftSignalKit -import Postbox import TelegramCore import TelegramUIPreferences import UniversalMediaPlayer @@ -114,7 +113,7 @@ private enum SharedMediaPlaybackItem: Equatable { final class SharedMediaPlayer { private weak var context: AccountContext? private weak var mediaManager: MediaManager? - let account: Account + let engine: TelegramEngine private let audioSession: ManagedAudioSession private let overlayMediaManager: OverlayMediaManager private let playerIndex: Int32 @@ -181,10 +180,10 @@ final class SharedMediaPlayer { let type: MediaManagerPlayerType - init(context: AccountContext, mediaManager: MediaManager, inForeground: Signal, account: Account, audioSession: ManagedAudioSession, overlayMediaManager: OverlayMediaManager, playlist: SharedMediaPlaylist, initialOrder: MusicPlaybackSettingsOrder, initialLooping: MusicPlaybackSettingsLooping, initialPlaybackRate: AudioPlaybackRate, playerIndex: Int32, controlPlaybackWithProximity: Bool, type: MediaManagerPlayerType, continueInstantVideoLoopAfterFinish: Bool) { + init(context: AccountContext, mediaManager: MediaManager, inForeground: Signal, engine: TelegramEngine, audioSession: ManagedAudioSession, overlayMediaManager: OverlayMediaManager, playlist: SharedMediaPlaylist, initialOrder: MusicPlaybackSettingsOrder, initialLooping: MusicPlaybackSettingsLooping, initialPlaybackRate: AudioPlaybackRate, playerIndex: Int32, controlPlaybackWithProximity: Bool, type: MediaManagerPlayerType, continueInstantVideoLoopAfterFinish: Bool) { self.context = context self.mediaManager = mediaManager - self.account = account + self.engine = engine self.audioSession = audioSession self.overlayMediaManager = overlayMediaManager playlist.setOrder(initialOrder) @@ -233,13 +232,13 @@ final class SharedMediaPlayer { case .voice, .music: switch playbackData.source { case let .telegramFile(fileReference, _, _): - strongSelf.playbackItem = .audio(MediaPlayer(audioSessionManager: strongSelf.audioSession, postbox: strongSelf.account.postbox, userLocation: .other, userContentType: .audio, resourceReference: fileReference.resourceReference(fileReference.media.resource), streamable: playbackData.type == .music ? .conservative : .none, video: false, preferSoftwareDecoding: false, enableSound: true, baseRate: rateValue, fetchAutomatically: true, playAndRecord: controlPlaybackWithProximity, isAudioVideoMessage: playbackData.type == .voice)) + strongSelf.playbackItem = .audio(MediaPlayer(audioSessionManager: strongSelf.audioSession, postbox: strongSelf.engine.account.postbox, userLocation: .other, userContentType: .audio, resourceReference: fileReference.resourceReference(fileReference.media.resource), streamable: playbackData.type == .music ? .conservative : .none, video: false, preferSoftwareDecoding: false, enableSound: true, baseRate: rateValue, fetchAutomatically: true, playAndRecord: controlPlaybackWithProximity, isAudioVideoMessage: playbackData.type == .voice)) } case .instantVideo: if let mediaManager = strongSelf.mediaManager, let context = strongSelf.context, let item = item as? MessageMediaPlaylistItem { switch playbackData.source { case let .telegramFile(fileReference, _, _): - let videoNode = OverlayInstantVideoNode(context: context, postbox: strongSelf.account.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.stableId, fileReference.media.fileId), userLocation: .peer(item.message.id.peerId), fileReference: fileReference, enableSound: false, baseRate: rateValue, isAudioVideoMessage: true, captureProtected: item.message.isCopyProtected(), storeAfterDownload: nil), close: { [weak mediaManager] in + let videoNode = OverlayInstantVideoNode(context: context, postbox: strongSelf.engine.account.postbox, audioSession: strongSelf.audioSession, manager: mediaManager.universalVideoManager, content: NativeVideoContent(id: .message(item.message.stableId, fileReference.media.fileId), userLocation: .peer(item.message.id.peerId), fileReference: fileReference, enableSound: false, baseRate: rateValue, isAudioVideoMessage: true, captureProtected: item.message.isCopyProtected(), storeAfterDownload: nil), close: { [weak mediaManager] in mediaManager?.setPlaylist(nil, type: .voice, control: .playback(.pause)) }) strongSelf.playbackItem = .instantVideo(videoNode) @@ -497,9 +496,9 @@ final class SharedMediaPlayer { let fetchedNextSignal: Signal switch current { case let .telegramFile(file, _, _): - fetchedCurrentSignal = self.account.postbox.mediaBox.resourceData(file.media.resource) + fetchedCurrentSignal = self.engine.resources.data(resource: EngineMediaResource(file.media.resource)) |> mapToSignal { data -> Signal in - if data.complete { + if data.isComplete { return .single(Void()) } else { return .complete() @@ -510,7 +509,7 @@ final class SharedMediaPlayer { } switch next { case let .telegramFile(file, _, _): - fetchedNextSignal = fetchedMediaResource(mediaBox: self.account.postbox.mediaBox, userLocation: .other, userContentType: .audio, reference: file.resourceReference(file.media.resource)) + fetchedNextSignal = self.engine.resources.fetch(reference: file.resourceReference(file.media.resource), userLocation: .other, userContentType: .audio) |> ignoreValues |> `catch` { _ -> Signal in return .complete() diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 11cdee3266..db5f613c5c 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -41,7 +41,7 @@ private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceh init(context: AccountContext) { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, preferredGlassType: .default, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, preferredGlassType: .default, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: context.account.peerId), subject: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: true) self.emptyNode = ChatEmptyNode(context: context, interaction: nil) @@ -55,7 +55,7 @@ private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceh func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData let preferredGlassType = self.presentationInterfaceState.preferredGlassType - self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, preferredGlassType: preferredGlassType, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.presentationInterfaceState.limitsConfiguration, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.presentationInterfaceState.accountPeerId, mode: .standard(.default), chatLocation: self.presentationInterfaceState.chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) + self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: self.presentationData.chatWallpaper, theme: self.presentationData.theme, preferredGlassType: preferredGlassType, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.presentationInterfaceState.limitsConfiguration, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.presentationInterfaceState.accountPeerId, mode: .standard(.default), chatLocation: self.presentationInterfaceState.chatLocation, subject: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil, businessIntro: nil) self.wallpaperBackgroundNode.update(wallpaper: presentationData.chatWallpaper, animated: false) } @@ -230,7 +230,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon sharedContext.switchingData = (nil, nil, nil) } - let accountSettingsController = PeerInfoScreenImpl(context: self.context, updatedPresentationData: nil, peerId: self.context.account.peerId, avatarInitiallyExpanded: false, isOpenedFromChat: false, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], isSettings: true) + let accountSettingsController = PeerInfoScreenImpl(context: self.context, updatedPresentationData: nil, peerId: self.context.account.peerId, avatarInitiallyExpanded: false, isOpenedFromChat: false, reactionSourceMessageId: nil, callMessages: [], isSettings: true) accountSettingsController.tabBarItemDebugTapAction = { [weak self] in guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Sources/TextLinkHandling.swift b/submodules/TelegramUI/Sources/TextLinkHandling.swift index b6e53647a7..de144e980d 100644 --- a/submodules/TelegramUI/Sources/TextLinkHandling.swift +++ b/submodules/TelegramUI/Sources/TextLinkHandling.swift @@ -18,12 +18,15 @@ import UndoUI import BrowserUI func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, navigateDisposable: MetaDisposable, controller: ViewController, action: TextLinkItemActionType, itemLink: TextLinkItem) { + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + let presentImpl: (ViewController, Any?) -> Void = { controllerToPresent, _ in controller.present(controllerToPresent, in: .window(.root)) } let openResolvedPeerImpl: (EnginePeer?, ChatControllerInteractionNavigateToPeer) -> Void = { [weak controller] peer, navigation in - guard let peer = peer else { + guard let peer else { + controller?.present(textAlertController(context: context, updatedPresentationData: nil, title: nil, text: presentationData.strings.Resolve_ErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return } context.sharedContext.openResolvedUrl(.peer(peer._asPeer(), navigation), context: context, urlContext: .generic, navigationController: (controller?.navigationController as? NavigationController), forceExternal: false, forceUpdate: false, openPeer: { (peer, navigation) in @@ -39,7 +42,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n ) navigateDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { peer in if let controller = controller, let peer = peer { - if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { (controller.navigationController as? NavigationController)?.pushViewController(infoController) } } @@ -73,7 +76,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n if let controller = controller { switch result { case let .externalUrl(url): - context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: false, presentationData: context.sharedContext.currentPresentationData.with({ $0 }), navigationController: controller.navigationController as? NavigationController, dismissInput: { + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: false, presentationData: presentationData, navigationController: controller.navigationController as? NavigationController, dismissInput: { }) case let .peer(peer, navigation): openResolvedPeerImpl(peer.flatMap(EnginePeer.init), navigation) @@ -100,7 +103,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n let sourceLocation = InstantPageSourceLocation(userLocation: peerId.flatMap(MediaResourceUserLocation.peer) ?? .other, peerType: .group) let browserController = context.sharedContext.makeInstantPageController(context: context, webPage: webPage, anchor: anchor, sourceLocation: sourceLocation) (controller.navigationController as? NavigationController)?.pushViewController(browserController, animated: true) - case .boost, .chatFolder, .join, .invoice: + case .boost, .chatFolder, .join, .invoice, .proxy: if let navigationController = controller.navigationController as? NavigationController { openResolvedUrlImpl(result, context: context, urlContext: peerId.flatMap { .chat(peerId: $0, message: nil, updatedPresentationData: nil) } ?? .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigateToPeer in openResolvedPeerImpl(peer, navigateToPeer) @@ -128,7 +131,6 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n })) } - let presentationData = context.sharedContext.currentPresentationData.with { $0 } switch action { case .tap: switch itemLink { @@ -156,10 +158,17 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: EnginePeer.Id?, n openPeerMentionImpl(mention) case let .hashtag(_, hashtag): if let peerId = peerId { - let peerSignal = context.account.postbox.loadedPeerWithId(peerId) + let peerSignal = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> mapToSignal { peer -> Signal in + if let peer { + return .single(peer) + } else { + return .never() + } + } let _ = (peerSignal |> deliverOnMainQueue).start(next: { peer in - let searchController = HashtagSearchController(context: context, peer: EnginePeer(peer), query: hashtag) + let searchController = HashtagSearchController(context: context, peer: peer, query: hashtag) (controller.navigationController as? NavigationController)?.pushViewController(searchController) }) } diff --git a/submodules/TelegramUI/Sources/ThemeUpdateManager.swift b/submodules/TelegramUI/Sources/ThemeUpdateManager.swift index c157d3ef20..36ac6f790c 100644 --- a/submodules/TelegramUI/Sources/ThemeUpdateManager.swift +++ b/submodules/TelegramUI/Sources/ThemeUpdateManager.swift @@ -109,7 +109,7 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager { guard complete, let fullSizeData = fullSizeData else { return .complete() } - accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true) + accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: fullSizeData, synchronous: true) return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper, creatorAccountId: theme.isCreator ? account.id : nil)), presentationTheme)) } } else { diff --git a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift index 5ef372684d..a352a13a08 100644 --- a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputContextPanelNode.swift @@ -100,12 +100,12 @@ private enum VerticalListContextResultsChatInputContextPanelEntry: Comparable, I } } - func item(account: Account, actionSelected: @escaping () -> Void, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> ListViewItem { + func item(engine: TelegramEngine, actionSelected: @escaping () -> Void, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> ListViewItem { switch self { case let .action(theme, title): return VerticalListContextResultsChatInputPanelButtonItem(theme: theme, title: title, pressed: actionSelected) case let .result(_, theme, result): - return VerticalListContextResultsChatInputPanelItem(account: account, theme: theme, result: result, resultSelected: resultSelected) + return VerticalListContextResultsChatInputPanelItem(engine: engine, theme: theme, result: result, resultSelected: resultSelected) } } } @@ -116,12 +116,12 @@ private struct VerticalListContextResultsChatInputContextPanelTransition { let updates: [ListViewUpdateItem] } -private func preparedTransition(from fromEntries: [VerticalListContextResultsChatInputContextPanelEntry], to toEntries: [VerticalListContextResultsChatInputContextPanelEntry], account: Account, actionSelected: @escaping () -> Void, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> VerticalListContextResultsChatInputContextPanelTransition { +private func preparedTransition(from fromEntries: [VerticalListContextResultsChatInputContextPanelEntry], to toEntries: [VerticalListContextResultsChatInputContextPanelEntry], engine: TelegramEngine, actionSelected: @escaping () -> Void, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) -> VerticalListContextResultsChatInputContextPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, actionSelected: actionSelected, resultSelected: resultSelected), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, actionSelected: actionSelected, resultSelected: resultSelected), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, actionSelected: actionSelected, resultSelected: resultSelected), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, actionSelected: actionSelected, resultSelected: resultSelected), directionHint: nil) } return VerticalListContextResultsChatInputContextPanelTransition(deletions: deletions, insertions: insertions, updates: updates) } @@ -239,7 +239,7 @@ final class VerticalListContextResultsChatInputContextPanelNode: ChatInputContex private func prepareTransition(from: [VerticalListContextResultsChatInputContextPanelEntry]?, to: [VerticalListContextResultsChatInputContextPanelEntry], results: ChatContextResultCollection) { let firstTime = self.currentEntries == nil - let transition = preparedTransition(from: from ?? [], to: to, account: self.context.account, actionSelected: { [weak self] in + let transition = preparedTransition(from: from ?? [], to: to, engine: self.context.engine, actionSelected: { [weak self] in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { if let switchPeer = results.switchPeer { interfaceInteraction.botSwitchChatWithPayload(results.botId, switchPeer.startParam) diff --git a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift index 86c40fa8a4..60c4255d33 100644 --- a/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/Sources/VerticalListContextResultsChatInputPanelItem.swift @@ -10,15 +10,15 @@ import PhotoResources import StickerResources final class VerticalListContextResultsChatInputPanelItem: ListViewItem { - fileprivate let account: Account + fileprivate let engine: TelegramEngine fileprivate let theme: PresentationTheme fileprivate let result: ChatContextResult fileprivate let resultSelected: (ChatContextResult, ASDisplayNode, CGRect) -> Bool - + let selectable: Bool = true - - public init(account: Account, theme: PresentationTheme, result: ChatContextResult, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) { - self.account = account + + public init(engine: TelegramEngine, theme: PresentationTheme, result: ChatContextResult, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) { + self.engine = engine self.theme = theme self.result = result self.resultSelected = resultSelected @@ -226,8 +226,7 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) iconImageApply = iconImageLayout(arguments) - updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(imageResource) - |> map(EngineMediaResource.FetchStatus.init) + updatedStatusSignal = item.engine.resources.status(resource: EngineMediaResource(imageResource)) } var updatedIconImageResource = false @@ -242,11 +241,11 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { if updatedIconImageResource { if let imageResource = imageResource { if let stickerFile = stickerFile { - updateIconImageSignal = chatMessageSticker(account: item.account, userLocation: .other, file: stickerFile, small: false, fetched: true) + updateIconImageSignal = chatMessageSticker(account: item.engine.account, userLocation: .other, file: stickerFile, small: false, fetched: true) } else { let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 55, height: 55), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) let tmpImage = TelegramMediaImage(imageId: EngineMedia.Id(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) - updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, userLocation: .other, photoReference: .standalone(media: tmpImage)) + updateIconImageSignal = chatWebpageSnippetPhoto(account: item.engine.account, userLocation: .other, photoReference: .standalone(media: tmpImage)) } } else { updateIconImageSignal = .complete() diff --git a/submodules/TelegramUI/Sources/WallpaperUploadManager.swift b/submodules/TelegramUI/Sources/WallpaperUploadManager.swift index 5b20cb4bdf..7c732002ad 100644 --- a/submodules/TelegramUI/Sources/WallpaperUploadManager.swift +++ b/submodules/TelegramUI/Sources/WallpaperUploadManager.swift @@ -95,7 +95,7 @@ final class WallpaperUploadManagerImpl: WallpaperUploadManager { switch result { case let .complete(wallpaper): if case let .file(file) = wallpaper { - sharedContext.accountManager.mediaBox.moveResourceData(from: currentResource.id, to: file.file.resource.id) + sharedContext.accountManager.resources.moveResourceData(from: EngineMediaResource.Id(currentResource.id), to: EngineMediaResource.Id(file.file.resource.id)) account.postbox.mediaBox.moveResourceData(from: currentResource.id, to: file.file.resource.id) } default: diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 90c5e4f0d2..1274aaf1fc 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -72,6 +72,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var enablePWA: Bool public var forceClearGlass: Bool public var debugRipple: Bool + public var debugRichText: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -121,7 +122,8 @@ public struct ExperimentalUISettings: Codable, Equatable { enableUpdates: false, enablePWA: false, forceClearGlass: false, - debugRipple: false + debugRipple: false, + debugRichText: false ) } @@ -172,7 +174,8 @@ public struct ExperimentalUISettings: Codable, Equatable { enableUpdates: Bool, enablePWA: Bool, forceClearGlass: Bool, - debugRipple: Bool + debugRipple: Bool, + debugRichText: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -221,6 +224,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enablePWA = enablePWA self.forceClearGlass = forceClearGlass self.debugRipple = debugRipple + self.debugRichText = debugRichText } public init(from decoder: Decoder) throws { @@ -273,6 +277,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.enablePWA = try container.decodeIfPresent(Bool.self, forKey: "enablePWA") ?? false self.forceClearGlass = try container.decodeIfPresent(Bool.self, forKey: "forceClearGlass") ?? false self.debugRipple = try container.decodeIfPresent(Bool.self, forKey: "debugRipple") ?? false + self.debugRichText = try container.decodeIfPresent(Bool.self, forKey: "debugRichText") ?? false } public func encode(to encoder: Encoder) throws { @@ -325,6 +330,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encodeIfPresent(self.enablePWA, forKey: "enablePWA") try container.encodeIfPresent(self.forceClearGlass, forKey: "forceClearGlass") try container.encodeIfPresent(self.debugRipple, forKey: "debugRipple") + try container.encodeIfPresent(self.debugRichText, forKey: "debugRichText") } } diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 61dde36ecb..75d29e5a3b 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -604,20 +604,9 @@ private func refreshTextMentions(text: NSString, initialAttributedText: NSAttrib } } -private let textUrlEdgeCharacters: CharacterSet = { - var set: CharacterSet = .alphanumerics - set.formUnion(.symbols) - set.formUnion(.punctuationCharacters) - set.remove("(") - set.remove(")") - return set -}() - -private let textUrlCharacters: CharacterSet = { - var set: CharacterSet = textUrlEdgeCharacters - set.formUnion(.whitespacesAndNewlines) - return set -}() +private func isTextUrlInnerCharacter(_ c: UnicodeScalar) -> Bool { + return alphanumericCharacters.contains(c) || c == " " as UnicodeScalar +} private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributedString, attributedText: NSMutableAttributedString, fullRange: NSRange) { var textUrlRanges: [(NSRange, ChatTextInputTextUrlAttribute)] = [] @@ -635,7 +624,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed var validLower = range.lowerBound inner1: for i in range.lowerBound ..< range.upperBound { if let c = UnicodeScalar(text.character(at: i)) { - if textUrlCharacters.contains(c) { + if isTextUrlInnerCharacter(c) { validLower = i break inner1 } @@ -646,7 +635,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed var validUpper = range.upperBound inner2: for i in (validLower ..< range.upperBound).reversed() { if let c = UnicodeScalar(text.character(at: i)) { - if textUrlCharacters.contains(c) { + if isTextUrlInnerCharacter(c) { validUpper = i + 1 break inner2 } @@ -658,7 +647,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed let minLower = (i == 0) ? fullRange.lowerBound : textUrlRanges[i - 1].0.upperBound inner3: for i in (minLower ..< validLower).reversed() { if let c = UnicodeScalar(text.character(at: i)) { - if textUrlEdgeCharacters.contains(c) { + if alphanumericCharacters.contains(c) { validLower = i } else { break inner3 @@ -671,7 +660,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed let maxUpper = (i == textUrlRanges.count - 1) ? fullRange.upperBound : textUrlRanges[i + 1].0.lowerBound inner3: for i in validUpper ..< maxUpper { if let c = UnicodeScalar(text.character(at: i)) { - if textUrlEdgeCharacters.contains(c) { + if alphanumericCharacters.contains(c) { validUpper = i + 1 } else { break inner3 @@ -693,7 +682,7 @@ private func refreshTextUrls(text: NSString, initialAttributedText: NSAttributed var combine = true inner: for j in textUrlRanges[i].0.upperBound ..< textUrlRanges[i + 1].0.lowerBound { if let c = UnicodeScalar(text.character(at: j)) { - if textUrlCharacters.contains(c) { + if isTextUrlInnerCharacter(c) { } else { combine = false break inner diff --git a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift index 35cd855bfb..29234377d7 100644 --- a/submodules/TextFormat/Sources/StringWithAppliedEntities.swift +++ b/submodules/TextFormat/Sources/StringWithAppliedEntities.swift @@ -83,6 +83,50 @@ public func chatInputStateStringWithAppliedEntities(_ text: String, entities: [M private let syntaxHighlighter = Syntaxer() +private func isValidMessageSyntaxHighlightRange(_ range: Range, expectedLength: Int) -> Bool { + return range.lowerBound >= 0 && range.upperBound <= expectedLength && range.lowerBound < range.upperBound +} + +private func validatedCachedMessageSyntaxHighlight(_ highlight: MessageSyntaxHighlight, expectedLength: Int, language: String) -> MessageSyntaxHighlight? { + for entity in highlight.entities { + if !isValidMessageSyntaxHighlightRange(entity.range, expectedLength: expectedLength) { + return nil + } + } + return highlight +} + +private func generateMessageSyntaxHighlight(spec: CachedMessageSyntaxHighlight.Spec, theme: SyntaxterTheme) -> MessageSyntaxHighlight { + let expectedLength = (spec.text as NSString).length + guard let syntaxHighlighter else { + return MessageSyntaxHighlight(entities: []) + } + guard let highlightedString = syntaxHighlighter.syntax(spec.text, language: spec.language, theme: theme) else { + return MessageSyntaxHighlight(entities: []) + } + guard highlightedString.length == expectedLength else { + return MessageSyntaxHighlight(entities: []) + } + + var entities: [MessageSyntaxHighlight.Entity] = [] + var hasInvalidRange = false + highlightedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: highlightedString.length), using: { value, subRange, stop in + let range = subRange.lowerBound ..< subRange.upperBound + if !isValidMessageSyntaxHighlightRange(range, expectedLength: expectedLength) { + hasInvalidRange = true + stop.pointee = true + return + } + if let value = value as? UIColor, value != .black { + entities.append(MessageSyntaxHighlight.Entity(color: Int32(bitPattern: value.rgb), range: range)) + } + }) + if hasInvalidRange { + return MessageSyntaxHighlight(entities: []) + } + return MessageSyntaxHighlight(entities: entities) +} + public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEntity], strings: PresentationStrings? = nil, dateTimeFormat: PresentationDateTimeFormat? = nil, baseColor: UIColor, linkColor: UIColor, baseQuoteTintColor: UIColor? = nil, baseQuoteSecondaryTintColor: UIColor? = nil, baseQuoteTertiaryTintColor: UIColor? = nil, codeBlockTitleColor: UIColor? = nil, codeBlockAccentColor: UIColor? = nil, codeBlockBackgroundColor: UIColor? = nil, baseFont: UIFont, linkFont: UIFont, boldFont: UIFont, italicFont: UIFont, boldItalicFont: UIFont, fixedFont: UIFont, blockQuoteFont: UIFont, underlineLinks: Bool = true, external: Bool = false, message: Message?, entityFiles: [MediaId: TelegramMediaFile] = [:], adjustQuoteFontSize: Bool = false, cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, paragraphAlignment: NSTextAlignment? = nil) -> NSAttributedString { let baseQuoteTintColor = baseQuoteTintColor ?? baseColor @@ -399,8 +443,10 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti let codeText = (string.string as NSString).substring(with: range) if let cachedMessageSyntaxHighlight, let entry = cachedMessageSyntaxHighlight.values[CachedMessageSyntaxHighlight.Spec(language: language, text: codeText)] { - for entity in entry.entities { - string.addAttribute(.foregroundColor, value: UIColor(rgb: UInt32(bitPattern: entity.color)), range: NSRange(location: range.location + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + if let validatedEntry = validatedCachedMessageSyntaxHighlight(entry, expectedLength: range.length, language: language) { + for entity in validatedEntry.entities { + string.addAttribute(.foregroundColor, value: UIColor(rgb: UInt32(bitPattern: entity.color)), range: NSRange(location: range.location + entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + } } } }) @@ -588,8 +634,18 @@ public func extractMessageSyntaxHighlightSpecs(text: String, entities: [MessageT private let internalFixedCodeFont = Font.regular(17.0) public func asyncUpdateMessageSyntaxHighlight(engine: TelegramEngine, messageId: EngineMessage.Id, current: CachedMessageSyntaxHighlight?, specs: [CachedMessageSyntaxHighlight.Spec]) -> Signal { - if let current, !specs.contains(where: { current.values[$0] == nil }) { - return .complete() + if let current { + var hasMissingOrInvalidSpec = false + for spec in specs { + let expectedLength = (spec.text as NSString).length + guard let value = current.values[spec], validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) != nil else { + hasMissingOrInvalidSpec = true + break + } + } + if !hasMissingOrInvalidSpec { + return .complete() + } } return Signal { subscriber in @@ -598,22 +654,11 @@ public func asyncUpdateMessageSyntaxHighlight(engine: TelegramEngine, messageId: let theme = SyntaxterTheme(dark: false, textColor: .black, textFont: internalFixedCodeFont, italicFont: internalFixedCodeFont, mediumFont: internalFixedCodeFont) for spec in specs { + let expectedLength = (spec.text as NSString).length if let value = current?.values[spec] { - updated[spec] = value - } else { - var entities: [MessageSyntaxHighlight.Entity] = [] - - if let syntaxHighlighter { - if let highlightedString = syntaxHighlighter.syntax(spec.text, language: spec.language, theme: theme) { - highlightedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: highlightedString.length), using: { value, subRange, _ in - if let value = value as? UIColor, value != .black { - entities.append(MessageSyntaxHighlight.Entity(color: Int32(bitPattern: value.rgb), range: subRange.lowerBound ..< subRange.upperBound)) - } - }) - } - } - - updated[spec] = MessageSyntaxHighlight(entities: entities) + updated[spec] = validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) ?? MessageSyntaxHighlight(entities: []) + } else if let theme { + updated[spec] = generateMessageSyntaxHighlight(spec: spec, theme: theme) } } @@ -629,8 +674,18 @@ public func asyncUpdateMessageSyntaxHighlight(engine: TelegramEngine, messageId: } public func asyncStanaloneSyntaxHighlight(current: CachedMessageSyntaxHighlight?, specs: [CachedMessageSyntaxHighlight.Spec]) -> Signal { - if let current, !specs.contains(where: { current.values[$0] == nil }) { - return .single(current) + if let current { + var hasMissingOrInvalidSpec = false + for spec in specs { + let expectedLength = (spec.text as NSString).length + guard let value = current.values[spec], validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) != nil else { + hasMissingOrInvalidSpec = true + break + } + } + if !hasMissingOrInvalidSpec { + return .single(current) + } } return Signal { subscriber in @@ -639,22 +694,11 @@ public func asyncStanaloneSyntaxHighlight(current: CachedMessageSyntaxHighlight? let theme = SyntaxterTheme(dark: false, textColor: .black, textFont: internalFixedCodeFont, italicFont: internalFixedCodeFont, mediumFont: internalFixedCodeFont) for spec in specs { + let expectedLength = (spec.text as NSString).length if let value = current?.values[spec] { - updated[spec] = value - } else { - var entities: [MessageSyntaxHighlight.Entity] = [] - - if let syntaxHighlighter { - if let highlightedString = syntaxHighlighter.syntax(spec.text, language: spec.language, theme: theme) { - highlightedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: highlightedString.length), using: { value, subRange, _ in - if let value = value as? UIColor, value != .black { - entities.append(MessageSyntaxHighlight.Entity(color: Int32(bitPattern: value.rgb), range: subRange.lowerBound ..< subRange.upperBound)) - } - }) - } - } - - updated[spec] = MessageSyntaxHighlight(entities: entities) + updated[spec] = validatedCachedMessageSyntaxHighlight(value, expectedLength: expectedLength, language: spec.language) ?? MessageSyntaxHighlight(entities: []) + } else if let theme { + updated[spec] = generateMessageSyntaxHighlight(spec: spec, theme: theme) } } diff --git a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift index a779d44732..9754b3e8dd 100644 --- a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift +++ b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift @@ -531,7 +531,13 @@ public final class TextSelectionNode: ASDisplayNode { }) } - return adjustedRange + func normalizedSelectionRange(_ range: NSRange, length: Int) -> NSRange { + let location = min(max(range.location, 0), length) + let upperBound = min(max(location, range.location + range.length), length) + return NSRange(location: location, length: upperBound - location) + } + + return normalizedSelectionRange(adjustedRange, length: attributedString.length) } private func convertSelectionFromOriginalText(attributedString: NSAttributedString, range: NSRange) -> NSRange { diff --git a/submodules/TranslateUI/BUILD b/submodules/TranslateUI/BUILD index b559ed1036..dfe9b76ada 100644 --- a/submodules/TranslateUI/BUILD +++ b/submodules/TranslateUI/BUILD @@ -13,7 +13,6 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AsyncDisplayKit:AsyncDisplayKit", "//submodules/Display:Display", - "//submodules/Postbox:Postbox", "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/AccountContext:AccountContext", diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 47f9cd7ab1..bc0e2e3aba 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -361,14 +361,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil - self.animationNode = AnimationNode(animation: "anim_banned", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0) + self.animationNode = AnimationNode(animation: "anim_banned", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 0.9) self.animatedStickerNode = nil let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .natural) self.textNode.attributedText = attributedText - self.textNode.maximumNumberOfLines = 2 + self.textNode.maximumNumberOfLines = 5 displayUndo = false self.originalRemainingSeconds = 5 case let .importedMessage(text): @@ -597,7 +597,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { updatedImageSignal = chatMessageStickerPackThumbnail(postbox: context.account.postbox, resource: resource._asResource(), animated: true) } if let resourceReference = resourceReference { - updatedFetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resourceReference) + updatedFetchSignal = context.engine.resources.fetch(reference: resourceReference, userLocation: .other, userContentType: .other) |> mapError { _ -> EngineMediaResource.Fetch.Error in return .generic } @@ -914,7 +914,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { updatedImageSignal = chatMessageStickerPackThumbnail(postbox: context.account.postbox, resource: resource._asResource(), animated: true) } if let resourceReference = resourceReference { - updatedFetchSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resourceReference) + updatedFetchSignal = context.engine.resources.fetch(reference: resourceReference, userLocation: .other, userContentType: .other) |> mapError { _ -> EngineMediaResource.Fetch.Error in return .generic } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 8e54a2d406..d1102a40b0 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -54,6 +54,9 @@ extension ResolvedBotAdminRights { if components.contains("manage_chat") { rawValue |= ResolvedBotAdminRights.manageChat.rawValue } + if components.contains("manage_topics") { + rawValue |= ResolvedBotAdminRights.manageTopics.rawValue + } if components.contains("anonymous") { rawValue |= ResolvedBotAdminRights.canBeAnonymous.rawValue } @@ -128,6 +131,7 @@ public enum ParsedInternalUrl { case auction(slug: String) case oauth(url: String) case createBot(parentBot: String, username: String?, title: String?) + case textStyle(slug: String) case externalUrl(url: String) } @@ -600,9 +604,12 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou return .peer(.name(pathComponents[1]), .boost) } else if pathComponents[0] == "giftcode", pathComponents.count == 2 { return .premiumGiftCode(slug: pathComponents[1]) - } else if pathComponents.count >= 3 && pathComponents[0] == "newbot" { + } else if pathComponents.count >= 2 && pathComponents[0] == "newbot" { let parentBot = pathComponents[1] - let username = pathComponents[2] + var username: String? + if pathComponents.count >= 3 { + username = pathComponents[2] + } var title: String? for queryItem in components.queryItems ?? [] { if let value = queryItem.value { @@ -612,6 +619,8 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou } } return .createBot(parentBot: parentBot, username: username, title: title) + } else if pathComponents.count >= 2 && pathComponents[0] == "addstyle" { + return .textStyle(slug: pathComponents[1]) } else if pathComponents[0] == "m" { return .messageLink(slug: pathComponents[1]) } else if pathComponents.count == 3 && pathComponents[0] == "c" { @@ -1335,6 +1344,19 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } } } + case let .textStyle(slug): + let signal: Signal = context.engine.messages.requestAIMessageStyle(slug: slug) + |> map { result -> ResolveInternalUrlResult in + guard let result else { + return .result(nil) + } + guard case let .custom(style) = result.style.content else { + return .result(nil) + } + return .result(.textStyle(style: style, initialPreview: result.initialPreview)) + } + return .single(.progress) + |> then(signal) } } diff --git a/submodules/WallpaperResources/BUILD b/submodules/WallpaperResources/BUILD index 8fcb8fa8c2..999d61068c 100644 --- a/submodules/WallpaperResources/BUILD +++ b/submodules/WallpaperResources/BUILD @@ -21,7 +21,7 @@ swift_library( "//submodules/PhotoResources:PhotoResources", "//submodules/PersistentStringHash:PersistentStringHash", "//submodules/AppBundle:AppBundle", - "//submodules/Svg:Svg", + "//submodules/Svg/LegacyImpl", "//submodules/GZip:GZip", "//submodules/GradientBackground:GradientBackground", "//submodules/TelegramPresentationData:TelegramPresentationData", diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index 588c1cc851..438b4904d5 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -14,7 +14,7 @@ import LocalMediaResources import TelegramPresentationData import TelegramUIPreferences import AppBundle -import Svg +import LegacyImpl import GradientBackground import GZip @@ -954,11 +954,11 @@ public func photoWallpaper(postbox: Postbox, photoLibraryResource: PhotoLibraryM } public func telegramThemeData(account: Account, accountManager: AccountManager, reference: MediaResourceReference, synchronousLoad: Bool = false) -> Signal { - let maybeFetched = accountManager.mediaBox.resourceData(reference.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) + let maybeFetched = accountManager.resources.data(resource: EngineMediaResource(reference.resource), attemptSynchronously: synchronousLoad) return maybeFetched |> take(1) |> mapToSignal { maybeData in - if maybeData.complete { + if maybeData.isComplete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(loadedData) } else { @@ -970,7 +970,7 @@ public func telegramThemeData(account: Account, accountManager: AccountManager take(1) |> mapToSignal { maybeData -> Signal<(PresentationTheme?, Data?), NoError> in - if maybeData.complete && isSupportedTheme { + if maybeData.isComplete && isSupportedTheme { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((loadedData.flatMap { makePresentationTheme(data: $0) }, nil)) } else { @@ -1211,7 +1211,7 @@ public func themeImage(account: Account, accountManager: AccountManager mapToSignal { data in - if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + if data.isComplete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { return .single((theme, .pattern(data: imageData, colors: file.settings.colors, intensity: intensity), thumbnailData)) } else { return .complete() @@ -1360,7 +1360,7 @@ public func themeImage(account: Account, accountManager: AccountManager Void, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (UIImage) -> Void, present: @escaping (ViewController, Any?) -> Void) { - guard let item = legacyWebSearchItem(account: context.account, result: result) else { + guard let item = legacyWebSearchItem(engine: context.engine, result: result) else { return } var screenImage: Signal = .single(nil) if let resource = item.thumbnailResource { - screenImage = context.account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: true) + screenImage = context.engine.resources.data(resource: EngineMediaResource(resource), attemptSynchronously: true) |> map { maybeData -> UIImage? in - if maybeData.complete { + if maybeData.isComplete { if let loadedData = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []), let image = UIImage(data: loadedData) { return image } diff --git a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift index 29f65144f9..ac7aee4ab9 100644 --- a/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift +++ b/submodules/WebSearchUI/Sources/LegacyWebSearchGallery.swift @@ -11,7 +11,6 @@ import AccountContext import PhotoResources import LegacyUI import LegacyMediaPickerUI -import Postbox class LegacyWebSearchItem: NSObject, TGMediaEditableItem, TGMediaSelectableItem { var isVideo: Bool { @@ -209,7 +208,7 @@ private class LegacyWebSearchGalleryItemView: TGModernGalleryImageItemView, TGMo } } -func legacyWebSearchItem(account: Account, result: ChatContextResult) -> LegacyWebSearchItem? { +func legacyWebSearchItem(engine: TelegramEngine, result: ChatContextResult) -> LegacyWebSearchItem? { var thumbnailDimensions: CGSize? var thumbnailResource: TelegramMediaResource? var imageResource: TelegramMediaResource? @@ -246,7 +245,7 @@ func legacyWebSearchItem(account: Account, result: ChatContextResult) -> LegacyW } if let imageResource = imageResource { - let progressSignal = account.postbox.mediaBox.resourceStatus(imageResource) + let progressSignal = engine.resources.status(resource: EngineMediaResource(imageResource)) |> map { status -> Float in switch status { case .Local: @@ -264,7 +263,7 @@ func legacyWebSearchItem(account: Account, result: ChatContextResult) -> LegacyW } representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(imageDimensions), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let tmpImage = TelegramMediaImage(imageId: EngineMedia.Id(namespace: 0, id: 0), representations: representations, immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) - thumbnailSignal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage), autoFetchFullSize: false) + thumbnailSignal = chatMessagePhotoDatas(postbox: engine.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage), autoFetchFullSize: false) |> mapToSignal { value -> Signal in let thumbnailData = value._0 if let data = thumbnailData, let image = UIImage(data: data) { @@ -273,7 +272,7 @@ func legacyWebSearchItem(account: Account, result: ChatContextResult) -> LegacyW return .complete() } } - originalSignal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage), autoFetchFullSize: true) + originalSignal = chatMessagePhotoDatas(postbox: engine.account.postbox, userLocation: .other, photoReference: .standalone(media: tmpImage), autoFetchFullSize: true) |> mapToSignal { value -> Signal in let thumbnailData = value._0 let fullSizeData = value._1 @@ -295,11 +294,11 @@ func legacyWebSearchItem(account: Account, result: ChatContextResult) -> LegacyW } } -private func galleryItems(account: Account, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext) -> ([TGModernGalleryItem], TGModernGalleryItem?) { +private func galleryItems(engine: TelegramEngine, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext) -> ([TGModernGalleryItem], TGModernGalleryItem?) { var focusItem: TGModernGalleryItem? var galleryItems: [TGModernGalleryItem] = [] for result in results { - if let item = legacyWebSearchItem(account: account, result: result) { + if let item = legacyWebSearchItem(engine: engine, result: result) { let galleryItem = LegacyWebSearchGalleryItem(item: item) galleryItem.selectionContext = selectionContext galleryItem.editingContext = editingContext @@ -336,7 +335,7 @@ func presentLegacyWebSearchGallery(context: AccountContext, peer: EnginePeer?, t controller.asyncTransitionIn = true legacyController.bind(controller: controller) - let (items, focusItem) = galleryItems(account: context.account, results: results, current: current, selectionContext: selectionContext, editingContext: editingContext) + let (items, focusItem) = galleryItems(engine: context.engine, results: results, current: current, selectionContext: selectionContext, editingContext: editingContext) let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: false, allowCaptionEntities: true, hasTimer: false, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: false, hasCamera: false, recipientName: recipientName, isScheduledMessages: false, hasCoverButton: false)! model.stickersContext = paintStickersContext diff --git a/submodules/WebSearchUI/Sources/WebSearchController.swift b/submodules/WebSearchUI/Sources/WebSearchController.swift index 9e960e9a63..d82b94462d 100644 --- a/submodules/WebSearchUI/Sources/WebSearchController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchController.swift @@ -16,7 +16,7 @@ public enum WebSearchMode { } public enum WebSearchControllerMode { - case media(attachment: Bool, completion: (ChatContextResultCollection, TGMediaSelectionContext, TGMediaEditingContext, Bool) -> Void) + case media(attachment: Bool, completion: (ChatContextResultCollection, TGMediaSelectionContext, TGMediaEditingContext, Bool, Int32?) -> Void) case editor(completion: (UIImage) -> Void) case avatar(initialQuery: String?, completion: (UIImage) -> Void) @@ -115,7 +115,7 @@ public final class WebSearchController: ViewController { } } - public var presentSchedulePicker: (Bool, @escaping (Int32) -> Void) -> Void = { _, _ in } + public var presentSchedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { _, _ in } public var dismissed: () -> Void = { } @@ -161,9 +161,9 @@ public final class WebSearchController: ViewController { } } - let gifProvider = self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) - |> map { view -> String? in - guard let appConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) else { + let gifProvider = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ApplicationSpecificPreference(key: PreferencesKeys.appConfiguration)) + |> map { preferencesEntry -> String? in + guard let appConfiguration = preferencesEntry?.get(AppConfiguration.self) else { return nil } let configuration = WebSearchConfiguration(appConfiguration: appConfiguration) @@ -261,13 +261,13 @@ public final class WebSearchController: ViewController { selectionState.setItem(currentItem, selected: true) } if case let .media(_, sendSelected) = mode { - sendSelected(results, selectionState, editingState, false) + sendSelected(results, selectionState, editingState, silently, scheduleTime) } } }, schedule: { [weak self] messageEffect in if let strongSelf = self { - strongSelf.presentSchedulePicker(false, { [weak self] time in - self?.controllerInteraction?.sendSelected(nil, false, time, nil) + strongSelf.presentSchedulePicker(false, { [weak self] time, silentPosting in + self?.controllerInteraction?.sendSelected(nil, silentPosting, time, nil) }) } }, avatarCompleted: { result in diff --git a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift b/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift index 75d2142a62..0aabc3e62a 100644 --- a/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift +++ b/submodules/WebSearchUI/Sources/WebSearchControllerNode.swift @@ -792,7 +792,7 @@ class WebSearchControllerNode: ASDisplayNode { } } else { if let mode = self.controller?.mode, case let .editor(completion) = mode { - if let item = legacyWebSearchItem(account: self.context.account, result: currentResult) { + if let item = legacyWebSearchItem(engine: self.context.engine, result: currentResult) { let _ = (item.originalImage |> deliverOnMainQueue).start(next: { image in if !image.degraded() { diff --git a/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift b/submodules/WebSearchUI/Sources/WebSearchGalleryController.swift index b6d8b61f14..dcf02efb5f 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, accentDisabledButtonColor: .white, accentForegroundColor: .black) private var galleryNode: GalleryControllerNode { return self.displayNode as! GalleryControllerNode diff --git a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift index 676cce6782..79ccb825de 100644 --- a/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift +++ b/submodules/WebSearchUI/Sources/WebSearchVideoGalleryItem.swift @@ -122,7 +122,7 @@ final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode { switch gesture { case .tap: if let item = self.item, let selectionState = item.controllerInteraction?.selectionState { - let legacyItem = legacyWebSearchItem(account: item.context.account, result: item.result) + let legacyItem = legacyWebSearchItem(engine: item.context.engine, result: item.result) selectionState.toggleItemSelection(legacyItem, success: nil) } case .doubleTap: @@ -176,10 +176,7 @@ final class WebSearchVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.requiresDownload = true var mediaFileStatus: Signal = .single(nil) if let mediaResource = mediaResource { - mediaFileStatus = item.context.account.postbox.mediaBox.resourceStatus(mediaResource._asResource()) - |> map { status in - return EngineMediaResource.FetchStatus(status) - } + mediaFileStatus = item.context.engine.resources.status(resource: mediaResource) |> map(Optional.init) } diff --git a/submodules/WebUI/Sources/WebAppAddToAttachmentController.swift b/submodules/WebUI/Sources/WebAppAddToAttachmentController.swift index 5c96fd4112..ea6b1d2060 100644 --- a/submodules/WebUI/Sources/WebAppAddToAttachmentController.swift +++ b/submodules/WebUI/Sources/WebAppAddToAttachmentController.swift @@ -3,7 +3,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 7e0ce945ea..64616b7429 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 @@ -345,7 +344,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return .single(nil) } |> mapToSignal { bot -> Signal<(FileMediaReference, Bool)?, NoError> in - if let bot = bot, let peerReference = PeerReference(bot.peer._asPeer()) { + if let bot = bot, let peerReference = PeerReference(bot.peer) { var imageFile: TelegramMediaFile? var isPlaceholder = false if let file = bot.icons[.placeholder] { @@ -517,6 +516,12 @@ public final class WebAppController: ViewController, AttachmentContainable { return } #endif*/ + + if !"".isEmpty { + self.webView?.bindTrustedOrigin(from: url) + } else { + self.webView?.setupEventProxySource() + } self.webView?.load(URLRequest(url: url)) } @@ -1093,7 +1098,7 @@ public final class WebAppController: ViewController, AttachmentContainable { guard let controller = self.controller else { return } - guard message.frameInfo.isMainFrame else { + guard self.webView?.isTrustedMainFrameMessage(message) == true else { return } guard let body = message.body as? [String: Any] else { @@ -1601,7 +1606,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.controller?._isPanGestureEnabled = isPanGestureEnabled } case "web_app_share_to_story": - if let json, let mediaUrl = json["media_url"] as? String { + if let json, let mediaUrl = json["media_url"] as? String, isAllowedBotMediaUrl(mediaUrl) { let text = json["text"] as? String let link = json["widget_link"] as? [String: Any] @@ -3373,7 +3378,7 @@ public final class WebAppController: ViewController, AttachmentContainable { guard let self, let controller = self.controller, let peer else { return } - if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { controller.parentController()?.push(infoController) } }) @@ -3800,7 +3805,10 @@ 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, + accentDisabledButtonColor: self.presentationData.theme.chat.inputPanel.panelControlDisabledColor, + accentForegroundColor: self.presentationData.theme.list.itemCheckColors.foregroundColor ), strings: NavigationBarStrings(back: "", close: "") ) @@ -3938,11 +3946,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)) }))) } @@ -4307,3 +4314,116 @@ private struct WebAppConfiguration { } } } + +private func isAllowedBotMediaUrl(_ urlString: String) -> Bool { + guard let escaped = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: escaped) else { + return false + } + guard url.scheme?.lowercased() == "https" else { + return false + } + if url.user != nil || url.password != nil { + return false + } + guard var host = url.host?.lowercased(), !host.isEmpty else { + return false + } + if host.hasPrefix("[") && host.hasSuffix("]") { + host = String(host.dropFirst().dropLast()) + } + + // Strict canonical dotted-decimal IPv4 (4 octets, no leading zeros, each 0-255). + // Do NOT use inet_pton here: Darwin's inet_pton accepts "0177.0.0.1" as + // decimal 177.0.0.1, but getaddrinfo (used by URLSession) interprets the + // same string as octal 127.0.0.1 — the divergence is a loopback bypass. + if let v4Bytes = parseCanonicalIPv4(host) { + return isPublicIPv4(v4Bytes) + } + + // IPv6 only — host must contain ":" so we don't accidentally hand a + // numeric-looking hostname to inet_pton. + if host.contains(":") { + var v6 = in6_addr() + if host.withCString({ inet_pton(AF_INET6, $0, &v6) }) == 1 { + let bytes = withUnsafeBytes(of: &v6) { ptr -> [UInt8] in + return Array(ptr) + } + return isPublicIPv6(bytes) + } + return false + } + + // Strict DNS-name validation. Anything that doesn't look like a real + // FQDN is rejected — this catches non-canonical numeric IP forms + // (decimal-32 like "2130706433", octal like "0177.0.0.1", hex like + // "0x7f.0.0.1", short forms like "127.1") that the OS resolver may + // still treat as 127.0.0.1 even when inet_pton would accept them as + // a different value or reject outright. + let labels = host.split(separator: ".", omittingEmptySubsequences: false) + guard labels.count >= 2 else { return false } + for label in labels { + guard !label.isEmpty, label.count <= 63 else { return false } + if label.first == "-" || label.last == "-" { return false } + for ch in label { + guard ch.isASCII else { return false } + if !(ch.isLetter || ch.isNumber || ch == "-") { return false } + } + } + guard let tld = labels.last, tld.count >= 2, tld.contains(where: { $0.isLetter }) else { + return false + } + + if host == "localhost" || host.hasSuffix(".localhost") || host.hasSuffix(".local") { + return false + } + return true +} + +private func parseCanonicalIPv4(_ host: String) -> [UInt8]? { + let parts = host.split(separator: ".", omittingEmptySubsequences: false) + guard parts.count == 4 else { return nil } + var bytes: [UInt8] = [] + bytes.reserveCapacity(4) + for part in parts { + guard !part.isEmpty, part.count <= 3 else { return nil } + if part.count > 1 && part.first == "0" { return nil } // no leading zeros (octal-spoof) + guard part.allSatisfy({ $0.isASCII && $0.isNumber }) else { return nil } + guard let value = UInt8(part) else { return nil } // also caps at 255 + bytes.append(value) + } + return bytes +} + +private func isPublicIPv4(_ bytes: [UInt8]) -> Bool { + guard bytes.count == 4 else { return false } + let a = bytes[0] + let b = bytes[1] + if a == 0 { return false } // 0.0.0.0/8 + if a == 10 { return false } // 10.0.0.0/8 + if a == 127 { return false } // 127.0.0.0/8 loopback + if a == 169 && b == 254 { return false } // 169.254.0.0/16 link-local + if a == 172 && (b & 0xf0) == 16 { return false } // 172.16.0.0/12 + if a == 192 && b == 168 { return false } // 192.168.0.0/16 + if a == 100 && (b & 0xc0) == 64 { return false } // 100.64.0.0/10 CGNAT + if a >= 224 { return false } // multicast + reserved + 255.255.255.255 + return true +} + +private func isPublicIPv6(_ bytes: [UInt8]) -> Bool { + guard bytes.count == 16 else { return false } + if bytes.allSatisfy({ $0 == 0 }) { return false } // :: + let loopback: [UInt8] = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1] + if bytes == loopback { return false } // ::1 + if bytes[0] == 0xff { return false } // ff00::/8 multicast + if bytes[0] == 0xfe && (bytes[1] & 0xc0) == 0x80 { return false } // fe80::/10 link-local + if (bytes[0] & 0xfe) == 0xfc { return false } // fc00::/7 unique-local + let v4MappedPrefix: [UInt8] = [0,0,0,0,0,0,0,0,0,0,0xff,0xff] + if Array(bytes.prefix(12)) == v4MappedPrefix { // ::ffff:a.b.c.d + return isPublicIPv4(Array(bytes.suffix(4))) + } + if Array(bytes.prefix(12)).allSatisfy({ $0 == 0 }) { // ::a.b.c.d (deprecated) + return isPublicIPv4(Array(bytes.suffix(4))) + } + return true +} diff --git a/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift b/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift index ea8dc4ed73..6d64d0f270 100644 --- a/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift +++ b/submodules/WebUI/Sources/WebAppEmojiStatusAlertController.swift @@ -3,7 +3,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import ComponentFlow import TelegramCore import TelegramPresentationData diff --git a/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift b/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift index 2fa118db47..5233814842 100644 --- a/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift +++ b/submodules/WebUI/Sources/WebAppLaunchConfirmationController.swift @@ -4,7 +4,6 @@ import SwiftSignalKit import AsyncDisplayKit import Display import ComponentFlow -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/WebUI/Sources/WebAppLocationAlertController.swift b/submodules/WebUI/Sources/WebAppLocationAlertController.swift index 1fe62d6073..e1be2cf404 100644 --- a/submodules/WebUI/Sources/WebAppLocationAlertController.swift +++ b/submodules/WebUI/Sources/WebAppLocationAlertController.swift @@ -4,7 +4,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift index 5af46a886f..5d960c53b3 100644 --- a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift +++ b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift @@ -168,8 +168,10 @@ private final class SheetContent: CombinedComponent { case let .invoice(invoice, replyMarkupValue): media = [invoice] replyMarkup = replyMarkupValue - default: - break + case let .webpage(textValue, entitiesValue, _, _, replyMarkupValue): + text = textValue + entities = entitiesValue + replyMarkup = replyMarkupValue } case let .externalReference(reference): switch reference.message { @@ -195,8 +197,10 @@ private final class SheetContent: CombinedComponent { case let .invoice(invoice, replyMarkupValue): media = [invoice] replyMarkup = replyMarkupValue - default: - break + case let .webpage(textValue, entitiesValue, _, _, replyMarkupValue): + text = textValue + entities = entitiesValue + replyMarkup = replyMarkupValue } } diff --git a/submodules/WebUI/Sources/WebAppTermsAlertController.swift b/submodules/WebUI/Sources/WebAppTermsAlertController.swift index ba305d3bb7..92ccaac0c1 100644 --- a/submodules/WebUI/Sources/WebAppTermsAlertController.swift +++ b/submodules/WebUI/Sources/WebAppTermsAlertController.swift @@ -3,7 +3,6 @@ import UIKit import SwiftSignalKit import AsyncDisplayKit import Display -import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index 436d8c36db..acebade7a2 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -36,11 +36,39 @@ private class WebViewTouchGestureRecognizer: UITapGestureRecognizer { } } -private let eventProxySource = "var TelegramWebviewProxyProto = function() {}; " + - "TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " + - "window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " + - "}; " + -"var TelegramWebviewProxy = new TelegramWebviewProxyProto();" +private func jsStringLiteral(_ value: String) -> String { + if let data = try? JSONSerialization.data(withJSONObject: [value], options: []), let string = String(data: data, encoding: .utf8), string.hasPrefix("["), string.hasSuffix("]") { + return String(string.dropFirst().dropLast()) + } + return "\"\"" +} + +private func eventProxySource() -> String { + return """ + (function() { + var TelegramWebviewProxyProto = function() {}; + TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { + window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); + }; + window.TelegramWebviewProxy = new TelegramWebviewProxyProto(); + })(); + """ +} + +private func securedEventProxySource(trustedOrigin: String) -> String { + return """ + (function() { + if (window.location.origin !== \(jsStringLiteral(trustedOrigin))) { + return; + } + var TelegramWebviewProxyProto = function() {}; + TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { + window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); + }; + window.TelegramWebviewProxy = new TelegramWebviewProxyProto(); + })(); + """ +} private let selectionSource = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea):not([\"contenteditable\"=\"true\"]){-webkit-user-select:none;}';" + " var head = document.head || document.getElementsByTagName('head')[0];" @@ -91,6 +119,7 @@ function tgBrowserDisconnectObserver() { final class WebAppWebView: WKWebView { var handleScriptMessage: (WKScriptMessage) -> Void = { _ in } + private(set) var trustedOrigin: String? var customInsets: UIEdgeInsets = .zero { didSet { @@ -134,8 +163,6 @@ final class WebAppWebView: WKWebView { let contentController = WKUserContentController() var handleScriptMessageImpl: ((WKScriptMessage) -> Void)? - let eventProxyScript = WKUserScript(source: eventProxySource, injectionTime: .atDocumentStart, forMainFrameOnly: true) - contentController.addUserScript(eventProxyScript) contentController.add(WeakGameScriptMessageHandler { message in handleScriptMessageImpl?(message) }, name: "performAction") @@ -187,6 +214,47 @@ final class WebAppWebView: WKWebView { print() } + var useSecuredEventProxy = true + func bindTrustedOrigin(from url: URL) { + guard self.trustedOrigin == nil else { + return + } + guard let origin = normalizedOrigin(url: url) else { + return + } + + self.trustedOrigin = origin + + let eventProxyScript = WKUserScript(source: securedEventProxySource(trustedOrigin: origin), injectionTime: .atDocumentStart, forMainFrameOnly: true) + self.configuration.userContentController.addUserScript(eventProxyScript) + } + + func setupEventProxySource() { + self.useSecuredEventProxy = false + + let eventProxyScript = WKUserScript(source: eventProxySource(), injectionTime: .atDocumentStart, forMainFrameOnly: true) + self.configuration.userContentController.addUserScript(eventProxyScript) + } + + func isTrustedMainFrameMessage(_ message: WKScriptMessage) -> Bool { + guard message.frameInfo.isMainFrame else { + return false + } + if !self.useSecuredEventProxy { + return true + } + guard let trustedOrigin = self.trustedOrigin else { + return false + } + guard message.frameInfo.securityOriginString == trustedOrigin else { + return false + } + if let currentOrigin = self.origin, currentOrigin != trustedOrigin { + return false + } + return true + } + override func didMoveToSuperview() { super.didMoveToSuperview() @@ -223,6 +291,11 @@ final class WebAppWebView: WKWebView { } func sendEvent(name: String, data: String?) { + if self.useSecuredEventProxy { + guard let trustedOrigin = self.trustedOrigin, self.origin == trustedOrigin else { + return + } + } let script = "window.TelegramGameProxy && window.TelegramGameProxy.receiveEvent && window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data ?? "null"))" self.evaluateJavaScript(script, completionHandler: { _, _ in }) @@ -277,4 +350,41 @@ final class WebAppWebView: WKWebView { override var inputAccessoryView: UIView? { return nil } + + var origin: String? { + guard let url = self.url else { + return nil + } + return normalizedOrigin(url: url) + } +} + +extension WKFrameInfo { + var securityOriginString: String { + let securityOrigin = self.securityOrigin + return normalizedOrigin(scheme: securityOrigin.protocol, host: securityOrigin.host, port: securityOrigin.port == 0 ? nil : securityOrigin.port) ?? "" + } +} + +private func normalizedOrigin(url: URL) -> String? { + return normalizedOrigin(scheme: url.scheme, host: url.host, port: url.port) +} + +private func normalizedOrigin(scheme: String?, host: String?, port: Int?) -> String? { + guard let scheme = scheme?.lowercased(), !scheme.isEmpty, let host = host?.lowercased(), !host.isEmpty else { + return nil + } + + let includePort: Bool + if let port { + includePort = !(scheme == "http" && port == 80) && !(scheme == "https" && port == 443) + } else { + includePort = false + } + + if includePort, let port { + return "\(scheme)://\(host):\(port)" + } else { + return "\(scheme)://\(host)" + } } diff --git a/third-party/Swift2D/BUILD b/third-party/Swift2D/BUILD new file mode 100644 index 0000000000..cf88b4459a --- /dev/null +++ b/third-party/Swift2D/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "Swift2D", + module_name = "Swift2D", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-suppress-warnings", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/third-party/Swift2D/Sources/Point+CoreGraphics.swift b/third-party/Swift2D/Sources/Point+CoreGraphics.swift new file mode 100644 index 0000000000..2d85614228 --- /dev/null +++ b/third-party/Swift2D/Sources/Point+CoreGraphics.swift @@ -0,0 +1,19 @@ +#if canImport(CoreGraphics) +import CoreGraphics +#else +import Foundation +#endif + +public extension Point { + /// Initialize a `Point` using the provided `CGPoint` values. + init(_ point: CGPoint) { + self.init(x: point.x, y: point.y) + } +} + +public extension CGPoint { + /// Initialize a `CPPoint` using the provided `Point` values. + init(_ point: Point) { + self.init(x: point.x, y: point.y) + } +} diff --git a/third-party/Swift2D/Sources/Point.swift b/third-party/Swift2D/Sources/Point.swift new file mode 100644 index 0000000000..72170ad657 --- /dev/null +++ b/third-party/Swift2D/Sources/Point.swift @@ -0,0 +1,82 @@ +/// The representation of a single point in a two-dimensional plane. +public struct Point: Hashable, Codable, Sendable, CustomStringConvertible { + + public static let zero: Point = Point(x: 0, y: 0) + public static let nan: Point = Point(x: Double.nan, y: Double.nan) + public static let infinite: Point = Point(x: -Double.greatestFiniteMagnitude / 2, y: -Double.greatestFiniteMagnitude / 2) + public static let null: Point = Point(x: Double.infinity, y: Double.infinity) + + public let x: Double + public let y: Double + + public init(x: Double = 0.0, y: Double = 0.0) { + self.x = x + self.y = y + } + + public init(x: Float, y: Float) { + self.x = Double(x) + self.y = Double(y) + } + + public init(x: Int, y: Int) { + self.x = Double(x) + self.y = Double(y) + } + + public var description: String { "Point(x: \(x), y: \(y))" } + public var isZero: Bool { self == .zero } + public var isNaN: Bool { self == .nan } + public var isInfinite: Bool { self == .infinite } + public var isNull: Bool { self == .null } + + /// Create a new instance maintaining the `y` value while using the provided `x` value. + public func x(_ value: Double) -> Point { + Point(x: value, y: y) + } + + /// Create a new instance maintaining the `x` value while using the provided `y` value. + public func y(_ value: Double) -> Point { + Point(x: x, y: value) + } + + /// The _mirror_ of the instance, rotated 180° around the provided `point` + /// in a two-dimensional plane. + /// + /// - parameters: + /// - point: The anchor to use in calculating the reflection. + public func reflecting(around point: Point) -> Point { + let x = (2 * point.x) - x + let y = (2 * point.y) - y + return Point(x: x, y: y) + } + + public static func == (lhs: Point, rhs: Point) -> Bool { + if lhs.x.isNaN, rhs.x.isNaN, lhs.y.isNaN, rhs.y.isNaN { + return true + } + + if lhs.x.isInfinite, rhs.x.isInfinite, lhs.y.isInfinite, rhs.y.isInfinite { + return true + } + + return lhs.x == rhs.x && lhs.y == rhs.y + } +} + +public extension Point { + @available(*, deprecated, renamed: "x(_:)") + func with(x value: Double) -> Point { + x(value) + } + + @available(*, deprecated, renamed: "y(_:)") + func with(y value: Double) -> Point { + y(value) + } + + @available(*, deprecated, renamed: "reflecting(around:)") + func reflection(using point: Point) -> Point { + reflecting(around: point) + } +} diff --git a/third-party/Swift2D/Sources/Rect+CoreGraphics.swift b/third-party/Swift2D/Sources/Rect+CoreGraphics.swift new file mode 100644 index 0000000000..45eb402e3b --- /dev/null +++ b/third-party/Swift2D/Sources/Rect+CoreGraphics.swift @@ -0,0 +1,32 @@ +#if canImport(CoreGraphics) +import CoreGraphics +#else +import Foundation +#endif + +public extension Rect { + /// Initialize a `Rect` using the provided `CGRect` values. + init(_ rect: CGRect) { + self.init( + origin: Point(rect.origin), + size: Size(rect.size) + ) + } + + @available(*, deprecated, renamed: "CGRect(_:)", message: "Use CGRect initializer directly") + var cgRect: CGRect { + CGRect(self) + } +} + +public extension CGRect { + /// Initialize a `CGRect` using the provided `Rect` values. + init(_ rect: Rect) { + self.init( + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height + ) + } +} diff --git a/third-party/Swift2D/Sources/Rect.swift b/third-party/Swift2D/Sources/Rect.swift new file mode 100644 index 0000000000..1fd40a2701 --- /dev/null +++ b/third-party/Swift2D/Sources/Rect.swift @@ -0,0 +1,251 @@ +/// The location and dimensions of a rectangle. +public struct Rect: Hashable, Codable, Sendable, CustomStringConvertible { + + public static let zero: Rect = Rect(origin: .zero, size: .zero) + public static let nan: Rect = Rect(origin: .nan, size: .nan) + public static let infinite: Rect = Rect(origin: .infinite, size: .infinite) + public static let null: Rect = Rect(origin: .null, size: .zero) + + /// A point that specifies the coordinates of the rectangle’s origin. + public let origin: Point + /// A size that specifies the height and width of the rectangle. + public let size: Size + + public init(origin: Point = .zero, size: Size = .zero) { + self.origin = origin + self.size = size + } + + public init(x: Double, y: Double, width: Double, height: Double) { + origin = Point(x: x, y: y) + size = Size(width: width, height: height) + } + + public init(x: Float, y: Float, width: Float, height: Float) { + origin = Point(x: x, y: y) + size = Size(width: width, height: height) + } + + public init(x: Int, y: Int, width: Int, height: Int) { + origin = Point(x: x, y: y) + size = Size(width: width, height: height) + } + + public var description: String { "Rect(origin: \(origin), size: \(size))" } + public var isZero: Bool { self == .zero } + public var isNaN: Bool { self == .nan } + public var isInfinite: Bool { self == .infinite } + public var isNull: Bool { origin == .infinite || origin == .null } + public var isEmpty: Bool { isNull || width == 0.0 || height == 0.0 } + + /// The x-coordinate of the rectangle origin + public var x: Double { origin.x } + + /// The y-coordinate of the rectangle origin + public var y: Double { origin.y } + + /// The width of the rectangle. + public var width: Double { size.width } + + /// The height of the rectangle. + public var height: Double { size.height } + + /// The middle of the `Rect` both horizontally and vertically. + public var center: Point { Point(x: midX, y: midY) } + + /// The smallest value for the x-coordinate of the rectangle. + public var minX: Double { + if size.width < 0.0 { + return origin.x - abs(size.width) + } + + return origin.x + } + + /// The x-coordinate that establishes the center of a rectangle. + public var midX: Double { minX + size.widthRadius } + + /// The largest value of the x-coordinate for the rectangle. + public var maxX: Double { + if size.width < 0.0 { + return origin.x + } + + return origin.x + size.width + } + + /// The smallest value for the y-coordinate of the rectangle. + public var minY: Double { + if size.height < 0.0 { + return origin.y - abs(size.height) + } + + return origin.y + } + + /// The y-coordinate that establishes the center of the rectangle. + public var midY: Double { minY + size.heightRadius } + + /// The largest value for the y-coordinate of the rectangle. + public var maxY: Double { + if size.height < 0.0 { + return origin.y + } + + return origin.y + size.height + } + + /// Returns a rectangle with a positive width and height. + public var standardized: Rect { + guard !isNull else { + return .null + } + + return Rect(x: minX, y: minY, width: abs(width), height: abs(height)) + } + + public func origin(_ value: Point) -> Rect { + Rect(origin: value, size: size) + } + + public func size(_ value: Size) -> Rect { + Rect(origin: origin, size: value) + } + + public func contains(_ point: Point) -> Bool { + guard !isNull else { + return false + } + + guard !isEmpty else { + return false + } + + return (minX ..< maxX).contains(point.x) && (minY ..< maxY).contains(point.y) + } + + public func contains(_ rect: Rect) -> Bool { + union(rect) == self + } + + public func intersects(_ rect: Rect) -> Bool { + !intersection(rect).isNull + } + + public func intersection(_ rect: Rect) -> Rect { + guard !isNull else { + return .null + } + + guard !rect.isNull else { + return .null + } + + let r1 = standardized + let r1spanH = r1.minX ... r1.maxX + let r1spanV = r1.minY ... r1.maxY + + let r2 = rect.standardized + let r2spanH = r2.minX ... r2.maxX + let r2spanV = r2.minY ... r2.maxY + + guard r1spanH.overlaps(r2spanH), r2spanV.overlaps(r2spanV) else { + return .null + } + + let overlapH = r1spanH.clamped(to: r2spanH) + let width: Double = if overlapH == r1spanH { + r1.width + } else if overlapH == r2spanH { + r2.width + } else { + overlapH.upperBound - overlapH.lowerBound + } + + let overlapV = r1spanV.clamped(to: r2spanV) + let height: Double = if overlapV == r1spanV { + r1.height + } else if overlapV == r2spanV { + r2.height + } else { + overlapV.upperBound - overlapV.lowerBound + } + + return Rect(x: overlapH.lowerBound, y: overlapV.lowerBound, width: width, height: height) + } + + public func union(_ rect: Rect) -> Rect { + guard !isNull else { + return rect + } + + guard !rect.isNull else { + return self + } + + let r1 = standardized + let r2 = rect.standardized + + let minX = min(r1.minX, r2.minX) + let maxX = max(r1.maxX, r2.maxX) + let minY = min(r1.minY, r2.minY) + let maxY = max(r1.maxY, r2.maxY) + + return Rect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) + } + + public func offsetBy(dx: Double, dy: Double) -> Rect { + guard !isNull else { + return self + } + + let rect = standardized + return Rect( + origin: Point( + x: rect.origin.x + dx, + y: rect.origin.y + dy + ), + size: rect.size + ) + } + + public func insetBy(dx: Double, dy: Double) -> Rect { + guard !isNull else { + return self + } + + let normalized = standardized + let rect = Rect( + origin: Point( + x: normalized.x + dx, + y: normalized.y + dy + ), + size: Size( + width: normalized.width - (dx * 2), + height: normalized.height - (dy * 2) + ) + ) + + guard rect.size.width >= 0, rect.size.height >= 0 else { + return .null + } + + return rect + } + + public func expandedBy(dx: Double, dy: Double) -> Rect { + insetBy(dx: -dx, dy: -dy) + } +} + +public extension Rect { + @available(*, deprecated, renamed: "origin(_:)") + func with(origin value: Point) -> Rect { + Rect(origin: value, size: size) + } + + @available(*, deprecated, renamed: "size(_:)") + func with(size value: Size) -> Rect { + Rect(origin: origin, size: value) + } +} diff --git a/third-party/Swift2D/Sources/Size+CoreGraphics.swift b/third-party/Swift2D/Sources/Size+CoreGraphics.swift new file mode 100644 index 0000000000..014ba8ace4 --- /dev/null +++ b/third-party/Swift2D/Sources/Size+CoreGraphics.swift @@ -0,0 +1,19 @@ +#if canImport(CoreGraphics) +import CoreGraphics +#else +import Foundation +#endif + +public extension Size { + /// Initialize a `Size` using the provided `CGSize` values. + init(_ size: CGSize) { + self.init(width: size.width, height: size.height) + } +} + +public extension CGSize { + /// Initialize a `CGSize` using the provided `Size` values. + init(_ size: Size) { + self.init(width: size.width, height: size.height) + } +} diff --git a/third-party/Swift2D/Sources/Size.swift b/third-party/Swift2D/Sources/Size.swift new file mode 100644 index 0000000000..202cfa5f36 --- /dev/null +++ b/third-party/Swift2D/Sources/Size.swift @@ -0,0 +1,87 @@ +/// A representation of two-dimensional width and height values. +public struct Size: Hashable, Codable, Sendable, CustomStringConvertible { + + public static let zero: Size = Size(width: 0.0, height: 0.0) + public static let nan: Size = Size(width: Double.nan, height: Double.nan) + public static let infinite: Size = Size(width: Double.greatestFiniteMagnitude, height: Double.greatestFiniteMagnitude) + + public let width: Double + public let height: Double + + public init(width: Double = 0.0, height: Double = 0.0) { + self.width = width + self.height = height + } + + public init(width: Float, height: Float) { + self.width = Double(width) + self.height = Double(height) + } + + public init(width: Int, height: Int) { + self.width = Double(width) + self.height = Double(height) + } + + public var description: String { "Size(width: \(width), height: \(height))" } + public var isZero: Bool { self == .zero } + public var isNaN: Bool { self == .nan } + public var isInfinite: Bool { self == .infinite } + + /// The radius defined by the `width` dimension. + public var widthRadius: Double { abs(width) / 2.0 } + + /// The radius defined by the `height` dimension. + public var heightRadius: Double { abs(height) / 2.0 } + + /// The largest of the width and height radii. + public var maxRadius: Double { max(widthRadius, heightRadius) } + + /// The smallest of the width and height radii. + public var minRadius: Double { min(widthRadius, heightRadius) } + + /// The `Point` at which the `widthRadius` & `heightRadius` intersect. + public var center: Point { Point(x: widthRadius, y: heightRadius) } + + /// Create a new instance maintaining the `height` value while using the provided `width` value. + public func width(_ value: Double) -> Size { + Size(width: value, height: height) + } + + /// Create a new instance maintaining the `width` value while using the provided `height` value. + public func height(_ value: Double) -> Size { + Size(width: width, height: value) + } + + public static func == (lhs: Size, rhs: Size) -> Bool { + if lhs.width.isNaN, rhs.width.isNaN, lhs.height.isNaN, rhs.height.isNaN { + return true + } + + return lhs.width == rhs.width && lhs.height == rhs.height + } +} + +public extension Size { + @available(*, deprecated, renamed: "widthRadius") + var xRadius: Double { abs(width) / 2.0 } + + @available(*, deprecated, renamed: "heightRadius") + var yRadius: Double { abs(height) / 2.0 } + + @available(*, deprecated, renamed: "widthRadius") + var horizontalRadius: Double { xRadius } + + @available(*, deprecated, renamed: "heightRadius") + var verticalRadius: Double { yRadius } + + @available(*, deprecated, renamed: "width(_:)") + func with(width value: Double) -> Size { + width(value) + } + + @available(*, deprecated, renamed: "height(_:)") + func with(height value: Double) -> Size { + height(value) + } +} diff --git a/third-party/SwiftColor/BUILD b/third-party/SwiftColor/BUILD new file mode 100644 index 0000000000..a45048f861 --- /dev/null +++ b/third-party/SwiftColor/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SwiftColor", + module_name = "SwiftColor", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-suppress-warnings", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/third-party/SwiftColor/Sources/Clamping.swift b/third-party/SwiftColor/Sources/Clamping.swift new file mode 100644 index 0000000000..4154946f98 --- /dev/null +++ b/third-party/SwiftColor/Sources/Clamping.swift @@ -0,0 +1,20 @@ +@propertyWrapper +public struct Clamping { + var value: Value + let range: ClosedRange + + public init(wrappedValue value: Value, _ range: ClosedRange) { + precondition(range.contains(value)) + self.value = value + self.range = range + } + + public var wrappedValue: Value { + get { + value + } + set(newValue) { + value = min(max(range.lowerBound, newValue), range.upperBound) + } + } +} diff --git a/third-party/SwiftColor/Sources/ColorSpace.swift b/third-party/SwiftColor/Sources/ColorSpace.swift new file mode 100644 index 0000000000..fef838c450 --- /dev/null +++ b/third-party/SwiftColor/Sources/ColorSpace.swift @@ -0,0 +1,3 @@ +public enum ColorSpace: Equatable, Sendable { + case rgba +} diff --git a/third-party/SwiftColor/Sources/Pigment+AppKit.swift b/third-party/SwiftColor/Sources/Pigment+AppKit.swift new file mode 100644 index 0000000000..38c875d965 --- /dev/null +++ b/third-party/SwiftColor/Sources/Pigment+AppKit.swift @@ -0,0 +1,24 @@ +import Foundation +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit + +public extension Pigment { + /// Initialize a `Pigment` using an `NSColor`. + init(_ color: NSColor) { + red = color.redComponent + green = color.greenComponent + blue = color.blueComponent + alpha = color.alphaComponent + } + + var nsColor: NSColor { + NSColor(red: red, green: green, blue: blue, alpha: alpha) + } +} + +public extension NSColor { + var pigment: Pigment { + Pigment(self) + } +} +#endif diff --git a/third-party/SwiftColor/Sources/Pigment+CoreGraphics.swift b/third-party/SwiftColor/Sources/Pigment+CoreGraphics.swift new file mode 100644 index 0000000000..29eaba236e --- /dev/null +++ b/third-party/SwiftColor/Sources/Pigment+CoreGraphics.swift @@ -0,0 +1,39 @@ +import Foundation +#if canImport(CoreGraphics) +import CoreGraphics + +public extension Pigment { + /// Initialize a `Pigment` using an `CGColor`. + init?(_ color: CGColor) { + let components = color.components ?? [] + switch components.count { + case 2: + // Monochrome/Grayscale + // TODO: Express this in 'Color'. + red = 0.0 + green = 0.0 + blue = 0.0 + alpha = components[1] + case 4: + // RGB + red = components[0] + green = components[1] + blue = components[2] + alpha = components[3] + default: + return nil + } + } +} + +public extension CGColor { + static func make(_ color: Pigment) -> CGColor { + if color.red == 0.0, color.green == 0.0, color.blue == 0.0, color.alpha == 0.0 { + // Return the CG color equivalent of `UIColor.clear`. + return CGColor(genericGrayGamma2_2Gray: 0.0, alpha: 0.0) + } + + return CGColor(srgbRed: color.red, green: color.green, blue: color.blue, alpha: color.alpha) + } +} +#endif diff --git a/third-party/SwiftColor/Sources/Pigment+Float.swift b/third-party/SwiftColor/Sources/Pigment+Float.swift new file mode 100644 index 0000000000..fbcaf7001a --- /dev/null +++ b/third-party/SwiftColor/Sources/Pigment+Float.swift @@ -0,0 +1,95 @@ +import Foundation + +public extension Pigment { + /// Initialize a `Pigment` using `Float` values leaning towards the 'Red' spectrum. + /// + /// - parameters + /// - red: A value in the range of 0.0 to 1.0 representing the **red** percent. + /// - green: A value in the range of 0.0 to 1.0 representing the **green** percent. + /// - blue: A value in the range of 0.0 to 1.0 representing the **blue** percent. + /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent. + init( + red: Float, + green: Float = 0.0, + blue: Float = 0.0, + alpha: Float = 1.0 + ) { + self.red = Double(red) + self.green = Double(green) + self.blue = Double(blue) + self.alpha = Double(alpha) + } + + /// Initialize a `Pigment` using `Float` values leaning towards the 'Green' spectrum. + /// + /// - parameters: + /// - green: A value in the range of 0.0 to 1.0 representing the **green** percent. + /// - blue: A value in the range of 0.0 to 1.0 representing the **blue** percent. + /// - red: A value in the range of 0.0 to 1.0 representing the **red** percent. + /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent. + init( + green: Float, + blue: Float = 0.0, + red: Float = 0.0, + alpha: Float = 1.0 + ) { + self.red = Double(red) + self.green = Double(green) + self.blue = Double(blue) + self.alpha = Double(alpha) + } + + /// Initialize a `Pigment` using `Float` values leaning towards the 'Blue' spectrum. + /// + /// - parameter: + /// - blue: A value in the range of 0.0 to 1.0 representing the **blue** percent. + /// - red: A value in the range of 0.0 to 1.0 representing the **red** percent. + /// - green: A value in the range of 0.0 to 1.0 representing the **green** percent. + /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent. + init( + blue: Float, + red: Float = 0.0, + green: Float = 0.0, + alpha: Float = 1.0 + ) { + self.red = Double(red) + self.green = Double(green) + self.blue = Double(blue) + self.alpha = Double(alpha) + } + + /// Initialize a `Pigment` using variadic `Float` values. + /// + /// All _values_ should be expressed in the range of 0.0 to 1.0. + /// + /// - parameters: + /// - values: A number of `Float` which are mapped to **red**, **green**, **blue** in that order. + /// - alpha: Amount of _opacity/transparency_ to apply. + init( + _ values: Float..., + alpha: Float + ) { + if values.count > 0 { + red = Double(values[0].clamped(to: 0 ... 1)) + } else { + red = 0.0 + } + if values.count > 1 { + green = Double(values[1].clamped(to: 0 ... 1)) + } else { + green = 0.0 + } + if values.count > 2 { + blue = Double(values[2].clamped(to: 0 ... 1)) + } else { + blue = 0.0 + } + self.alpha = Double(alpha.clamped(to: 0 ... 1)) + } +} + +extension Float { + func clamped(to range: ClosedRange) -> Float { + min(max(range.lowerBound, self), range.upperBound) + } +} diff --git a/third-party/SwiftColor/Sources/Pigment+Hex.swift b/third-party/SwiftColor/Sources/Pigment+Hex.swift new file mode 100644 index 0000000000..c969b9538c --- /dev/null +++ b/third-party/SwiftColor/Sources/Pigment+Hex.swift @@ -0,0 +1,145 @@ +import Foundation + +public extension Pigment { + /// Initializes a `Pigment` with an `Int` in the expected format of **0x000**. + /// + /// Used as a short-hand. If the hex 0x123 is provided, it is interpreted as 0x112233. + init( + hex3 hex: Int, + @Clamping(0 ... 1) alpha: Double = 1.0 + ) { + let values = Self.hex3(hex: hex, alpha: alpha) + red = values.red + green = values.green + blue = values.blue + self.alpha = values.alpha + } + + /// Shorthand **0x0000** initializer similar to `init(hex3:alpha:)` where + /// the last digit represent the alpha component. + init( + hex4 hex: Int + ) { + let values = Self.hex4(hex: hex) + red = values.red + green = values.green + blue = values.blue + alpha = values.alpha + } + + /// Initializes with a standard format hex representation of color in the form of **0x1E2C3D**. + init( + hex6 hex: Int, + @Clamping(0 ... 1) alpha: Double = 1.0 + ) { + let values = Self.hex6(hex: hex, alpha: alpha) + red = values.red + green = values.green + blue = values.blue + self.alpha = values.alpha + } + + #if !os(watchOS) + /// Extended form of `init(hex6:alpha:)` expecting **0x112233FF**, that uses the last + /// bits for the alpha component. + init( + hex8 hex: Int + ) { + let values = Self.hex8(hex: hex) + red = values.red + green = values.green + blue = values.blue + alpha = values.alpha + } + #endif + + /// Initializes a `Pigment` with an `Int` representation of an RGB(a) Hex Value + /// + /// This initializer will do its best to interpret the intentions of what is provided. + /// **YOUR RESULTS WILL VARY**, and it's best to use one of the `init(hex?:)` initializers. + /// + /// - Parameter hex: Hex value + /// - Parameter alpha: The opacity value of the color object + init( + _ hex: Int, + alpha: Double? = nil + ) { + #if !os(watchOS) + if hex > 0xFFFFFF { + let values = Self.hex8(hex: hex) + red = values.red + green = values.green + blue = values.blue + self.alpha = values.alpha + return + } + #endif + if hex > 0xFFFF { + let values = Self.hex6(hex: hex, alpha: alpha ?? 1.0) + red = values.red + green = values.green + blue = values.blue + self.alpha = values.alpha + } else if hex > 0xFFF { + let values = Self.hex4(hex: hex) + red = values.red + green = values.green + blue = values.blue + self.alpha = values.alpha + } else { + let values = Self.hex3(hex: hex, alpha: alpha ?? 1.0) + red = values.red + green = values.green + blue = values.blue + self.alpha = values.alpha + } + } +} + +extension Pigment { + static func hex3( + hex: Int, + @Clamping(0 ... 1) alpha: Double = 1.0 + ) -> (red: Double, green: Double, blue: Double, alpha: Double) { + let red = Double(duplicateBits((hex & 0xF00) >> 8)) / 255.0 + let green = Double(duplicateBits((hex & 0x0F0) >> 4)) / 255.0 + let blue = Double(duplicateBits((hex & 0x00F) >> 0)) / 255.0 + return (red, green, blue, alpha) + } + + static func hex4( + hex: Int + ) -> (red: Double, green: Double, blue: Double, alpha: Double) { + let red = Double(duplicateBits((hex & 0xF000) >> 12)) / 255.0 + let green = Double(duplicateBits((hex & 0x0F00) >> 8)) / 255.0 + let blue = Double(duplicateBits((hex & 0x00F0) >> 4)) / 255.0 + let alpha = Double(duplicateBits((hex & 0x000F) >> 0)) / 255.0 + return (red, green, blue, alpha) + } + + static func hex6( + hex: Int, + @Clamping(0 ... 1) alpha: Double = 1.0 + ) -> (red: Double, green: Double, blue: Double, alpha: Double) { + let red = Double((hex & 0xFF0000) >> 16) / 255.0 + let green = Double((hex & 0x00FF00) >> 8) / 255.0 + let blue = Double((hex & 0x0000FF) >> 0) / 255.0 + return (red, green, blue, alpha) + } + + #if !os(watchOS) + static func hex8( + hex: Int + ) -> (red: Double, green: Double, blue: Double, alpha: Double) { + let red = Double((hex & 0xFF00_0000) >> 24) / 255.0 + let green = Double((hex & 0x00FF_0000) >> 16) / 255.0 + let blue = Double((hex & 0x0000_FF00) >> 8) / 255.0 + let alpha = Double((hex & 0x0000_00FF) >> 0) / 255.0 + return (red, green, blue, alpha) + } + #endif + + static func duplicateBits(_ value: Int) -> Int { + (value << 4) + value + } +} diff --git a/third-party/SwiftColor/Sources/Pigment+Int.swift b/third-party/SwiftColor/Sources/Pigment+Int.swift new file mode 100644 index 0000000000..16568d0142 --- /dev/null +++ b/third-party/SwiftColor/Sources/Pigment+Int.swift @@ -0,0 +1,89 @@ +import Foundation + +public extension Pigment { + /// Initialize a `Pigment` using `Int` values leaning towards the 'Red' spectrum. + /// + /// - parameters: + /// - red: A value in the range of 0 to 255 representing the **red** bits + /// - green: A value in the range of 0 to 255 representing the **green** bits + /// - blue: A value in the range of 0 to 255 representing the **blue** bits + /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent. + init( + @Clamping(0 ... 255) red: Int, + @Clamping(0 ... 255) green: Int = 0, + @Clamping(0 ... 255) blue: Int = 0, + @Clamping(0.0 ... 1.0) alpha: Double = 1.0 + ) { + self.red = Double(red) / 255.0 + self.green = Double(green) / 255.0 + self.blue = Double(blue) / 255.0 + self.alpha = alpha + } + + /// Initialize a `Pigment` using `Int` values leaning towards the 'Green' spectrum. + /// + /// - parameters: + /// - green: A value in the range of 0 to 255 representing the **green** bits + /// - blue: A value in the range of 0 to 255 representing the **blue** bits + /// - red: A value in the range of 0 to 255 representing the **red** bits + /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent. + init( + @Clamping(0 ... 255) green: Int, + @Clamping(0 ... 255) blue: Int = 0, + @Clamping(0 ... 255) red: Int = 0, + @Clamping(0.0 ... 1.0) alpha: Double = 1.0 + ) { + self.red = Double(red) / 255.0 + self.green = Double(green) / 255.0 + self.blue = Double(blue) / 255.0 + self.alpha = alpha + } + + /// Initialize a `Pigment` using `Int` values leaning towards the 'Blue' spectrum. + /// + /// - parameters: + /// - blue: A value in the range of 0 to 255 representing the **blue** bits + /// - red: A value in the range of 0 to 255 representing the **red** bits + /// - green: A value in the range of 0 to 255 representing the **green** bits + /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent. + init( + @Clamping(0 ... 255) blue: Int, + @Clamping(0 ... 255) red: Int = 0, + @Clamping(0 ... 255) green: Int = 0, + @Clamping(0.0 ... 1.0) alpha: Double = 1.0 + ) { + self.red = Double(red) / 255.0 + self.green = Double(green) / 255.0 + self.blue = Double(blue) / 255.0 + self.alpha = alpha + } + + /// Initialize a `Pigment` using variadic `Int` values. + /// + /// All _values_ should be expressed in the range of 0 to 255. + /// + /// - parameters: + /// - values: A number of `Int` which are mapped to **red**, **green**, **blue** in that order. + /// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent. + init( + _ values: Int..., + @Clamping(0 ... 1) alpha: Double + ) { + if values.count > 0 { + red = Double(values[0]) / 255.0 + } else { + red = 0.0 + } + if values.count > 1 { + green = Double(values[1]) / 255.0 + } else { + green = 0.0 + } + if values.count > 2 { + blue = Double(values[2]) / 255.0 + } else { + blue = 0.0 + } + self.alpha = alpha + } +} diff --git a/third-party/SwiftColor/Sources/Pigment+Name.swift b/third-party/SwiftColor/Sources/Pigment+Name.swift new file mode 100644 index 0000000000..8857f8f450 --- /dev/null +++ b/third-party/SwiftColor/Sources/Pigment+Name.swift @@ -0,0 +1,470 @@ +import Foundation + +public extension Pigment { + @available(*, deprecated, renamed: "Name") + typealias Keyword = Name + + enum Name: String, CaseIterable { + case aliceBlue + case antiqueWhite + case aqua + case aquamarine + case azure + case beige + case bisque + case black + case blanchedAlmond + case blue + case blueViolet + case brown + case burlywood + case cadetBlue + case chartreuse + case chocolate + case coral + case cornflowerBlue + case cornsilk + case crimson + case cyan + case darkBlue + case darkCyan + case darkGoldenrod + case darkGray + case darkGreen + case darkGrey + case darkKhaki + case darkMagenta + case darkOliveGreen + case darkOrange + case darkOrchid + case darkRed + case darkSalmon + case darkSeagreen + case darkSlateBlue + case darkSlateGray + case darkSlateGrey + case darkTurquoise + case darkViolet + case deepPink + case deepSkyblue + case dimGray + case dimGrey + case dodgerBlue + case firebrick + case floralWhite + case forestGreen + case fuchsia + case gainsboro + case ghostWhite + case gold + case goldenrod + case gray + case green + case greenYellow + case grey + case honeydew + case hotPink + case indianRed + case indigo + case ivory + case khaki + case lavender + case lavenderBlush + case lawnGreen + case lemonChiffon + case lightBlue + case lightCoral + case lightCyan + case lightGoldenrodYellow + case lightGray + case lightGreen + case lightGrey + case lightPink + case lightSalmon + case lightSeagreen + case lightSkyBlue + case lightSlateGray + case lightSlateGrey + case lightSteelBlue + case lightYellow + case lime + case limeGreen + case linen + case magenta + case maroon + case mediumAquamarine + case mediumBlue + case mediumOrchid + case mediumPurple + case mediumSeagreen + case mediumSlateBlue + case mediumSpringGreen + case mediumTurquoise + case mediumVioletRed + case midnightBlue + case mintCream + case mistyRose + case moccasin + case navajoWhite + case navy + case oldLace + case olive + case oliveDrab + case orange + case orangeRed + case orchid + case paleGoldenrod + case paleGreen + case paleTurquoise + case paleVioletRed + case papayaWhip + case peachPuff + case peru + case pink + case plum + case powderBlue + case purple + case red + case rosyBrown + case royalBlue + case saddleBrown + case salmon + case sandyBrown + case seagreen + case seashell + case sienna + case silver + case skyBlue + case slateBlue + case slateGray + case slateGrey + case snow + case springGreen + case steelBlue + case tan + case teal + case thistle + case tomato + case turquoise + case violet + case wheat + case white + case whitesmoke + case yellow + case yellowGreen + + public var rgb: (red: Int, green: Int, blue: Int) { + switch self { + case .aliceBlue: (240, 248, 255) + case .antiqueWhite: (250, 235, 215) + case .aqua: (0, 255, 255) + case .aquamarine: (127, 255, 212) + case .azure: (240, 255, 255) + case .beige: (245, 245, 220) + case .bisque: (255, 228, 196) + case .black: (0, 0, 0) + case .blanchedAlmond: (255, 235, 205) + case .blue: (0, 0, 255) + case .blueViolet: (138, 43, 226) + case .brown: (165, 42, 42) + case .burlywood: (222, 184, 135) + case .cadetBlue: (95, 158, 160) + case .chartreuse: (127, 255, 0) + case .chocolate: (210, 105, 30) + case .coral: (255, 127, 80) + case .cornflowerBlue: (100, 149, 237) + case .cornsilk: (255, 248, 220) + case .crimson: (220, 20, 60) + case .cyan: (0, 255, 255) + case .darkBlue: (0, 0, 139) + case .darkCyan: (0, 139, 139) + case .darkGoldenrod: (184, 134, 11) + case .darkGray: (169, 169, 169) + case .darkGreen: (0, 100, 0) + case .darkGrey: (169, 169, 169) + case .darkKhaki: (189, 183, 107) + case .darkMagenta: (139, 0, 139) + case .darkOliveGreen: (85, 107, 47) + case .darkOrange: (255, 140, 0) + case .darkOrchid: (153, 50, 204) + case .darkRed: (139, 0, 0) + case .darkSalmon: (233, 150, 122) + case .darkSeagreen: (143, 188, 143) + case .darkSlateBlue: (72, 61, 139) + case .darkSlateGray: (47, 79, 79) + case .darkSlateGrey: (47, 79, 79) + case .darkTurquoise: (0, 206, 209) + case .darkViolet: (148, 0, 211) + case .deepPink: (255, 20, 147) + case .deepSkyblue: (0, 191, 255) + case .dimGray: (105, 105, 105) + case .dimGrey: (105, 105, 105) + case .dodgerBlue: (30, 144, 255) + case .firebrick: (178, 34, 34) + case .floralWhite: (255, 250, 240) + case .forestGreen: (34, 139, 34) + case .fuchsia: (255, 0, 255) + case .gainsboro: (220, 220, 220) + case .ghostWhite: (248, 248, 255) + case .gold: (255, 215, 0) + case .goldenrod: (218, 165, 32) + case .gray: (128, 128, 128) + case .green: (0, 128, 0) + case .greenYellow: (173, 255, 47) + case .grey: (128, 128, 128) + case .honeydew: (240, 255, 240) + case .hotPink: (255, 105, 180) + case .indianRed: (205, 92, 92) + case .indigo: (75, 0, 130) + case .ivory: (255, 255, 240) + case .khaki: (240, 230, 140) + case .lavender: (230, 230, 250) + case .lavenderBlush: (255, 240, 245) + case .lawnGreen: (124, 252, 0) + case .lemonChiffon: (255, 250, 205) + case .lightBlue: (173, 216, 230) + case .lightCoral: (240, 128, 128) + case .lightCyan: (224, 255, 255) + case .lightGoldenrodYellow: (250, 250, 210) + case .lightGray: (211, 211, 211) + case .lightGreen: (144, 238, 144) + case .lightGrey: (211, 211, 211) + case .lightPink: (255, 182, 193) + case .lightSalmon: (255, 160, 122) + case .lightSeagreen: (32, 178, 170) + case .lightSkyBlue: (135, 206, 250) + case .lightSlateGray: (119, 136, 153) + case .lightSlateGrey: (119, 136, 153) + case .lightSteelBlue: (176, 196, 222) + case .lightYellow: (255, 255, 224) + case .lime: (0, 255, 0) + case .limeGreen: (50, 205, 50) + case .linen: (250, 240, 230) + case .magenta: (255, 0, 255) + case .maroon: (128, 0, 0) + case .mediumAquamarine: (102, 205, 170) + case .mediumBlue: (0, 0, 205) + case .mediumOrchid: (186, 85, 211) + case .mediumPurple: (147, 112, 219) + case .mediumSeagreen: (60, 179, 113) + case .mediumSlateBlue: (123, 104, 238) + case .mediumSpringGreen: (0, 250, 154) + case .mediumTurquoise: (72, 209, 204) + case .mediumVioletRed: (199, 21, 133) + case .midnightBlue: (25, 25, 112) + case .mintCream: (245, 255, 250) + case .mistyRose: (255, 228, 225) + case .moccasin: (255, 228, 181) + case .navajoWhite: (255, 222, 173) + case .navy: (0, 0, 128) + case .oldLace: (253, 245, 230) + case .olive: (128, 128, 0) + case .oliveDrab: (107, 142, 35) + case .orange: (255, 165, 0) + case .orangeRed: (255, 69, 0) + case .orchid: (218, 112, 214) + case .paleGoldenrod: (238, 232, 170) + case .paleGreen: (152, 251, 152) + case .paleTurquoise: (175, 238, 238) + case .paleVioletRed: (219, 112, 147) + case .papayaWhip: (255, 239, 213) + case .peachPuff: (255, 218, 185) + case .peru: (205, 133, 63) + case .pink: (255, 192, 203) + case .plum: (221, 160, 221) + case .powderBlue: (176, 224, 230) + case .purple: (128, 0, 128) + case .red: (255, 0, 0) + case .rosyBrown: (188, 143, 143) + case .royalBlue: (65, 105, 225) + case .saddleBrown: (139, 69, 19) + case .salmon: (250, 128, 114) + case .sandyBrown: (244, 164, 96) + case .seagreen: (46, 139, 87) + case .seashell: (255, 245, 238) + case .sienna: (160, 82, 45) + case .silver: (192, 192, 192) + case .skyBlue: (135, 206, 235) + case .slateBlue: (106, 90, 205) + case .slateGray: (112, 128, 144) + case .slateGrey: (112, 128, 144) + case .snow: (255, 250, 250) + case .springGreen: (0, 255, 127) + case .steelBlue: (70, 130, 180) + case .tan: (210, 180, 140) + case .teal: (0, 128, 128) + case .thistle: (216, 191, 216) + case .tomato: (255, 99, 71) + case .turquoise: (64, 224, 208) + case .violet: (238, 130, 238) + case .wheat: (245, 222, 179) + case .white: (255, 255, 255) + case .whitesmoke: (245, 245, 245) + case .yellow: (255, 255, 0) + case .yellowGreen: (154, 205, 50) + } + } + + public var pigment: Pigment { + switch self { + case .aliceBlue: Pigment(240, 248, 255, alpha: 1.0) + case .antiqueWhite: Pigment(250, 235, 215, alpha: 1.0) + case .aqua: Pigment(0, 255, 255, alpha: 1.0) + case .aquamarine: Pigment(127, 255, 212, alpha: 1.0) + case .azure: Pigment(240, 255, 255, alpha: 1.0) + case .beige: Pigment(245, 245, 220, alpha: 1.0) + case .bisque: Pigment(255, 228, 196, alpha: 1.0) + case .black: Pigment(0, 0, 0, alpha: 1.0) + case .blanchedAlmond: Pigment(255, 235, 205, alpha: 1.0) + case .blue: Pigment(0, 0, 255, alpha: 1.0) + case .blueViolet: Pigment(138, 43, 226, alpha: 1.0) + case .brown: Pigment(165, 42, 42, alpha: 1.0) + case .burlywood: Pigment(222, 184, 135, alpha: 1.0) + case .cadetBlue: Pigment(95, 158, 160, alpha: 1.0) + case .chartreuse: Pigment(127, 255, 0, alpha: 1.0) + case .chocolate: Pigment(210, 105, 30, alpha: 1.0) + case .coral: Pigment(255, 127, 80, alpha: 1.0) + case .cornflowerBlue: Pigment(100, 149, 237, alpha: 1.0) + case .cornsilk: Pigment(255, 248, 220, alpha: 1.0) + case .crimson: Pigment(220, 20, 60, alpha: 1.0) + case .cyan: Pigment(0, 255, 255, alpha: 1.0) + case .darkBlue: Pigment(0, 0, 139, alpha: 1.0) + case .darkCyan: Pigment(0, 139, 139, alpha: 1.0) + case .darkGoldenrod: Pigment(184, 134, 11, alpha: 1.0) + case .darkGray: Pigment(169, 169, 169, alpha: 1.0) + case .darkGreen: Pigment(0, 100, 0, alpha: 1.0) + case .darkGrey: Pigment(169, 169, 169, alpha: 1.0) + case .darkKhaki: Pigment(189, 183, 107, alpha: 1.0) + case .darkMagenta: Pigment(139, 0, 139, alpha: 1.0) + case .darkOliveGreen: Pigment(85, 107, 47, alpha: 1.0) + case .darkOrange: Pigment(255, 140, 0, alpha: 1.0) + case .darkOrchid: Pigment(153, 50, 204, alpha: 1.0) + case .darkRed: Pigment(139, 0, 0, alpha: 1.0) + case .darkSalmon: Pigment(233, 150, 122, alpha: 1.0) + case .darkSeagreen: Pigment(143, 188, 143, alpha: 1.0) + case .darkSlateBlue: Pigment(72, 61, 139, alpha: 1.0) + case .darkSlateGray: Pigment(47, 79, 79, alpha: 1.0) + case .darkSlateGrey: Pigment(47, 79, 79, alpha: 1.0) + case .darkTurquoise: Pigment(0, 206, 209, alpha: 1.0) + case .darkViolet: Pigment(148, 0, 211, alpha: 1.0) + case .deepPink: Pigment(255, 20, 147, alpha: 1.0) + case .deepSkyblue: Pigment(0, 191, 255, alpha: 1.0) + case .dimGray: Pigment(105, 105, 105, alpha: 1.0) + case .dimGrey: Pigment(105, 105, 105, alpha: 1.0) + case .dodgerBlue: Pigment(30, 144, 255, alpha: 1.0) + case .firebrick: Pigment(178, 34, 34, alpha: 1.0) + case .floralWhite: Pigment(255, 250, 240, alpha: 1.0) + case .forestGreen: Pigment(34, 139, 34, alpha: 1.0) + case .fuchsia: Pigment(255, 0, 255, alpha: 1.0) + case .gainsboro: Pigment(220, 220, 220, alpha: 1.0) + case .ghostWhite: Pigment(248, 248, 255, alpha: 1.0) + case .gold: Pigment(255, 215, 0, alpha: 1.0) + case .goldenrod: Pigment(218, 165, 32, alpha: 1.0) + case .gray: Pigment(128, 128, 128, alpha: 1.0) + case .green: Pigment(0, 128, 0, alpha: 1.0) + case .greenYellow: Pigment(173, 255, 47, alpha: 1.0) + case .grey: Pigment(128, 128, 128, alpha: 1.0) + case .honeydew: Pigment(240, 255, 240, alpha: 1.0) + case .hotPink: Pigment(255, 105, 180, alpha: 1.0) + case .indianRed: Pigment(205, 92, 92, alpha: 1.0) + case .indigo: Pigment(75, 0, 130, alpha: 1.0) + case .ivory: Pigment(255, 255, 240, alpha: 1.0) + case .khaki: Pigment(240, 230, 140, alpha: 1.0) + case .lavender: Pigment(230, 230, 250, alpha: 1.0) + case .lavenderBlush: Pigment(255, 240, 245, alpha: 1.0) + case .lawnGreen: Pigment(124, 252, 0, alpha: 1.0) + case .lemonChiffon: Pigment(255, 250, 205, alpha: 1.0) + case .lightBlue: Pigment(173, 216, 230, alpha: 1.0) + case .lightCoral: Pigment(240, 128, 128, alpha: 1.0) + case .lightCyan: Pigment(224, 255, 255, alpha: 1.0) + case .lightGoldenrodYellow: Pigment(250, 250, 210, alpha: 1.0) + case .lightGray: Pigment(211, 211, 211, alpha: 1.0) + case .lightGreen: Pigment(144, 238, 144, alpha: 1.0) + case .lightGrey: Pigment(211, 211, 211, alpha: 1.0) + case .lightPink: Pigment(255, 182, 193, alpha: 1.0) + case .lightSalmon: Pigment(255, 160, 122, alpha: 1.0) + case .lightSeagreen: Pigment(32, 178, 170, alpha: 1.0) + case .lightSkyBlue: Pigment(135, 206, 250, alpha: 1.0) + case .lightSlateGray: Pigment(119, 136, 153, alpha: 1.0) + case .lightSlateGrey: Pigment(119, 136, 153, alpha: 1.0) + case .lightSteelBlue: Pigment(176, 196, 222, alpha: 1.0) + case .lightYellow: Pigment(255, 255, 224, alpha: 1.0) + case .lime: Pigment(0, 255, 0, alpha: 1.0) + case .limeGreen: Pigment(50, 205, 50, alpha: 1.0) + case .linen: Pigment(250, 240, 230, alpha: 1.0) + case .magenta: Pigment(255, 0, 255, alpha: 1.0) + case .maroon: Pigment(128, 0, 0, alpha: 1.0) + case .mediumAquamarine: Pigment(102, 205, 170, alpha: 1.0) + case .mediumBlue: Pigment(0, 0, 205, alpha: 1.0) + case .mediumOrchid: Pigment(186, 85, 211, alpha: 1.0) + case .mediumPurple: Pigment(147, 112, 219, alpha: 1.0) + case .mediumSeagreen: Pigment(60, 179, 113, alpha: 1.0) + case .mediumSlateBlue: Pigment(123, 104, 238, alpha: 1.0) + case .mediumSpringGreen: Pigment(0, 250, 154, alpha: 1.0) + case .mediumTurquoise: Pigment(72, 209, 204, alpha: 1.0) + case .mediumVioletRed: Pigment(199, 21, 133, alpha: 1.0) + case .midnightBlue: Pigment(25, 25, 112, alpha: 1.0) + case .mintCream: Pigment(245, 255, 250, alpha: 1.0) + case .mistyRose: Pigment(255, 228, 225, alpha: 1.0) + case .moccasin: Pigment(255, 228, 181, alpha: 1.0) + case .navajoWhite: Pigment(255, 222, 173, alpha: 1.0) + case .navy: Pigment(0, 0, 128, alpha: 1.0) + case .oldLace: Pigment(253, 245, 230, alpha: 1.0) + case .olive: Pigment(128, 128, 0, alpha: 1.0) + case .oliveDrab: Pigment(107, 142, 35, alpha: 1.0) + case .orange: Pigment(255, 165, 0, alpha: 1.0) + case .orangeRed: Pigment(255, 69, 0, alpha: 1.0) + case .orchid: Pigment(218, 112, 214, alpha: 1.0) + case .paleGoldenrod: Pigment(238, 232, 170, alpha: 1.0) + case .paleGreen: Pigment(152, 251, 152, alpha: 1.0) + case .paleTurquoise: Pigment(175, 238, 238, alpha: 1.0) + case .paleVioletRed: Pigment(219, 112, 147, alpha: 1.0) + case .papayaWhip: Pigment(255, 239, 213, alpha: 1.0) + case .peachPuff: Pigment(255, 218, 185, alpha: 1.0) + case .peru: Pigment(205, 133, 63, alpha: 1.0) + case .pink: Pigment(255, 192, 203, alpha: 1.0) + case .plum: Pigment(221, 160, 221, alpha: 1.0) + case .powderBlue: Pigment(176, 224, 230, alpha: 1.0) + case .purple: Pigment(128, 0, 128, alpha: 1.0) + case .red: Pigment(255, 0, 0, alpha: 1.0) + case .rosyBrown: Pigment(188, 143, 143, alpha: 1.0) + case .royalBlue: Pigment(65, 105, 225, alpha: 1.0) + case .saddleBrown: Pigment(139, 69, 19, alpha: 1.0) + case .salmon: Pigment(250, 128, 114, alpha: 1.0) + case .sandyBrown: Pigment(244, 164, 96, alpha: 1.0) + case .seagreen: Pigment(46, 139, 87, alpha: 1.0) + case .seashell: Pigment(255, 245, 238, alpha: 1.0) + case .sienna: Pigment(160, 82, 45, alpha: 1.0) + case .silver: Pigment(192, 192, 192, alpha: 1.0) + case .skyBlue: Pigment(135, 206, 235, alpha: 1.0) + case .slateBlue: Pigment(106, 90, 205, alpha: 1.0) + case .slateGray: Pigment(112, 128, 144, alpha: 1.0) + case .slateGrey: Pigment(112, 128, 144, alpha: 1.0) + case .snow: Pigment(255, 250, 250, alpha: 1.0) + case .springGreen: Pigment(0, 255, 127, alpha: 1.0) + case .steelBlue: Pigment(70, 130, 180, alpha: 1.0) + case .tan: Pigment(210, 180, 140, alpha: 1.0) + case .teal: Pigment(0, 128, 128, alpha: 1.0) + case .thistle: Pigment(216, 191, 216, alpha: 1.0) + case .tomato: Pigment(255, 99, 71, alpha: 1.0) + case .turquoise: Pigment(64, 224, 208, alpha: 1.0) + case .violet: Pigment(238, 130, 238, alpha: 1.0) + case .wheat: Pigment(245, 222, 179, alpha: 1.0) + case .white: Pigment(255, 255, 255, alpha: 1.0) + case .whitesmoke: Pigment(245, 245, 245, alpha: 1.0) + case .yellow: Pigment(255, 255, 0, alpha: 1.0) + case .yellowGreen: Pigment(154, 205, 50, alpha: 1.0) + } + } + } + + init( + _ name: Name, + @Clamping(0 ... 1) alpha: Double = 1.0 + ) { + red = Double(name.rgb.red) / 255.0 + green = Double(name.rgb.green) / 255.0 + blue = Double(name.rgb.blue) / 255.0 + self.alpha = alpha + } +} diff --git a/third-party/SwiftColor/Sources/Pigment+String.swift b/third-party/SwiftColor/Sources/Pigment+String.swift new file mode 100644 index 0000000000..22c7b01124 --- /dev/null +++ b/third-party/SwiftColor/Sources/Pigment+String.swift @@ -0,0 +1,76 @@ +import Foundation + +public extension Pigment { + init(_ value: String, alpha: Double = 1.0) { + if let keyword = Name.allCases.first(where: { $0.rawValue.caseInsensitiveCompare(value) == .orderedSame }) { + red = keyword.pigment.red + green = keyword.pigment.green + blue = keyword.pigment.blue + self.alpha = alpha + return + } + + if ExtendedKeyword.allCases.contains(where: { $0.rawValue.caseInsensitiveCompare(value) == .orderedSame }) { + red = 1.0 + green = 1.0 + blue = 1.0 + self.alpha = alpha + return + } + + var hex = value + if hex.hasPrefix("#") { + hex = String(hex.dropFirst()) + } + + guard let hexValue = Int(hex, radix: 16) else { + red = 1.0 + green = 1.0 + blue = 1.0 + self.alpha = alpha + return + } + + switch hex.count { + case 3: + let values = Self.hex3(hex: hexValue) + red = values.red + green = values.green + blue = values.blue + self.alpha = values.alpha + case 4: + let values = Self.hex4(hex: hexValue) + red = values.red + green = values.green + blue = values.blue + self.alpha = values.alpha + case 6: + let values = Self.hex6(hex: hexValue, alpha: alpha) + red = values.red + green = values.green + blue = values.blue + self.alpha = values.alpha + #if !os(watchOS) + case 8: + let values = Self.hex8(hex: hexValue) + red = values.red + green = values.green + blue = values.blue + self.alpha = values.alpha + #endif + default: + red = 1.0 + green = 1.0 + blue = 1.0 + self.alpha = alpha + } + } +} + +private extension Pigment { + enum ExtendedKeyword: String, CaseIterable { + case none + case clear + case transparent + } +} diff --git a/third-party/SwiftColor/Sources/Pigment+SwiftUI.swift b/third-party/SwiftColor/Sources/Pigment+SwiftUI.swift new file mode 100644 index 0000000000..d07aa1aec1 --- /dev/null +++ b/third-party/SwiftColor/Sources/Pigment+SwiftUI.swift @@ -0,0 +1,9 @@ +#if canImport(SwiftUI) +import SwiftUI + +public extension Pigment { + var color: Color { + Color(red: red, green: green, blue: blue, opacity: alpha) + } +} +#endif diff --git a/third-party/SwiftColor/Sources/Pigment+UIKit.swift b/third-party/SwiftColor/Sources/Pigment+UIKit.swift new file mode 100644 index 0000000000..8c96c2cc7e --- /dev/null +++ b/third-party/SwiftColor/Sources/Pigment+UIKit.swift @@ -0,0 +1,37 @@ +import Foundation +#if canImport(UIKit) +import UIKit + +public extension Pigment { + init(_ color: UIColor) { + var redComponent: CGFloat = 1.0 + var greenComponent: CGFloat = 1.0 + var blueComponent: CGFloat = 1.0 + var alphaComponent: CGFloat = 1.0 + + guard color.getRed(&redComponent, green: &greenComponent, blue: &blueComponent, alpha: &alphaComponent) else { + // TODO: Fail Initializer? Default Colors? + red = redComponent + green = greenComponent + blue = blueComponent + alpha = alphaComponent + return + } + + red = redComponent + green = greenComponent + blue = blueComponent + alpha = alphaComponent + } + + var uiColor: UIColor { + UIColor(red: red, green: green, blue: blue, alpha: alpha) + } +} + +public extension UIColor { + var pigment: Pigment { + Pigment(self) + } +} +#endif diff --git a/third-party/SwiftColor/Sources/Pigment.swift b/third-party/SwiftColor/Sources/Pigment.swift new file mode 100644 index 0000000000..3db75b50bb --- /dev/null +++ b/third-party/SwiftColor/Sources/Pigment.swift @@ -0,0 +1,53 @@ +/// A platform agnostic representation of Color +/// +/// The components - red, green, blue, & alpha - are maintained as a floating-point representation. +/// Each value can range from 0.0 to 1.0 (e.g. 0 to 100 percent). +/// +/// 'Pure White' is represented by values all equal to **1.0**. +public struct Pigment: Sendable { + + public let colorSpace: ColorSpace = .rgba + public let red: Double + public let green: Double + public let blue: Double + public let alpha: Double + + public init( + @Clamping(0 ... 1) red: Double = 1.0, + @Clamping(0 ... 1) green: Double = 1.0, + @Clamping(0 ... 1) blue: Double = 1.0, + @Clamping(0 ... 1) alpha: Double = 1.0 + ) { + self.red = red + self.green = green + self.blue = blue + self.alpha = alpha + } +} + +extension Pigment: CustomStringConvertible { + public var description: String { + String(format: "Pigment(red: %.4f, green: %.4f, blue: %.4f, alpha: %.2f)", red, green, blue, alpha) + } +} + +extension Pigment: Equatable { + public static func == (lhs: Pigment, rhs: Pigment) -> Bool { + guard lhs.red == rhs.red else { + return false + } + guard lhs.green == rhs.green else { + return false + } + guard lhs.blue == rhs.blue else { + return false + } + guard lhs.alpha == rhs.alpha else { + return false + } + guard lhs.colorSpace == rhs.colorSpace else { + return false + } + return true + } +} diff --git a/third-party/SwiftMath/BUILD b/third-party/SwiftMath/BUILD new file mode 100644 index 0000000000..66634a8c41 --- /dev/null +++ b/third-party/SwiftMath/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SwiftMath", + module_name = "SwiftMath", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AppBundle", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/third-party/SwiftMath/LICENSE b/third-party/SwiftMath/LICENSE new file mode 100755 index 0000000000..dddb1a7a28 --- /dev/null +++ b/third-party/SwiftMath/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Computer Inspirations + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third-party/SwiftMath/README.md b/third-party/SwiftMath/README.md new file mode 100644 index 0000000000..883c9038f5 --- /dev/null +++ b/third-party/SwiftMath/README.md @@ -0,0 +1,714 @@ +# SwiftMath + +`SwiftMath` provides a full Swift implementation of [iosMath](https://travis-ci.org/kostub/iosMath) +for displaying beautifully rendered math equations in iOS and MacOS applications. It typesets formulae written +using LaTeX in a `UILabel` equivalent class. It uses the same typesetting rules as LaTeX and +so the equations are rendered exactly as LaTeX would render them. + +Please also check out [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo.git) for examples of how to use `SwiftMath` +from SwiftUI. + +`SwiftMath` is similar to [MathJax](https://www.mathjax.org) or +[KaTeX](https://github.com/Khan/KaTeX) for the web but for native iOS or MacOS +applications without having to use a `UIWebView` and Javascript. More +importantly, it is significantly faster than using a `UIWebView`. + +`SwiftMath` is a Swift translation of the latest `iosMath` v0.9.5 release but includes bug fixes +and enhancements like a new \lbar (lambda bar) character and cyrillic alphabet support. +The original `iosMath` test suites have also been translated to Swift and run without errors. +Note: Error test conditions are ignored to avoid tagging everything with silly `throw`s. +Please let me know of any bugs or bug fixes that you find. + +`SwiftMath` prepackages everything needed for direct access via the Swift Package Manager. + +## Examples +Here are screenshots of some formulae that were rendered with this library: + +```LaTeX +x = \frac{-b \pm \sqrt{b^2-4ac}}{2a} +``` + +![Quadratic Formula](img/quadratic-light.png#gh-light-mode-only) +![Quadratic Formula](img/quadratic-dark.png#gh-dark-mode-only) + +```LaTeX +f(x) = \int\limits_{-\infty}^\infty\!\hat f(\xi)\,e^{2 \pi i \xi x}\,\mathrm{d}\xi +``` + +![Calculus](img/calculus-light.png#gh-light-mode-only) +![Calculus](img/calculus-dark.png#gh-dark-mode-only) + +```LaTeX +\frac{1}{n}\sum_{i=1}^{n}x_i \geq \sqrt[n]{\prod_{i=1}^{n}x_i} +``` + +![AM-GM](img/amgm-light.png#gh-light-mode-only) +![AM-GM](img/amgm-dark.png#gh-dark-mode-only) + +```LaTex +\frac{1}{\left(\sqrt{\phi \sqrt{5}}-\phi\\right) e^{\frac25 \pi}} += 1+\frac{e^{-2\pi}} {1 +\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } } +``` + +![Ramanujan Identity](img/ramanujan-light.png#gh-light-mode-only) +![Ramanujan Identity](img/ramanujan-dark.png#gh-dark-mode-only) + +More examples are included in [EXAMPLES](EXAMPLES.md) + +## Fonts +Here are previews of the included fonts: + +![](img/FontsPreview.png#gh-dark-mode-only) +![](img/FontsPreviewLight.png#gh-light-mode-only) + +## Requirements +`SwiftMath` works on iOS 11+ or MacOS 12+. It depends +on the following Apple frameworks: + +* Foundation.framework +* CoreGraphics.framework +* QuartzCore.framework +* CoreText.framework + +Additionally for iOS it requires: +* UIKit.framework + +Additionally for MacOS it requires: +* AppKit.framework + +## Installation + +### Swift Package + +`SwiftMath` is available from [SwiftMath](https://github.com/mgriebling/SwiftMath.git). +To use it in your code, just add the https://github.com/mgriebling/SwiftMath.git path to +XCode's package manager. + +## Usage + +The library provides a class `MTMathUILabel` which is a `UIView` that +supports rendering math equations. To display an equation simply create +an `MTMathUILabel` as follows: + +```swift + +import SwiftMath + +let label = MTMathUILabel() +label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" + +``` +Adding `MTMathUILabel` as a sub-view of your `UIView` will render the +quadratic formula example shown above. + +The following code creates a SwiftUI component called `MathView` encapsulating the MTMathUILabel: + +```swift +import SwiftUI +import SwiftMath + +struct MathView: UIViewRepresentable { + var equation: String + var font: MathFont = .latinModernFont + var textAlignment: MTTextAlignment = .center + var fontSize: CGFloat = 30 + var labelMode: MTMathUILabelMode = .text + var insets: MTEdgeInsets = MTEdgeInsets() + + func makeUIView(context: Context) -> MTMathUILabel { + let view = MTMathUILabel() + view.setContentHuggingPriority(.required, for: .vertical) + view.setContentCompressionResistancePriority(.required, for: .vertical) + return view + } + + func updateUIView(_ view: MTMathUILabel, context: Context) { + view.latex = equation + let font = MTFontManager().font(withName: font.rawValue, size: fontSize) + font?.fallbackFont = UIFont.systemFont(ofSize: fontSize) + view.font = font + view.textAlignment = textAlignment + view.labelMode = labelMode + view.textColor = MTColor(Color.primary) + view.contentInsets = insets + view.invalidateIntrinsicContentSize() + } + + func sizeThatFits(_ proposal: ProposedViewSize, uiView: MTMathUILabel, context: Context) -> CGSize? { + // Enable line wrapping by passing proposed width to the label + if let width = proposal.width, width.isFinite, width > 0 { + uiView.preferredMaxLayoutWidth = width + let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)) + return size + } + return nil + } +} +``` + +For code that works with SwiftUI running natively under MacOS use the following: + +```swift +import SwiftUI +import SwiftMath + +struct MathView: NSViewRepresentable { + var equation: String + var font: MathFont = .latinModernFont + var textAlignment: MTTextAlignment = .center + var fontSize: CGFloat = 30 + var labelMode: MTMathUILabelMode = .text + var insets: MTEdgeInsets = MTEdgeInsets() + + func makeNSView(context: Context) -> MTMathUILabel { + let view = MTMathUILabel() + view.setContentHuggingPriority(.required, for: .vertical) + view.setContentCompressionResistancePriority(.required, for: .vertical) + return view + } + + func updateNSView(_ view: MTMathUILabel, context: Context) { + view.latex = equation + let font = MTFontManager().font(withName: font.rawValue, size: fontSize) + font?.fallbackFont = NSFont.systemFont(ofSize: fontSize) + view.font = font + view.textAlignment = textAlignment + view.labelMode = labelMode + view.textColor = MTColor(Color.primary) + view.contentInsets = insets + view.invalidateIntrinsicContentSize() + } + + func sizeThatFits(_ proposal: ProposedViewSize, nsView: MTMathUILabel, context: Context) -> CGSize? { + // Enable line wrapping by passing proposed width to the label + if let width = proposal.width, width.isFinite, width > 0 { + nsView.preferredMaxLayoutWidth = width + let size = nsView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)) + return size + } + return nil + } +} +``` + +### Automatic Line Wrapping + +`SwiftMath` supports automatic line wrapping (multiline display) for mathematical content. The implementation uses **interatom line breaking** which breaks equations at atom boundaries (between mathematical elements) rather than within them, preserving the semantic structure of the mathematics. + +#### Using Line Wrapping with UIKit/AppKit + +For direct `MTMathUILabel` usage, set the `preferredMaxLayoutWidth` property: + +```swift +let label = MTMathUILabel() +label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5" +label.font = MTFontManager.fontManager.defaultFont + +// Enable line wrapping by setting a maximum width +label.preferredMaxLayoutWidth = 235 +``` + +You can also use `sizeThatFits` to calculate the size with a width constraint: + +```swift +let constrainedSize = label.sizeThatFits(CGSize(width: 235, height: .greatestFiniteMagnitude)) +``` + +#### Using Line Wrapping with SwiftUI + +The `MathView` examples above include `sizeThatFits()` which automatically enables line wrapping when SwiftUI proposes a width constraint. No additional configuration is needed: + +```swift +VStack(alignment: .leading, spacing: 8) { + MathView( + equation: "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5", + fontSize: 17, + labelMode: .text + ) +} +.frame(maxWidth: 235) // The equation will break across multiple lines +``` + +#### Line Wrapping Behavior and Capabilities + +SwiftMath implements **two complementary line breaking mechanisms**: + +##### 1. Interatom Line Breaking (Primary) +Breaks equations **between atoms** (mathematical elements) when content exceeds the width constraint. This is the preferred method as it maintains semantic integrity. + +##### 2. Universal Line Breaking (Fallback) +For very long text within single atoms, breaks at Unicode word boundaries using Core Text with number protection (prevents splitting numbers like "3.14"). + +See `MULTILINE_IMPLEMENTATION_NOTES.md` for implementation details and recent changes. + +#### Fully Supported Cases + +These atom types work perfectly with interatom line breaking: + +**✅ Variables and ordinary text:** +```swift +label.latex = "a b c d e f g h i j k l m n o p" +label.preferredMaxLayoutWidth = 150 +// Breaks between individual variables at natural boundaries +``` + +**✅ Binary operators (+, -, ×, ÷):** +```swift +label.latex = "a+b+c+d+e+f+g+h" +label.preferredMaxLayoutWidth = 100 +// Breaks cleanly: "a+b+c+d+" +// "e+f+g+h" +``` + +**✅ Relations (=, <, >, ≤, ≥, etc.):** +```swift +label.latex = "a=1, b=2, c=3, d=4, e=5" +label.preferredMaxLayoutWidth = 120 +// Breaks after commas and operators +``` + +**✅ Mixed text and simple math:** +```swift +label.latex = "\\text{Calculer }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1" +label.preferredMaxLayoutWidth = 200 +// Breaks between text and math atoms naturally +``` + +**✅ Punctuation (commas, periods):** +```swift +label.latex = "\\text{First, second, third, fourth, fifth}" +label.preferredMaxLayoutWidth = 150 +// Breaks at commas and spaces +``` + +**✅ Brackets and parentheses (simple):** +```swift +label.latex = "(a+b)+(c+d)+(e+f)" +label.preferredMaxLayoutWidth = 120 +// Breaks between parenthesized groups +``` + +**✅ Greek letters and symbols:** +```swift +label.latex = "\\alpha+\\beta+\\gamma+\\delta+\\epsilon+\\zeta" +label.preferredMaxLayoutWidth = 150 +// Breaks between Greek letters +``` + +**✅ Fractions (NEW!):** +```swift +label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c" +label.preferredMaxLayoutWidth = 150 +// Fractions stay inline if they fit, break to new line only when needed +// Example: "a + ½ + b" stays on one line if it fits +``` + +**✅ Radicals/Square roots (NEW!):** +```swift +label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z" +label.preferredMaxLayoutWidth = 150 +// Radicals stay inline if they fit, break to new line only when needed +// Example: "x + √2 + y" stays on one line if it fits +``` + +**✅ Mixed fractions and radicals (NEW!):** +```swift +label.latex = "a+\\frac{1}{2}+\\sqrt{3}+b" +label.preferredMaxLayoutWidth = 200 +// Breaks between complex mathematical elements +``` + +#### Limited Support Cases + +These cases work but with some constraints: + +**✅ Large operators (NEW!):** +```swift +label.latex = "a + \\sum_{i=1}^{n} x_i + \\int_{0}^{1} f(x)dx + b" +label.preferredMaxLayoutWidth = 200 +// Large operators stay inline when they fit! +// Includes height checking for operators with limits +``` + +**✅ Delimited expressions (NEW!):** +```swift +label.latex = "(a+b) + \\left(\\frac{c}{d}\\right) + e" +label.preferredMaxLayoutWidth = 200 +// Delimiters stay inline when they fit! +// Inner content respects width constraints and wraps naturally +``` + +**✅ Colored expressions (NEW!):** +```swift +label.latex = "a + \\color{red}{b + c + d} + e" +label.preferredMaxLayoutWidth = 200 +// Colored sections stay inline when they fit! +// Inner content respects width constraints and wraps properly +``` + +**✅ Matrices/tables (NEW!):** +```swift +label.latex = "A = \\begin{pmatrix} 1 & 2 \\end{pmatrix} + B" +label.preferredMaxLayoutWidth = 200 +// Small matrices stay inline when they fit! +``` + +**✅ Atoms with superscripts/subscripts (IMPROVED!):** +```swift +label.latex = "a^{2}+b^{2}+c^{2}+d^{2}+e^{2}" +label.preferredMaxLayoutWidth = 150 +// Now works with width-based breaking! +// Scripted atoms participate in smart line breaking decisions +``` + +**✅ Math accents:** +```swift +label.latex = "\\hat{x} + \\tilde{y} + \\bar{z} + \\vec{w}" +label.preferredMaxLayoutWidth = 150 +// Common accents (\hat, \tilde, \bar, \vec) work well +// Vector arrows (\overrightarrow, etc.) supported with stretching +``` + +**⚠️ Very long single text atoms:** +```swift +label.latex = "\\text{This is an extremely long piece of text within a single text command}" +label.preferredMaxLayoutWidth = 200 +// Uses Unicode word boundary breaking with Core Text +// Protects numbers from being split (e.g., "3.14" stays together) +``` +**Note**: Breaks within the text atom rather than between atoms, which is expected behavior for very long continuous text. + +#### Best Practices + +**DO:** +- Use interatom breaking for simple equations with operators and relations +- Use for mixed text and math where you want natural breaks +- Use for long sequences of variables, numbers, and operators +- Set appropriate `preferredMaxLayoutWidth` based on your layout needs + +**DON'T:** +- Expect natural breaking in expressions with large operators (∑, ∫, etc. - not yet optimized) +- Expect natural breaking in expressions with \left...\right delimiters (not yet optimized) +- Use extremely narrow widths (less than ~80pt) which may cause poor breaks + +#### Examples + +**Excellent use case (discriminant formula):** +```swift +label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5" +label.preferredMaxLayoutWidth = 235 +// ✅ Breaks naturally at good points between atoms +``` + +**Good use case (simple arithmetic):** +```swift +label.latex = "5+10+15+20+25+30+35+40+45+50" +label.preferredMaxLayoutWidth = 150 +// ✅ Breaks between operators cleanly +``` + +**Excellent use case (fractions inline - NEW!):** +```swift +label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c" +label.preferredMaxLayoutWidth = 200 +// ✅ Fractions stay inline when they fit! +// Breaks: "a + ½ + b" on line 1, "+ ¾ + c" on line 2 +``` + +**Excellent use case (radicals inline - NEW!):** +```swift +label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z" +label.preferredMaxLayoutWidth = 150 +// ✅ Radicals stay inline when they fit! +// Example: "x + √2 + y" on line 1, "+ √3 + z" on line 2 +``` + +**Alternative for complex expressions:** +```swift +// Instead of trying to break this: +label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" +// Consider it as a single display equation without width constraint +label.preferredMaxLayoutWidth = 0 // No breaking +``` + +#### Technical Details + +- **Line spacing**: Dynamic line height based on actual content (tall fractions get more space, regular content stays compact) +- **Breaking algorithm**: Greedy with look-ahead and break quality scoring - prefers breaking after operators over other positions +- **Width calculation**: Includes inter-element spacing according to TeX spacing rules +- **Number protection**: Numbers in patterns like "3.14", "1,000", etc. are kept intact +- **Supports locales**: English, French, Swiss number formats +- **Advanced features**: Tokenization infrastructure with phase-based processing (preprocessing → tokenization → line fitting → display generation) + +For complete implementation details including recent improvements (dynamic line height, break quality scoring, early exit optimization), see [MULTILINE_IMPLEMENTATION_NOTES.md](MULTILINE_IMPLEMENTATION_NOTES.md). + +### Included Features +This is a list of formula types that the library currently supports: + +* Simple algebraic equations +* Fractions and continued fractions (including `\frac`, `\dfrac`, `\tfrac`, `\cfrac`) +* Exponents and subscripts +* Trigonometric formulae (including inverse hyperbolic: `\arcsinh`, `\arccosh`, etc.) +* Square roots and n-th roots +* Calculus symbols - limits, derivatives, integrals (including `\iint`, `\iiint`, `\iiiint`) +* Big operators (e.g. product, sum) +* Big delimiters (using `\left` and `\right`) +* Manual delimiter sizing (`\big`, `\Big`, `\bigg`, `\Bigg` and variants) +* Greek alphabet +* Bold Greek symbols (`\boldsymbol`) +* Combinatorics (`\binom`, `\choose` etc.) +* Geometry symbols (e.g. angle, congruence etc.) +* Ratios, proportions, percentages +* Math spacing +* Overline and underline +* Math accents (including `\hat`, `\tilde`, `\bar`, `\vec`, `\dot`, `\ddot`, etc.) +* Vector arrows (`\vec`, `\overrightarrow`, `\overleftarrow`, `\overleftrightarrow` with automatic stretching) +* Wide accents (`\widehat`, `\widetilde`) +* Operator names (`\operatorname{name}`) +* Matrices (including `\smallmatrix` and starred variants like `pmatrix*` with alignment) +* Multi-line subscripts and limits (`\substack`) +* Equation alignment +* Change bold, roman, caligraphic and other font styles (`\bf`, `\text`, etc.) +* Style commands (`\displaystyle`, `\textstyle`) +* Custom operators (`\operatorname`, `\operatorname*`) +* Dirac notation (`\bra`, `\ket`, `\braket`) +* Most commonly used math symbols +* Colors for both text and background +* **Inline and display math mode delimiters** (see below) +* **Automatic line wrapping** (see Automatic Line Wrapping section) + +### LaTeX Math Delimiters + +`SwiftMath` now supports all standard LaTeX math delimiters for both inline and display modes. The parser automatically detects and handles these delimiters: + +#### Inline Math (Text Style) +Use these delimiters for inline math within text, which renders more compactly: + +```swift +// Dollar signs (TeX style) +label.latex = "$E = mc^2$" + +// Parentheses (LaTeX style) +label.latex = "\\(\\sum_{i=1}^{n} x_i\\)" + +// Cases environment in inline mode +label.latex = "\\(\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}\\)" +``` + +#### Display Math (Display Style) +Use these delimiters for standalone equations with larger operators and limits: + +```swift +// Double dollar signs (TeX style) +label.latex = "$$\\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$" + +// Square brackets (LaTeX style) +label.latex = "\\[\\sum_{k=1}^{n} k^2 = \\frac{n(n+1)(2n+1)}{6}\\]" + +// Equation environment +label.latex = "\\begin{equation} x^2 + y^2 = z^2 \\end{equation}" + +// Cases environment in display mode +label.latex = "\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}" +``` + +**Note:** The difference between inline and display modes: +- **Inline mode** (`$...$` or `\(...\)`) renders compactly, suitable for math within text +- **Display mode** (`$$...$$`, `\[...\]`, or environments) renders with larger operators and limits positioned above/below + +All delimiters are automatically stripped during parsing, and the math mode is set appropriately. No additional configuration is needed! + +#### Backward Compatibility +Equations without explicit delimiters continue to work as before, defaulting to display mode: + +```swift +label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" // Works as always +``` + +#### Programmatic API +For advanced use cases where you need to parse LaTeX and determine the detected style programmatically, use the `buildWithStyle` method: + +```swift +// Parse LaTeX and get both the math list and detected style +let (mathList, style) = MTMathListBuilder.buildWithStyle(fromString: "\\[x^2 + y^2 = z^2\\]") + +// style will be .display for \[...\] or $$...$$ +// style will be .text for \(...\) or $...$ + +// Create a display with the detected style +if let mathList = mathList { + let display = MTTypesetter.createLineForMathList(mathList, font: myFont, style: style) + // Use the display for rendering +} +``` + +This is particularly useful when building custom renderers or when you need to respect the user's choice of delimiter style. + +Note: SwiftMath only supports the commands in LaTeX's math mode. There is +also no language support for other than west European langugages and some +Cyrillic characters. There would be two ways to support more languages: + +1) Find a math font compatible with `SwiftMath` that contains all the glyphs +for that language. +2) Add support to `SwiftMath` for standard Unicode fonts that contain all +langauge glyphs. + +Of these two, the first is much easier. However, if you want a challenge, +try to tackle the second option. + +### Example + +The [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo) is a SwiftUI version +of the Objective-C demo included in `iosMath` that uses `SwiftMath` as a Swift package dependency. + +### Advanced configuration + +`MTMathUILabel` supports some advanced configuration options: + +##### Math mode + +You can change the mode of the `MTMathUILabel` between Display Mode +(equivalent to `$$` or `\[` in LaTeX) and Text Mode (equivalent to `$` +or `\(` in LaTeX). The default style is Display. To switch to Text +simply: + +```swift +label.labelMode = .text +``` + +##### Text Alignment +The default alignment of the equations is left. This can be changed to +center or right as follows: + +```swift +label.textAlignment = .center +``` + +##### Font size +The default font-size is 30pt. You can change it as follows: + +```swift +label.fontSize = 25 +``` +##### Font +The default font is *Latin Modern Math*. This can be changed as: + +```swift +label.font = MTFontManager.fontmanager.termesFont(withSize:20) +``` + +This project has 12 fonts bundled with it, but you can use any OTF math +font. A python script is included that generates the `.plist` files +required for an `.otf` font to work with `SwiftMath`. If you generate +(and test) any other fonts please contribute them back to this project for +others to benefit. + + + +Note: The `KpMath-Light`, `KpMath-Sans`, `Asana` fonts currently incorrectly +render very large radicals. It appears that the font files do +not properly define the offsets required to typeset these glyphs. If +anyone can fix this, it would be greatly appreciated. + +##### Text Color +The default color of the rendered equation is black. You can change +it to any other color as follows: + +```swift +label.textColor = .red +``` + +It is also possible to set different colors for different parts of the +equation. Just access the `displayList` field and set the `textColor` +of the underlying displays of which you want to change the color. + +##### Fallback Font for Unicode Text +By default, math fonts only support a limited set of characters (Latin, Greek, common math symbols). +To display other Unicode characters like Chinese, Japanese, Korean, emoji, or other scripts in `\text{}` +commands, you can configure a fallback font: + +```swift +let mathFont = MTFontManager().font(withName: MathFont.latinModernFont.rawValue, size: 30) + +// Set a fallback font for unsupported characters (defaults to nil) +#if os(iOS) || os(visionOS) +let systemFont = UIFont.systemFont(ofSize: 30) +mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil) +#elseif os(macOS) +let systemFont = NSFont.systemFont(ofSize: 30) +mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil) +#endif + +label.font = mathFont +label.latex = "\\text{Hello 世界 🌍}" // English, Chinese, and emoji +``` + +When the main math font doesn't contain a glyph for a character, the fallback font will be used automatically. +This is particularly useful for: +- Chinese text: `\text{中文}` +- Japanese text: `\text{日本語}` +- Korean text: `\text{한국어}` +- Emoji: `\text{Math is fun! 🎉📐}` +- Mixed scripts: `\text{Equation: 方程式}` + +**Note**: The fallback font only applies to characters within `\text{}` commands, not regular math mode. + +##### Custom Commands +You can define your own commands that are not already predefined. This is +similar to macros is LaTeX. To define your own command use: + +```swift +MTMathAtomFactory.addLatexSymbol("lcm", value: MTMathAtomFactory.operator(withName: "lcm", limits: false)) +``` + +This creates an `\lcm` command that can be used in the LaTeX. + +##### Content Insets +The `MTMathUILabel` has `contentInsets` for finer control of placement of the +equation in relation to the view. + +If you need to set it you can do as follows: + +```swift +label.contentInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 20) +``` + +##### Error handling + +If the LaTeX text given to `MTMathUILabel` is +invalid or if it contains commands that aren't currently supported then +an error message will be displayed instead of the label. + +This error can be programmatically retrieved as `label.error`. If you +prefer not to display anything then set: + +```swift +label.displayErrorInline = true +``` + +## Future Enhancements + +Note this is not a complete implementation of LaTeX math mode. There are +some pieces that are missing and may be included in future updates: + +* `\middle` delimiter for use between `\left` and `\right` +* Some fine spacing commands (`\:`, `\;`, `\!` - note that `\,` works) + +For a complete list of features and their implementation status, see [MISSING_FEATURES.md](MISSING_FEATURES.md). + +## License + +`SwiftMath` is available under the MIT license. See the [LICENSE](./LICENSE) +file for more info. + +### Fonts +This distribution contains the following fonts. These fonts are +licensed as follows: +* Latin Modern Math: + [GUST Font License](GUST-FONT-LICENSE.txt) +* Tex Gyre Termes: + [GUST Font License](GUST-FONT-LICENSE.txt) +* [XITS Math](https://github.com/khaledhosny/xits-math): + [Open Font License](OFL.txt) +* [KpMath Light/KpMath Sans](http://scripts.sil.org/OFL): + [SIL Open Font License](OFL.txt) diff --git a/third-party/SwiftMath/Sources/MathBundle/MTFontMathTableV2.swift b/third-party/SwiftMath/Sources/MathBundle/MTFontMathTableV2.swift new file mode 100755 index 0000000000..beb2320aa0 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathBundle/MTFontMathTableV2.swift @@ -0,0 +1,194 @@ +// +// MTFontMathTableV2.swift +// +// +// Created by Peter Tang on 15/9/2023. +// + +import Foundation +import CoreGraphics +import CoreText + +internal class MTFontMathTableV2: MTFontMathTable { + private let mathFont: MathFont + private let fontSize: CGFloat + private let unitsPerEm: UInt + private let mTable: NSDictionary + init(mathFont: MathFont, size: CGFloat, unitsPerEm: UInt) { + self.mathFont = mathFont + self.fontSize = size + self.unitsPerEm = unitsPerEm + mTable = mathFont.rawMathTable() + super.init(withFont: mathFont.mtfont(size: fontSize), mathTable: mTable) + super._mathTable = nil + // disable all possible access to _mathTable in superclass! + } + override var _mathTable: NSDictionary? { + set { fatalError("\(#function) change to _mathTable \(mathFont.rawValue) not allowed.") } + get { mTable } + } + override var muUnit: CGFloat { fontSize/18 } + + override func fontUnitsToPt(_ fontUnits:Int) -> CGFloat { + CGFloat(fontUnits) * fontSize / CGFloat(unitsPerEm) + } + override func constantFromTable(_ constName: String) -> CGFloat { + guard let consts = mTable[kConstants] as? NSDictionary, let val = consts[constName] as? NSNumber else { + return .zero + } + return fontUnitsToPt(val.intValue) + } + override func percentFromTable(_ percentName: String) -> CGFloat { + guard let consts = mTable[kConstants] as? NSDictionary, let val = consts[percentName] as? NSNumber else { + return .zero + } + return CGFloat(val.floatValue) / 100 + } + /** Returns an Array of all the vertical variants of the glyph if any. If + there are no variants for the glyph, the array contains the given glyph. */ + override func getVerticalVariantsForGlyph(_ glyph: CGGlyph) -> [NSNumber?] { + guard let variants = mTable[kVertVariants] as? NSDictionary else { return [] } + return self.getVariantsForGlyph(glyph, inDictionary: variants) + } + /** Returns an Array of all the horizontal variants of the glyph if any. If + there are no variants for the glyph, the array contains the given glyph. */ + override func getHorizontalVariantsForGlyph(_ glyph: CGGlyph) -> [NSNumber?] { + guard let variants = mTable[kHorizVariants] as? NSDictionary else { return [] } + return self.getVariantsForGlyph(glyph, inDictionary:variants) + } + override func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants: NSDictionary) -> [NSNumber?] { + let font = mathFont.mtfont(size: fontSize) + let glyphName = font.get(nameForGlyph: glyph) + + var glyphArray = [NSNumber]() + let variantGlyphs = variants[glyphName] as? NSArray + + guard let variantGlyphs = variantGlyphs, variantGlyphs.count != .zero else { + // There are no extra variants, so just add the current glyph to it. + let glyph = font.get(glyphWithName: glyphName) + glyphArray.append(NSNumber(value:glyph)) + return glyphArray + } + for gvn in variantGlyphs { + if let glyphVariantName = gvn as? String { + let variantGlyph = font.get(glyphWithName: glyphVariantName) + glyphArray.append(NSNumber(value:variantGlyph)) + } + } + return glyphArray + } + /** Returns a larger vertical variant of the given glyph if any. + If there is no larger version, this returns the current glyph. + + - Parameter glyph: The glyph to find a larger variant for + - Parameter forDisplayStyle: If true, selects the largest appropriate variant for display style. + If false, selects the next larger variant (incremental sizing). + - Returns: A larger glyph variant, or the original glyph if no variants exist + */ + override func getLargerGlyph(_ glyph: CGGlyph, forDisplayStyle: Bool = false) -> CGGlyph { + let font = mathFont.mtfont(size: fontSize) + let glyphName = font.get(nameForGlyph: glyph) + + guard let variants = mTable[kVertVariants] as? NSDictionary, + let variantGlyphs = variants[glyphName] as? NSArray, variantGlyphs.count != .zero else { + // There are no extra variants, so just returnt the current glyph. + return glyph + } + + if forDisplayStyle { + // For display style, select a large variant suitable for mathematical display mode + // Display integrals should be significantly larger (~2.2em) for visual prominence + let count = variantGlyphs.count + + // Strategy: Use the largest variant, but avoid extreme sizes for fonts with many variants + let targetIndex: Int + if count <= 2 { + // Small variant list: use the last one (e.g., integral.size1 at ~2.2em) + targetIndex = count - 1 + } else if count <= 4 { + // Medium variant list: use second-to-last to avoid extremes + targetIndex = count - 2 + } else { + // Large variant list (like texgyretermes with 6 variants): + // Use variant at ~60% position to get appropriate display size (~2.0em) + // For 7 variants (0-6), this gives index 4 + targetIndex = min(count - 2, Int(Double(count) * 0.6)) + } + + if let glyphVariantName = variantGlyphs[targetIndex] as? String { + let variantGlyph = font.get(glyphWithName: glyphVariantName) + return variantGlyph + } + } else { + // Text/inline style: use incremental sizing for moderate enlargement + // Find the first variant with a different name + for gvn in variantGlyphs { + if let glyphVariantName = gvn as? String, glyphVariantName != glyphName { + let variantGlyph = font.get(glyphWithName: glyphVariantName) + return variantGlyph + } + } + } + + // We did not find any variants of this glyph so return it. + return glyph + } + /** Returns the italic correction for the given glyph if any. If there + isn't any this returns 0. */ + override func getItalicCorrection(_ glyph: CGGlyph) -> CGFloat { + let font = mathFont.mtfont(size: fontSize) + let glyphName = font.get(nameForGlyph: glyph) + + guard let italics = mTable[kItalic] as? NSDictionary, let val = italics[glyphName] as? NSNumber else { + return .zero + } + // if val is nil, this returns 0. + return fontUnitsToPt(val.intValue) + } + override func getTopAccentAdjustment(_ glyph: CGGlyph) -> CGFloat { + let font = mathFont.mtfont(size: fontSize) + let glyphName = font.get(nameForGlyph: glyph) + + guard let accents = mTable[kAccents] as? NSDictionary, let val = accents[glyphName] as? NSNumber else { + // If no top accent is defined then it is the center of the advance width. + var glyph = glyph + var advances = CGSize.zero + CTFontGetAdvancesForGlyphs(font.ctFont, .horizontal, &glyph, &advances, 1) + return advances.width/2 + } + return fontUnitsToPt(val.intValue) + } + override func getVerticalGlyphAssembly(forGlyph glyph: CGGlyph) -> [GlyphPart] { + let font = mathFont.mtfont(size: fontSize) + let glyphName = font.get(nameForGlyph: glyph) + + guard let assemblyTable = mTable[kVertAssembly] as? NSDictionary, + let assemblyInfo = assemblyTable[glyphName] as? NSDictionary, + let parts = assemblyInfo[kAssemblyParts] as? NSArray else { + // No vertical assembly defined for glyph + // parts should always have been defined, but if it isn't return nil + return [] + } + + var rv = [GlyphPart]() + for part in parts { + guard let partInfo = part as? NSDictionary, + let adv = partInfo["advance"] as? NSNumber, + let end = partInfo["endConnector"] as? NSNumber, + let start = partInfo["startConnector"] as? NSNumber, + let ext = partInfo["extender"] as? NSNumber, + let glyphName = partInfo["glyph"] as? String else { continue } + let fullAdvance = fontUnitsToPt(adv.intValue) + let endConnectorLength = fontUnitsToPt(end.intValue) + let startConnectorLength = fontUnitsToPt(start.intValue) + let isExtender = ext.boolValue + let glyph = font.get(glyphWithName: glyphName) + let part = GlyphPart(glyph: glyph, fullAdvance: fullAdvance, + startConnectorLength: startConnectorLength, + endConnectorLength: endConnectorLength, + isExtender: isExtender) + rv.append(part) + } + return rv + } +} diff --git a/third-party/SwiftMath/Sources/MathBundle/MTFontV2.swift b/third-party/SwiftMath/Sources/MathBundle/MTFontV2.swift new file mode 100755 index 0000000000..14c09a063c --- /dev/null +++ b/third-party/SwiftMath/Sources/MathBundle/MTFontV2.swift @@ -0,0 +1,68 @@ +// +// MTFontV2.swift +// +// +// Created by Peter Tang on 15/9/2023. +// + +import Foundation +import CoreGraphics +import CoreText + +extension MathFont { + public func mtfont(size: CGFloat) -> MTFontV2 { + MTFontV2(font: self, size: size) + } +} +public final class MTFontV2: MTFont { + let font: MathFont + let size: CGFloat + private let _cgFont: CGFont + private let _ctFont: CTFont + private let unitsPerEm: UInt + private var _mathTab: MTFontMathTableV2? + init(font: MathFont = .latinModernFont, size: CGFloat) { + self.font = font + self.size = size + // MathFont cgfont and ctfont are fast & threadsafe, keep a local copy is cheaper than + // handling via NSLock + self._cgFont = font.cgFont() + self._ctFont = font.ctFont(withSize: size) + self.unitsPerEm = self._ctFont.unitsPerEm + super.init() + + super.defaultCGFont = nil + super.ctFont = nil + super.mathTable = nil + super.rawMathTable = nil + } + override var defaultCGFont: CGFont! { + set { fatalError("\(#function): change to \(font.fontName) not allowed.") } + get { _cgFont } + } + override var ctFont: CTFont! { + set { fatalError("\(#function): change to \(font.fontName) not allowed.") } + get { _ctFont } + } + private let mtfontV2LockOnMathTable = NSLock() + override var mathTable: MTFontMathTable? { + set { fatalError("\(#function): change to \(font.rawValue) not allowed.") } + get { + guard _mathTab == nil else { return _mathTab } + //Note: lazy _mathTab initialization is now threadsafe. + mtfontV2LockOnMathTable.lock() + defer { mtfontV2LockOnMathTable.unlock() } + if _mathTab == nil { + _mathTab = MTFontMathTableV2(mathFont: font, size: size, unitsPerEm: unitsPerEm) + } + return _mathTab + } + } + override var rawMathTable: NSDictionary? { + set { fatalError("\(#function): change to \(font.rawValue) not allowed.") } + get { fatalError("\(#function): access to \(font.rawValue) not allowed.") } + } + public override func copy(withSize size: CGFloat) -> MTFont { + MTFontV2(font: font, size: size) + } +} diff --git a/third-party/SwiftMath/Sources/MathBundle/MathFont.swift b/third-party/SwiftMath/Sources/MathBundle/MathFont.swift new file mode 100644 index 0000000000..650d8f6ee7 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathBundle/MathFont.swift @@ -0,0 +1,225 @@ +// +// MathFont.swift +// +// +// Created by Peter Tang on 10/9/2023. +// + +#if os(iOS) || os(visionOS) +import UIKit +#elseif os(macOS) +import AppKit +#endif + +/// Now available for everyone to use +public enum MathFont: String, CaseIterable, Identifiable { + + public var id: Self { self } // Makes things simpler for SwiftUI + + case latinModernFont = "latinmodern-math" + case kpMathLightFont = "KpMath-Light" + case kpMathSansFont = "KpMath-Sans" + case xitsFont = "xits-math" + case termesFont = "texgyretermes-math" + case asanaFont = "Asana-Math" + case eulerFont = "Euler-Math" + case firaFont = "FiraMath-Regular" + case notoSansFont = "NotoSansMath-Regular" + case libertinusFont = "LibertinusMath-Regular" + case garamondFont = "Garamond-Math" + case leteSansFont = "LeteSansMath" + + var fontFamilyName: String { + switch self { + case .latinModernFont: "Latin Modern Math" + case .kpMathLightFont: "KpMath" + case .kpMathSansFont: "KpMath" + case .xitsFont: "XITS Math" + case .termesFont: "TeX Gyre Termes Math" + case .asanaFont: "Asana Math" + case .eulerFont: "Euler Math" + case .firaFont: "Fira Math" + case .notoSansFont: "Noto Sans Math" + case .libertinusFont: "Libertinus Math" + case .garamondFont: "Garamond-Math" // PostScript name is "Garamond-Math", not "Garamond Math" + case .leteSansFont: "Lete Sans Math" + } + } + + var postScriptName: String { + switch self { + case .latinModernFont: "LatinModernMath-Regular" + case .kpMathLightFont: "KpMath-Light" + case .kpMathSansFont: "KpMath-Sans" + case .xitsFont: "XITSMath" + case .termesFont: "TeXGyreTermesMath-Regular" + case .asanaFont: "Asana-Math" + case .eulerFont: "Euler-Math" + case .firaFont: "FiraMath-Regular" + case .notoSansFont: "NotoSansMath-Regular" + case .libertinusFont: "LibertinusMath-Regular" + case .garamondFont: "Garamond-Math" + case .leteSansFont: "LeteSansMath" + } + } + + var fontName: String { self.rawValue } + + public func cgFont() -> CGFont { + BundleManager.manager.obtainCGFont(font: self) + } + public func ctFont(withSize size: CGFloat) -> CTFont { + BundleManager.manager.obtainCTFont(font: self, withSize: size) + } + internal func rawMathTable() -> NSDictionary { + BundleManager.manager.obtainRawMathTable(font: self) + } + + //Note: Below code are no longer supported, unable to tell if UIFont/NSFont is threadsafe, not used in SwiftMath. + // #if os(iOS) || os(visionOS) + // public func uiFont(withSize size: CGFloat) -> UIFont? { + // UIFont(name: fontName, size: size) + // } + // #endif + // #if os(macOS) + // public func nsFont(withSize size: CGFloat) -> NSFont? { + // NSFont(name: fontName, size: size) + // } + // #endif +} +internal extension CTFont { + /** The size of this font in points. */ + var fontSize: CGFloat { + CTFontGetSize(self) + } + var unitsPerEm: UInt { + return UInt(CTFontGetUnitsPerEm(self)) + } +} +private class BundleManager { + //Note: below should be lightweight and without threadsafe problem. + static internal let manager = BundleManager() + + private var cgFonts = [MathFont: CGFont]() + private var ctFonts = [CTFontSizePair: CTFont]() + private var rawMathTables = [MathFont: NSDictionary]() + + private let threadSafeQueue = DispatchQueue(label: "com.smartmath.mathfont.threadsafequeue", + qos: .userInitiated, + attributes: .concurrent) + + private func registerCGFont(mathFont: MathFont) throws { + guard let frameworkBundleURL = Bundle.main.url(forResource: "mathFonts", withExtension: "bundle"), + let resourceBundleURL = Bundle(url: frameworkBundleURL)?.path(forResource: mathFont.rawValue, ofType: "otf") else { + throw FontError.fontPathNotFound + } + guard let fontData = NSData(contentsOfFile: resourceBundleURL), let dataProvider = CGDataProvider(data: fontData) else { + throw FontError.invalidFontFile + } + guard let defaultCGFont = CGFont(dataProvider) else { + throw FontError.initFontError + } + + cgFonts[mathFont] = defaultCGFont + + /// This does not load the complete math font, it only has about half the glyphs of the full math font. + /// In particular it does not have the math italic characters which breaks our variable rendering. + /// So we first load a CGFont from the file and then convert it to a CTFont. + var errorRef: Unmanaged? = nil + guard CTFontManagerRegisterGraphicsFont(defaultCGFont, &errorRef) else { + throw FontError.registerFailed + } + let postsript = (defaultCGFont.postScriptName as? String) ?? "" + let cgfontName = (defaultCGFont.fullName as? String) ?? "" + let threadName = Thread.isMainThread ? "main" : "global" + debugPrint("mathFonts bundle resource: \(mathFont.rawValue), font: \(cgfontName), ps: \(postsript) registered on \(threadName).") + } + + private func registerMathTable(mathFont: MathFont) throws { + guard let frameworkBundleURL = Bundle.main.url(forResource: "mathFonts", withExtension: "bundle"), + let mathTablePlist = Bundle(url: frameworkBundleURL)?.url(forResource: mathFont.rawValue, withExtension:"plist") else { + throw FontError.fontPathNotFound + } + guard let rawMathTable = NSDictionary(contentsOf: mathTablePlist), + let version = rawMathTable["version"] as? String, + version == "1.3" else { + throw FontError.invalidMathTable + } + + rawMathTables[mathFont] = rawMathTable + + let threadName = Thread.isMainThread ? "main" : "global" + debugPrint("mathFonts bundle resource: \(mathFont.rawValue).plist registered on \(threadName).") + } + + private func onDemandRegistration(mathFont: MathFont) { + guard threadSafeQueue.sync(execute: { cgFonts[mathFont] }) == nil else { return } + // Note: resourceLoading is now serialized. + threadSafeQueue.sync(flags: .barrier, execute: { [weak self] in + if self?.cgFonts[mathFont] == nil { + do { + try BundleManager.manager.registerCGFont(mathFont: mathFont) + try BundleManager.manager.registerMathTable(mathFont: mathFont) + + } catch { + fatalError("MTMathFonts:\(#function) ondemand loading failed, mathFont \(mathFont.rawValue), reason \(error)") + } + } + }) + } + fileprivate func obtainCGFont(font: MathFont) -> CGFont { + onDemandRegistration(mathFont: font) + guard let cgFont = threadSafeQueue.sync(execute: { cgFonts[font] }) else { + fatalError("\(#function) unable to locate CGFont \(font.fontName)") + } + return cgFont + } + + fileprivate func obtainCTFont(font: MathFont, withSize size: CGFloat) -> CTFont { + onDemandRegistration(mathFont: font) + let fontSizePair = CTFontSizePair(font: font, size: size) + let ctFont = threadSafeQueue.sync(execute: { ctFonts[fontSizePair] }) + guard ctFont == nil else { return ctFont! } + guard let cgFont = threadSafeQueue.sync(execute: { cgFonts[font] }) else { + fatalError("\(#function) unable to locate CGFont \(font.fontName) to create CTFont") + } + //Note: ctfont creation and caching is now threadsafe. + guard threadSafeQueue.sync(execute: { ctFonts[fontSizePair] }) == nil else { return ctFonts[fontSizePair]! } + return threadSafeQueue.sync(flags: .barrier, execute: { + if let ctfont = ctFonts[fontSizePair] { + return ctfont + } else { + let result = CTFontCreateWithGraphicsFont(cgFont, size, nil, nil) + ctFonts[fontSizePair] = result + return result + } + }) + } + fileprivate func obtainRawMathTable(font: MathFont) -> NSDictionary { + onDemandRegistration(mathFont: font) + guard let mathTable = threadSafeQueue.sync(execute: { rawMathTables[font] } ) else { + fatalError("\(#function) unable to locate mathTable: \(font.rawValue).plist") + } + return mathTable + } + deinit { + ctFonts.removeAll() + var errorRef: Unmanaged? = nil + cgFonts.values.forEach { cgFont in + CTFontManagerUnregisterGraphicsFont(cgFont, &errorRef) + } + cgFonts.removeAll() + } + public enum FontError: Error { + case invalidFontFile + case fontPathNotFound + case initFontError + case registerFailed + case invalidMathTable + } + + private struct CTFontSizePair: Hashable { + let font: MathFont + let size: CGFloat + } +} diff --git a/third-party/SwiftMath/Sources/MathBundle/MathImage.swift b/third-party/SwiftMath/Sources/MathBundle/MathImage.swift new file mode 100755 index 0000000000..89a06cb5eb --- /dev/null +++ b/third-party/SwiftMath/Sources/MathBundle/MathImage.swift @@ -0,0 +1,123 @@ +// +// MathImage.swift +// +// +// Created by Peter Tang on 15/9/2023. +// + +import Foundation + +#if os(iOS) || os(visionOS) + import UIKit +#elseif os(macOS) + import AppKit +#endif + +public struct MathImage { + public var font: MathFont = .latinModernFont + public var fontSize: CGFloat + public var textColor: MTColor + + public var labelMode: MTMathUILabelMode + public var textAlignment: MTTextAlignment + + public var contentInsets: MTEdgeInsets = MTEdgeInsetsZero + + public let latex: String + + private(set) var intrinsicContentSize = CGSize.zero + + public init(latex: String, fontSize: CGFloat, textColor: MTColor, labelMode: MTMathUILabelMode = .display, textAlignment: MTTextAlignment = .center) { + self.latex = latex + self.fontSize = fontSize + self.textColor = textColor + self.labelMode = labelMode + self.textAlignment = textAlignment + } +} +extension MathImage { + public var currentStyle: MTLineStyle { + switch labelMode { + case .display: return .display + case .text: return .text + } + } + private func intrinsicContentSize(_ displayList: MTMathListDisplay) -> CGSize { + CGSize(width: displayList.width + contentInsets.left + contentInsets.right, + height: displayList.ascent + displayList.descent + contentInsets.top + contentInsets.bottom) + } + public struct LayoutInfo { + public var ascent: CGFloat = 0 + public var descent: CGFloat = 0 + + public init(ascent: CGFloat, descent: CGFloat) { + self.ascent = ascent + self.descent = descent + } + } + public mutating func asImage() -> (NSError?, MTImage?, LayoutInfo?) { + func layoutImage(size: CGSize, displayList: MTMathListDisplay) { + var textX = CGFloat(0) + switch self.textAlignment { + case .left: textX = contentInsets.left + case .center: textX = (size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left + case .right: textX = size.width - displayList.width - contentInsets.right + } + let availableHeight = size.height - contentInsets.bottom - contentInsets.top + + // center things vertically + var height = displayList.ascent + displayList.descent + if height < fontSize/2 { + height = fontSize/2 // set height to half the font size + } + let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom + displayList.position = CGPoint(x: textX, y: textY) + } + var error: NSError? + let mtfont: MTFont? = font.mtfont(size: fontSize) + + guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil, + let displayList = MTTypesetter.createLineForMathList(mathList, font: mtfont, style: currentStyle) else { + return (error, nil, nil) + } + + intrinsicContentSize = intrinsicContentSize(displayList) + displayList.textColor = textColor + + let size = intrinsicContentSize.regularized + layoutImage(size: size, displayList: displayList) + + #if os(iOS) || os(visionOS) + let renderer = UIGraphicsImageRenderer(size: size) + let image = renderer.image { rendererContext in + rendererContext.cgContext.saveGState() + rendererContext.cgContext.concatenate(.flippedVertically(size.height)) + displayList.draw(rendererContext.cgContext) + rendererContext.cgContext.restoreGState() + } + return (nil, image, LayoutInfo(ascent: displayList.ascent, descent: displayList.descent)) + #endif + #if os(macOS) + let image = NSImage(size: size, flipped: false) { bounds in + guard let context = NSGraphicsContext.current?.cgContext else { return false } + context.saveGState() + displayList.draw(context) + context.restoreGState() + return true + } + return (nil, image, LayoutInfo(ascent: displayList.ascent, descent: displayList.descent)) + #endif + } +} +private extension CGAffineTransform { + static func flippedVertically(_ height: CGFloat) -> CGAffineTransform { + var transform = CGAffineTransform(scaleX: 1, y: -1) + transform = transform.translatedBy(x: 0, y: -height) + return transform + } +} +extension CGSize { + fileprivate var regularized: CGSize { + CGSize(width: ceil(width), height: ceil(height)) + } +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTBezierPath.swift b/third-party/SwiftMath/Sources/MathRender/MTBezierPath.swift new file mode 100755 index 0000000000..fb5e082a89 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTBezierPath.swift @@ -0,0 +1,35 @@ + +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by 安志钢. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation + +#if os(macOS) + +extension MTBezierPath { + func addLine(to point: CGPoint) { + self.line(to: point) + } +} + +extension MTView { + + var backgroundColor:MTColor? { + get { + MTColor(cgColor: self.layer?.backgroundColor ?? MTColor.clear.cgColor) + } + set { + self.layer?.backgroundColor = MTColor.clear.cgColor + self.wantsLayer = true + } + } + +} + +#endif + diff --git a/third-party/SwiftMath/Sources/MathRender/MTColor.swift b/third-party/SwiftMath/Sources/MathRender/MTColor.swift new file mode 100755 index 0000000000..88f8a0d24a --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTColor.swift @@ -0,0 +1,28 @@ + +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Markus Sähn. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation + +extension MTColor { + + public convenience init?(fromHexString hexString:String) { + if hexString.isEmpty { return nil } + if !hexString.hasPrefix("#") { return nil } + + var rgbValue = UInt64(0) + let scanner = Scanner(string: hexString) + scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#") + scanner.scanHexInt64(&rgbValue) + self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16)/255.0, + green: CGFloat((rgbValue & 0xFF00) >> 8)/255.0, + blue: CGFloat((rgbValue & 0xFF))/255.0, + alpha: 1.0) + } + +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTConfig.swift b/third-party/SwiftMath/Sources/MathRender/MTConfig.swift new file mode 100755 index 0000000000..19f339bf69 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTConfig.swift @@ -0,0 +1,40 @@ +import Foundation + +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by 安志钢. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +#if os(iOS) || os(visionOS) + +import UIKit + +public typealias MTView = UIView +public typealias MTColor = UIColor +public typealias MTBezierPath = UIBezierPath +public typealias MTLabel = UILabel +public typealias MTEdgeInsets = UIEdgeInsets +public typealias MTRect = CGRect +public typealias MTImage = UIImage + +let MTEdgeInsetsZero = UIEdgeInsets.zero +func MTGraphicsGetCurrentContext() -> CGContext? { UIGraphicsGetCurrentContext() } + +#else + +import AppKit + +public typealias MTView = NSView +public typealias MTColor = NSColor +public typealias MTBezierPath = NSBezierPath +public typealias MTEdgeInsets = NSEdgeInsets +public typealias MTRect = NSRect +public typealias MTImage = NSImage + +let MTEdgeInsetsZero = NSEdgeInsets.init(top: 0, left: 0, bottom: 0, right: 0) +func MTGraphicsGetCurrentContext() -> CGContext? { NSGraphicsContext.current?.cgContext } + +#endif diff --git a/third-party/SwiftMath/Sources/MathRender/MTFont.swift b/third-party/SwiftMath/Sources/MathRender/MTFont.swift new file mode 100644 index 0000000000..e1e418d821 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTFont.swift @@ -0,0 +1,81 @@ + +import Foundation +import CoreText +import AppBundle + +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Kostub Deshmukh. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +public class MTFont { + + var defaultCGFont: CGFont! + var ctFont: CTFont! + var mathTable: MTFontMathTable? + var rawMathTable: NSDictionary? + + /// Fallback font for characters not supported by the main math font. + /// Defaults to the system font at the same size. This is particularly useful + /// for rendering text in \text{} commands with characters outside the math font's coverage + /// (e.g., Chinese, Japanese, Korean, emoji, etc.) + public var fallbackFont: CTFont? + + init() {} + + /// `MTFont(fontWithName:)` does not load the complete math font, it only has about half the glyphs of the full math font. + /// In particular it does not have the math italic characters which breaks our variable rendering. + /// So we first load a CGFont from the file and then convert it to a CTFont. + convenience init(fontWithName name: String, size:CGFloat) { + self.init() + //print("Loading font \(name)") + let bundle = getAppBundle() + let fontPath = bundle.path(forResource: name, ofType: "otf") + let fontDataProvider = CGDataProvider(filename: fontPath!) + self.defaultCGFont = CGFont(fontDataProvider!)! + //print("Num glyphs: \(self.defaultCGFont.numberOfGlyphs)") + + self.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil); + + //print("Loading associated .plist") + let mathTablePlist = bundle.url(forResource:name, withExtension:"plist") + self.rawMathTable = NSDictionary(contentsOf: mathTablePlist!) + self.mathTable = MTFontMathTable(withFont:self, mathTable:rawMathTable!) + } + + static var fontBundle:Bundle { + // Uses bundle for class so that this can be access by the unit tests. + Bundle(url: Bundle.main.url(forResource: "mathFonts", withExtension: "bundle")!)! + } + + /** Returns a copy of this font but with a different size. */ + public func copy(withSize size: CGFloat) -> MTFont { + let newFont = MTFont() + newFont.defaultCGFont = self.defaultCGFont + newFont.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil) + newFont.rawMathTable = self.rawMathTable + newFont.mathTable = MTFontMathTable(withFont: newFont, mathTable: newFont.rawMathTable!) + return newFont + } + + func get(nameForGlyph glyph:CGGlyph) -> String { + let name = defaultCGFont.name(for: glyph) as? String + return name ?? "" + } + + func get(glyphWithName name:String) -> CGGlyph { + defaultCGFont.getGlyphWithGlyphName(name: name as CFString) + } + + /** The size of this font in points. */ + public var fontSize:CGFloat { CTFontGetSize(self.ctFont) } + + deinit { + self.ctFont = nil + self.defaultCGFont = nil + } + +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTFontManager.swift b/third-party/SwiftMath/Sources/MathRender/MTFontManager.swift new file mode 100755 index 0000000000..71f42cc477 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTFontManager.swift @@ -0,0 +1,93 @@ + +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Kostub Deshmukh. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation + +public class MTFontManager { + + static public private(set) var manager: MTFontManager = { + MTFontManager() + }() + + let kDefaultFontSize = CGFloat(20) + + static var fontManager : MTFontManager { + return manager + } + + public init() { } + + @RWLocked + var nameToFontMap = [String: MTFont]() + + public func font(withName name:String, size:CGFloat) -> MTFont? { + var f = self.nameToFontMap[name] + if f == nil { + f = MTFont(fontWithName: name, size: size) + self.nameToFontMap[name] = f + } + + if f!.fontSize == size { return f } + else { return f!.copy(withSize: size) } + } + + public func latinModernFont(withSize size:CGFloat) -> MTFont? { + MTFontManager.fontManager.font(withName: "latinmodern-math", size: size) + } + + public func kpMathLightFont(withSize size:CGFloat) -> MTFont? { + MTFontManager.fontManager.font(withName: "KpMath-Light", size: size) + } + + public func kpMathSansFont(withSize size:CGFloat) -> MTFont? { + MTFontManager.fontManager.font(withName: "KpMath-Sans", size: size) + } + + public func xitsFont(withSize size:CGFloat) -> MTFont? { + MTFontManager.fontManager.font(withName: "xits-math", size: size) + } + + public func termesFont(withSize size:CGFloat) -> MTFont? { + MTFontManager.fontManager.font(withName: "texgyretermes-math", size: size) + } + + public func asanaFont(withSize size:CGFloat) -> MTFont? { + MTFontManager.fontManager.font(withName: "Asana-Math", size: size) + } + + public func eulerFont(withSize size:CGFloat) -> MTFont? { + MTFontManager.fontManager.font(withName: "Euler-Math", size: size) + } + + public func firaRegularFont(withSize size:CGFloat) -> MTFont? { + MTFontManager.fontManager.font(withName: "FiraMath-Regular", size: size) + } + + public func notoSansRegularFont(withSize size:CGFloat) -> MTFont? { + MTFontManager.fontManager.font(withName: "NotoSansMath-Regular", size: size) + } + + public func libertinusRegularFont(withSize size:CGFloat) -> MTFont? { + MTFontManager.fontManager.font(withName: "LibertinusMath-Regular", size: size) + } + + public func garamondMathFont(withSize size:CGFloat) -> MTFont? { + MTFontManager.fontManager.font(withName: "Garamond-Math", size: size) + } + + public func leteSansFont(withSize size:CGFloat) -> MTFont? { + MTFontManager.fontManager.font(withName: "LeteSansMath", size: size) + } + + public var defaultFont: MTFont? { + MTFontManager.fontManager.latinModernFont(withSize: kDefaultFontSize) + } + + +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTFontMathTable.swift b/third-party/SwiftMath/Sources/MathRender/MTFontMathTable.swift new file mode 100755 index 0000000000..35e252b1ad --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTFontMathTable.swift @@ -0,0 +1,355 @@ +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Kostub Deshmukh. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation +import CoreText + +struct GlyphPart { + /// The glyph that represents this part + var glyph: CGGlyph! + + /// Full advance width/height for this part, in the direction of the extension in points. + var fullAdvance: CGFloat = 0 + + /// Advance width/ height of the straight bar connector material at the beginning of the glyph in points. + var startConnectorLength: CGFloat = 0 + + /// Advance width/ height of the straight bar connector material at the end of the glyph in points. + var endConnectorLength: CGFloat = 0 + + /// If this part is an extender. If set, the part can be skipped or repeated. + var isExtender: Bool = false +} + +/** This class represents the Math table of an open type font. + + The math table is documented here: https://www.microsoft.com/typography/otspec/math.htm + + How the constants in this class affect the display is documented here: + http://www.tug.org/TUGboat/tb30-1/tb94vieth.pdf + + Note: We don't parse the math table from the open type font. Rather we parse it + in python and convert it to a .plist file which is easily consumed by this class. + This approach is preferable to spending an inordinate amount of time figuring out + how to parse the returned NSData object using the open type rules. + + Remark: This class is not meant to be used outside of this library. + */ +class MTFontMathTable { + + // The font for this math table. + public private(set) weak var font:MTFont? // @property (nonatomic, readonly, weak) MTFont* font; + + var _unitsPerEm: UInt + var _fontSize: CGFloat + var _mathTable: NSDictionary! + + let kConstants = "constants" + + /** MU unit in points */ + var muUnit:CGFloat { _fontSize/18 } + + func fontUnitsToPt(_ fontUnits:Int) -> CGFloat { + CGFloat(fontUnits) * _fontSize / CGFloat(_unitsPerEm) + } + + init(withFont font: MTFont?, mathTable:NSDictionary) { + assert(font != nil, "font has nil value") + assert(font!.ctFont != nil, "font.ctFont has nil value") + self.font = font + // do domething with font + _unitsPerEm = UInt(CTFontGetUnitsPerEm(font!.ctFont)) + _fontSize = font!.fontSize; + _mathTable = mathTable + let version = _mathTable["version"] as! String + if version != "1.3" { + NSException(name: NSExceptionName.internalInconsistencyException, reason: "Invalid version of math table plist: \(version)").raise() + } + } + + func constantFromTable(_ constName:String) -> CGFloat { + let consts = _mathTable[kConstants] as! NSDictionary? + let val = consts![constName] as! NSNumber? + return fontUnitsToPt(val!.intValue) + } + + func percentFromTable(_ percentName:String) -> CGFloat { + let consts = _mathTable[kConstants] as! NSDictionary? + let val = consts![percentName] as! NSNumber? + return CGFloat(val!.floatValue) / 100 + } + + /// Math Font Metrics from the opentype specification + // MARK: - Fractions + var fractionNumeratorDisplayStyleShiftUp:CGFloat { constantFromTable("FractionNumeratorDisplayStyleShiftUp") } // \sigma_8 in TeX + var fractionNumeratorShiftUp:CGFloat { constantFromTable("FractionNumeratorShiftUp") } // \sigma_9 in TeX + var fractionDenominatorDisplayStyleShiftDown:CGFloat { constantFromTable("FractionDenominatorDisplayStyleShiftDown") } // \sigma_11 in TeX + var fractionDenominatorShiftDown:CGFloat { constantFromTable("FractionDenominatorShiftDown") } // \sigma_12 in TeX + var fractionNumeratorDisplayStyleGapMin:CGFloat { constantFromTable("FractionNumDisplayStyleGapMin") } // 3 * \xi_8 in TeX + var fractionNumeratorGapMin:CGFloat { constantFromTable("FractionNumeratorGapMin") } // \xi_8 in TeX + var fractionDenominatorDisplayStyleGapMin:CGFloat { constantFromTable("FractionDenomDisplayStyleGapMin") } // 3 * \xi_8 in TeX + var fractionDenominatorGapMin:CGFloat { constantFromTable("FractionDenominatorGapMin") } // \xi_8 in TeX + var fractionRuleThickness:CGFloat { constantFromTable("FractionRuleThickness") } // \xi_8 in TeX + var skewedFractionHorizonalGap:CGFloat { constantFromTable("SkewedFractionHorizontalGap") } // \sigma_20 in TeX + var skewedFractionVerticalGap:CGFloat { constantFromTable("SkewedFractionVerticalGap") } // \sigma_21 in TeX + + // MARK: - Non-standard + /// FractionDelimiterSize and FractionDelimiterDisplayStyleSize are not constants + /// specified in the OpenType Math specification. Rather these are proposed LuaTeX extensions + /// for the TeX parameters \sigma_20 (delim1) and \sigma_21 (delim2). Since these do not + /// exist in the fonts that we have, we use the same approach as LuaTeX and use the fontSize + /// to determine these values. The constants used match the metrics values of the original TeX fonts. + /// Note: An alternative approach is to use DelimitedSubFormulaMinHeight for \sigma21 and use a factor + /// of 2 to get \sigma 20 as proposed in Vieth paper. + /// The XeTeX implementation sets \sigma21 = fontSize and \sigma20 = DelimitedSubFormulaMinHeight which + /// will produce smaller delimiters. + /// Of all the approaches we've implemented LuaTeX's approach since it mimics LaTeX most accurately. + var fractionDelimiterSize: CGFloat { 1.01 * _fontSize } + + /// Modified constant from 2.4 to 2.39 for better visual appearance. + var fractionDelimiterDisplayStyleSize: CGFloat { 2.39 * _fontSize } + + // MARK: - Stacks + var stackTopDisplayStyleShiftUp:CGFloat { constantFromTable("StackTopDisplayStyleShiftUp") } // \sigma_8 in TeX + var stackTopShiftUp:CGFloat { constantFromTable("StackTopShiftUp") } // \sigma_10 in TeX + var stackDisplayStyleGapMin:CGFloat { constantFromTable("StackDisplayStyleGapMin") } // 7 \xi_8 in TeX + var stackGapMin:CGFloat { constantFromTable("StackGapMin") } // 3 \xi_8 in TeX + var stackBottomDisplayStyleShiftDown:CGFloat { constantFromTable("StackBottomDisplayStyleShiftDown") } // \sigma_11 in TeX + var stackBottomShiftDown:CGFloat { constantFromTable("StackBottomShiftDown") } // \sigma_12 in TeX + + var stretchStackBottomShiftDown:CGFloat { constantFromTable("StretchStackBottomShiftDown") } + var stretchStackGapAboveMin:CGFloat { constantFromTable("StretchStackGapAboveMin") } + var stretchStackGapBelowMin:CGFloat { constantFromTable("StretchStackGapBelowMin") } + var stretchStackTopShiftUp:CGFloat { constantFromTable("StretchStackTopShiftUp") } + + // MARK: - super/sub scripts + + var superscriptShiftUp:CGFloat { constantFromTable("SuperscriptShiftUp") } // \sigma_13, \sigma_14 in TeX + var superscriptShiftUpCramped:CGFloat { constantFromTable("SuperscriptShiftUpCramped") } // \sigma_15 in TeX + var subscriptShiftDown:CGFloat { constantFromTable("SubscriptShiftDown") } // \sigma_16, \sigma_17 in TeX + var superscriptBaselineDropMax:CGFloat { constantFromTable("SuperscriptBaselineDropMax") } // \sigma_18 in TeX + var subscriptBaselineDropMin:CGFloat { constantFromTable("SubscriptBaselineDropMin") } // \sigma_19 in TeX + var superscriptBottomMin:CGFloat { constantFromTable("SuperscriptBottomMin") } // 1/4 \sigma_5 in TeX + var subscriptTopMax:CGFloat { constantFromTable("SubscriptTopMax") } // 4/5 \sigma_5 in TeX + var subSuperscriptGapMin:CGFloat { constantFromTable("SubSuperscriptGapMin") } // 4 \xi_8 in TeX + var superscriptBottomMaxWithSubscript:CGFloat { constantFromTable("SuperscriptBottomMaxWithSubscript") } // 4/5 \sigma_5 in TeX + + var spaceAfterScript:CGFloat { constantFromTable("SpaceAfterScript") } + + // MARK: - radicals + var radicalExtraAscender:CGFloat { constantFromTable("RadicalExtraAscender") } // \xi_8 in Tex + var radicalRuleThickness:CGFloat { constantFromTable("RadicalRuleThickness") } // \xi_8 in Tex + var radicalDisplayStyleVerticalGap:CGFloat { constantFromTable("RadicalDisplayStyleVerticalGap") } // \xi_8 + 1/4 \sigma_5 in Tex + var radicalVerticalGap:CGFloat { constantFromTable("RadicalVerticalGap") } // 5/4 \xi_8 in Tex + var radicalKernBeforeDegree:CGFloat { constantFromTable("RadicalKernBeforeDegree") } // 5 mu in Tex + var radicalKernAfterDegree:CGFloat { constantFromTable("RadicalKernAfterDegree") } // -10 mu in Tex + var radicalDegreeBottomRaisePercent:CGFloat { percentFromTable("RadicalDegreeBottomRaisePercent") } // 60% in Tex + + // MARK: - Limits + var upperLimitBaselineRiseMin:CGFloat { constantFromTable("UpperLimitBaselineRiseMin") } // \xi_11 in TeX + var upperLimitGapMin:CGFloat { constantFromTable("UpperLimitGapMin") } // \xi_9 in TeX + var lowerLimitGapMin:CGFloat { constantFromTable("LowerLimitGapMin") } // \xi_10 in TeX + var lowerLimitBaselineDropMin:CGFloat { constantFromTable("LowerLimitBaselineDropMin") } // \xi_12 in TeX + var limitExtraAscenderDescender:CGFloat { 0 } // \xi_13 in TeX, not present in OpenType so we always set it to 0. + + // MARK: - Underline + var underbarVerticalGap:CGFloat { constantFromTable("UnderbarVerticalGap") } // 3 \xi_8 in TeX + var underbarRuleThickness:CGFloat { constantFromTable("UnderbarRuleThickness") } // \xi_8 in TeX + var underbarExtraDescender:CGFloat { constantFromTable("UnderbarExtraDescender") } // \xi_8 in TeX + + // MARK: - Overline + var overbarVerticalGap:CGFloat { constantFromTable("OverbarVerticalGap") } // 3 \xi_8 in TeX + var overbarRuleThickness:CGFloat { constantFromTable("OverbarRuleThickness") } // \xi_8 in TeX + var overbarExtraAscender:CGFloat { constantFromTable("OverbarExtraAscender") } // \xi_8 in TeX + + // MARK: - Constants + + var axisHeight:CGFloat { constantFromTable("AxisHeight") } // \sigma_22 in TeX + var scriptScaleDown:CGFloat { percentFromTable("ScriptPercentScaleDown") } + var scriptScriptScaleDown:CGFloat { percentFromTable("ScriptScriptPercentScaleDown") } + var mathLeading:CGFloat { constantFromTable("MathLeading") } + var delimitedSubFormulaMinHeight:CGFloat { constantFromTable("DelimitedSubFormulaMinHeight") } + + // MARK: - Accent + + var accentBaseHeight:CGFloat { constantFromTable("AccentBaseHeight") } // \fontdimen5 in TeX (x-height) + var flattenedAccentBaseHeight:CGFloat { constantFromTable("FlattenedAccentBaseHeight") } + + // MARK: - Variants + + let kVertVariants = "v_variants" + let kHorizVariants = "h_variants" + + /** Returns an Array of all the vertical variants of the glyph if any. If + there are no variants for the glyph, the array contains the given glyph. */ + func getVerticalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] { + let variants = _mathTable[kVertVariants] as! NSDictionary? + return self.getVariantsForGlyph(glyph, inDictionary: variants!) + } + + /** Returns an Array of all the horizontal variants of the glyph if any. If + there are no variants for the glyph, the array contains the given glyph. */ + func getHorizontalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] { + let variants = _mathTable[kHorizVariants] as! NSDictionary + return self.getVariantsForGlyph(glyph, inDictionary:variants) + } + + func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants:NSDictionary) -> [NSNumber?] { + let glyphName = self.font!.get(nameForGlyph: glyph) + let variantGlyphs = variants[glyphName] as! NSArray? + var glyphArray = [NSNumber]() + if variantGlyphs == nil || variantGlyphs?.count == 0 { + // There are no extra variants, so just add the current glyph to it. + let glyph = self.font!.get(glyphWithName: glyphName) + glyphArray.append(NSNumber(value:glyph)) + return glyphArray + } + for gvn in variantGlyphs! { + let glyphVariantName = gvn as! String? + let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!) + glyphArray.append(NSNumber(value:variantGlyph!)) + } + return glyphArray + } + + /** Returns a larger vertical variant of the given glyph if any. + If there is no larger version, this returns the current glyph. + + - Parameter glyph: The glyph to find a larger variant for + - Parameter forDisplayStyle: If true, selects the largest appropriate variant for display style. + If false, selects the next larger variant (incremental sizing). + - Returns: A larger glyph variant, or the original glyph if no variants exist + */ + func getLargerGlyph(_ glyph:CGGlyph, forDisplayStyle: Bool = false) -> CGGlyph { + let variants = _mathTable[kVertVariants] as! NSDictionary? + let glyphName = self.font?.get(nameForGlyph: glyph) + let variantGlyphs = variants![glyphName!] as! NSArray? + if variantGlyphs == nil || variantGlyphs?.count == 0 { + // There are no extra variants, so just returnt the current glyph. + return glyph + } + + if forDisplayStyle { + // For display style, select a large variant suitable for mathematical display mode + // Display integrals should be significantly larger (~2.2em) for visual prominence + let count = variantGlyphs!.count + + // Strategy: Use the largest variant, but avoid extreme sizes for fonts with many variants + let targetIndex: Int + if count <= 2 { + // Small variant list: use the last one (e.g., integral.size1 at ~2.2em) + targetIndex = count - 1 + } else if count <= 4 { + // Medium variant list: use second-to-last to avoid extremes + targetIndex = count - 2 + } else { + // Large variant list (like texgyretermes with 6 variants): + // Use variant at ~60% position to get appropriate display size (~2.0em) + // For 7 variants (0-6), this gives index 4 + targetIndex = min(count - 2, Int(Double(count) * 0.6)) + } + + if let glyphVariantName = variantGlyphs![targetIndex] as? String { + let variantGlyph = self.font?.get(glyphWithName: glyphVariantName) + return variantGlyph! + } + } else { + // Text/inline style: use incremental sizing for moderate enlargement + // Find the first variant with a different name + for gvn in variantGlyphs! { + let glyphVariantName = gvn as! String? + if glyphVariantName != glyphName { + let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!) + return variantGlyph! + } + } + } + + // We did not find any variants of this glyph so return it. + return glyph; + } + + // MARK: - Italic Correction + + let kItalic = "italic" + + /** Returns the italic correction for the given glyph if any. If there + isn't any this returns 0. */ + func getItalicCorrection(_ glyph: CGGlyph) -> CGFloat { + let italics = _mathTable[kItalic] as! NSDictionary? + let glyphName = self.font?.get(nameForGlyph: glyph) + let val = italics![glyphName!] as! NSNumber? + // if val is nil, this returns 0. + return self.fontUnitsToPt(val?.intValue ?? 0) + } + + // MARK: - Accents + + let kAccents = "accents" + + /** Returns the adjustment to the top accent for the given glyph if any. + If there isn't any this returns -1. */ + func getTopAccentAdjustment(_ glyph: CGGlyph) -> CGFloat { + var glyph = glyph + let accents = _mathTable[kAccents] as! NSDictionary? + let glyphName = self.font?.get(nameForGlyph: glyph) + let val = accents![glyphName!] as! NSNumber? + if let val = val { + return self.fontUnitsToPt(val.intValue) + } else { + // If no top accent is defined then it is the center of the advance width. + var advances = CGSize.zero + CTFontGetAdvancesForGlyphs(self.font!.ctFont, .horizontal, &glyph, &advances, 1) + return advances.width/2 + } + } + + // MARK: - Glyph Construction + + /** Minimum overlap of connecting glyphs during glyph construction */ + var minConnectorOverlap:CGFloat { constantFromTable("MinConnectorOverlap") } + + let kVertAssembly = "v_assembly" + let kAssemblyParts = "parts" + + /** Returns an array of the glyph parts to be used for constructing vertical variants + of this glyph. If there is no glyph assembly defined, returns an empty array. */ + func getVerticalGlyphAssembly(forGlyph glyph:CGGlyph) -> [GlyphPart] { + let assemblyTable = _mathTable[kVertAssembly] as! NSDictionary? + let glyphName = self.font?.get(nameForGlyph: glyph) + let assemblyInfo = assemblyTable![glyphName!] as! NSDictionary? + if assemblyInfo == nil { + // No vertical assembly defined for glyph + return [] + } + let parts = assemblyInfo![kAssemblyParts] as! NSArray? + if parts == nil { + // parts should always have been defined, but if it isn't return nil + return [] + } + var rv = [GlyphPart]() + for part in parts! { + let partInfo = part as! NSDictionary? + var part = GlyphPart() + let adv = partInfo!["advance"] as! NSNumber? + part.fullAdvance = self.fontUnitsToPt(adv!.intValue) + let end = partInfo!["endConnector"] as! NSNumber? + part.endConnectorLength = self.fontUnitsToPt(end!.intValue) + let start = partInfo!["startConnector"] as! NSNumber? + part.startConnectorLength = self.fontUnitsToPt(start!.intValue) + let ext = partInfo!["extender"] as! NSNumber? + part.isExtender = ext!.boolValue + let glyphName = partInfo!["glyph"] as! String? + part.glyph = self.font?.get(glyphWithName: glyphName!) + rv.append(part) + } + return rv + } + + +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTLabel.swift b/third-party/SwiftMath/Sources/MathRender/MTLabel.swift new file mode 100755 index 0000000000..8964a9027b --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTLabel.swift @@ -0,0 +1,37 @@ +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by 安志钢. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation +import SwiftUI + +#if os(macOS) + +public class MTLabel : NSTextField { + + init() { + super.init(frame: .zero) + self.stringValue = "" + self.isBezeled = false + self.drawsBackground = false + self.isEditable = false + self.isSelectable = false + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + // MARK: - Customized getter and setter methods for property text. + var text:String? { + get { super.stringValue } + set { super.stringValue = newValue! } + } + +} + +#endif diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathAtomFactory.swift b/third-party/SwiftMath/Sources/MathRender/MTMathAtomFactory.swift new file mode 100644 index 0000000000..8d35a93dc3 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTMathAtomFactory.swift @@ -0,0 +1,1141 @@ +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Kostub Deshmukh. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation + +/** A factory to create commonly used MTMathAtoms. */ +public class MTMathAtomFactory { + + public static let aliases = [ + "lnot" : "neg", + "land" : "wedge", + "lor" : "vee", + "ne" : "neq", + "le" : "leq", + "ge" : "geq", + "lbrace" : "{", + "rbrace" : "}", + "Vert" : "|", + "gets" : "leftarrow", + "to" : "rightarrow", + "iff" : "Longleftrightarrow", + "AA" : "angstrom" + ] + + public static let delimiters = [ + "." : "", // . means no delimiter + "(" : "(", + ")" : ")", + "[" : "[", + "]" : "]", + "<" : "\u{2329}", + ">" : "\u{232A}", + "/" : "/", + "\\" : "\\", + "|" : "|", + "lgroup" : "\u{27EE}", + "rgroup" : "\u{27EF}", + "||" : "\u{2016}", + "Vert" : "\u{2016}", + "vert" : "|", + "uparrow" : "\u{2191}", + "downarrow" : "\u{2193}", + "updownarrow" : "\u{2195}", + "Uparrow" : "\u{21D1}", + "Downarrow" : "\u{21D3}", + "Updownarrow" : "\u{21D5}", + "backslash" : "\\", + "rangle" : "\u{232A}", + "langle" : "\u{2329}", + "rbrace" : "}", + "}" : "}", + "{" : "{", + "lbrace" : "{", + "lceil" : "\u{2308}", + "rceil" : "\u{2309}", + "lfloor" : "\u{230A}", + "rfloor" : "\u{230B}", + // Corner brackets (amssymb) + "ulcorner" : "\u{231C}", // upper left corner + "urcorner" : "\u{231D}", // upper right corner + "llcorner" : "\u{231E}", // lower left corner + "lrcorner" : "\u{231F}", // lower right corner + // Double square brackets (strachey brackets) + "llbracket" : "\u{27E6}", // left double bracket + "rrbracket" : "\u{27E7}", // right double bracket + ] + + private static let delimValueLock = NSLock() + static var _delimValueToName = [String: String]() + public static var delimValueToName: [String: String] { + if _delimValueToName.isEmpty { + var output = [String: String]() + for (key, value) in Self.delimiters { + if let existingValue = output[value] { + if key.count > existingValue.count { + continue + } else if key.count == existingValue.count { + if key.compare(existingValue) == .orderedDescending { + continue + } + } + } + output[value] = key + } + // protect lazily loading table in a multi-thread concurrent environment + delimValueLock.lock() + defer { delimValueLock.unlock() } + if _delimValueToName.isEmpty { + _delimValueToName = output + } + } + return _delimValueToName + } + + public static let accents = [ + "grave" : "\u{0300}", + "acute" : "\u{0301}", + "hat" : "\u{0302}", // In our implementation hat and widehat behave the same. + "tilde" : "\u{0303}", // In our implementation tilde and widetilde behave the same. + "bar" : "\u{0304}", + "breve" : "\u{0306}", + "dot" : "\u{0307}", + "ddot" : "\u{0308}", + "check" : "\u{030C}", + "vec" : "\u{20D7}", + "widehat" : "\u{0302}", + "widetilde" : "\u{0303}", + "overleftarrow" : "\u{20D6}", // Combining left arrow above + "overrightarrow" : "\u{20D7}", // Combining right arrow above (same as vec) + "overleftrightarrow" : "\u{20E1}" // Combining left right arrow above + ] + + private static let accentValueLock = NSLock() + static var _accentValueToName: [String: String]? = nil + public static var accentValueToName: [String: String] { + if _accentValueToName == nil { + var output = [String: String]() + + for (key, value) in Self.accents { + if let existingValue = output[value] { + if key.count > existingValue.count { + continue + } else if key.count == existingValue.count { + if key.compare(existingValue) == .orderedDescending { + continue + } + } + } + output[value] = key + } + // protect lazily loading table in a multi-thread concurrent environment + accentValueLock.lock() + defer { accentValueLock.unlock() } + if _accentValueToName == nil { + _accentValueToName = output + } + } + return _accentValueToName! + } + + static var supportedLatexSymbolNames:[String] { + let commands = MTMathAtomFactory.supportedLatexSymbols + return commands.keys.map { String($0) } + } + + static var supportedLatexSymbols: [String: MTMathAtom] = [ + "square" : MTMathAtomFactory.placeholder(), + + // Greek characters + "alpha" : MTMathAtom(type: .variable, value: "\u{03B1}"), + "beta" : MTMathAtom(type: .variable, value: "\u{03B2}"), + "gamma" : MTMathAtom(type: .variable, value: "\u{03B3}"), + "delta" : MTMathAtom(type: .variable, value: "\u{03B4}"), + "varepsilon" : MTMathAtom(type: .variable, value: "\u{03B5}"), + "zeta" : MTMathAtom(type: .variable, value: "\u{03B6}"), + "eta" : MTMathAtom(type: .variable, value: "\u{03B7}"), + "theta" : MTMathAtom(type: .variable, value: "\u{03B8}"), + "iota" : MTMathAtom(type: .variable, value: "\u{03B9}"), + "kappa" : MTMathAtom(type: .variable, value: "\u{03BA}"), + "lambda" : MTMathAtom(type: .variable, value: "\u{03BB}"), + "mu" : MTMathAtom(type: .variable, value: "\u{03BC}"), + "nu" : MTMathAtom(type: .variable, value: "\u{03BD}"), + "xi" : MTMathAtom(type: .variable, value: "\u{03BE}"), + "omicron" : MTMathAtom(type: .variable, value: "\u{03BF}"), + "pi" : MTMathAtom(type: .variable, value: "\u{03C0}"), + "rho" : MTMathAtom(type: .variable, value: "\u{03C1}"), + "varsigma" : MTMathAtom(type: .variable, value: "\u{03C2}"), + "sigma" : MTMathAtom(type: .variable, value: "\u{03C3}"), + "tau" : MTMathAtom(type: .variable, value: "\u{03C4}"), + "upsilon" : MTMathAtom(type: .variable, value: "\u{03C5}"), + "varphi" : MTMathAtom(type: .variable, value: "\u{03C6}"), + "chi" : MTMathAtom(type: .variable, value: "\u{03C7}"), + "psi" : MTMathAtom(type: .variable, value: "\u{03C8}"), + "omega" : MTMathAtom(type: .variable, value: "\u{03C9}"), + // We mark the following greek chars as ordinary so that we don't try + // to automatically italicize them as we do with variables. + // These characters fall outside the rules of italicization that we have defined. + "epsilon" : MTMathAtom(type: .ordinary, value: "\u{0001D716}"), + "vartheta" : MTMathAtom(type: .ordinary, value: "\u{0001D717}"), + "phi" : MTMathAtom(type: .ordinary, value: "\u{0001D719}"), + "varrho" : MTMathAtom(type: .ordinary, value: "\u{0001D71A}"), + "varpi" : MTMathAtom(type: .ordinary, value: "\u{0001D71B}"), + "varkappa" : MTMathAtom(type: .ordinary, value: "\u{03F0}"), + // Note: digamma (U+03DD) and Digamma (U+03DC) are not supported by Latin Modern Math font + + // Capital greek characters + "Gamma" : MTMathAtom(type: .variable, value: "\u{0393}"), + "Delta" : MTMathAtom(type: .variable, value: "\u{0394}"), + "Theta" : MTMathAtom(type: .variable, value: "\u{0398}"), + "Lambda" : MTMathAtom(type: .variable, value: "\u{039B}"), + "Xi" : MTMathAtom(type: .variable, value: "\u{039E}"), + "Pi" : MTMathAtom(type: .variable, value: "\u{03A0}"), + "Sigma" : MTMathAtom(type: .variable, value: "\u{03A3}"), + "Upsilon" : MTMathAtom(type: .variable, value: "\u{03A5}"), + "Phi" : MTMathAtom(type: .variable, value: "\u{03A6}"), + "Psi" : MTMathAtom(type: .variable, value: "\u{03A8}"), + "Omega" : MTMathAtom(type: .variable, value: "\u{03A9}"), + + // Open + "lceil" : MTMathAtom(type: .open, value: "\u{2308}"), + "lfloor" : MTMathAtom(type: .open, value: "\u{230A}"), + "langle" : MTMathAtom(type: .open, value: "\u{27E8}"), + "lgroup" : MTMathAtom(type: .open, value: "\u{27EE}"), + + // Close + "rceil" : MTMathAtom(type: .close, value: "\u{2309}"), + "rfloor" : MTMathAtom(type: .close, value: "\u{230B}"), + "rangle" : MTMathAtom(type: .close, value: "\u{27E9}"), + "rgroup" : MTMathAtom(type: .close, value: "\u{27EF}"), + + // Arrows + "leftarrow" : MTMathAtom(type: .relation, value: "\u{2190}"), + "uparrow" : MTMathAtom(type: .relation, value: "\u{2191}"), + "rightarrow" : MTMathAtom(type: .relation, value: "\u{2192}"), + "downarrow" : MTMathAtom(type: .relation, value: "\u{2193}"), + "leftrightarrow" : MTMathAtom(type: .relation, value: "\u{2194}"), + "updownarrow" : MTMathAtom(type: .relation, value: "\u{2195}"), + "nwarrow" : MTMathAtom(type: .relation, value: "\u{2196}"), + "nearrow" : MTMathAtom(type: .relation, value: "\u{2197}"), + "searrow" : MTMathAtom(type: .relation, value: "\u{2198}"), + "swarrow" : MTMathAtom(type: .relation, value: "\u{2199}"), + "mapsto" : MTMathAtom(type: .relation, value: "\u{21A6}"), + "Leftarrow" : MTMathAtom(type: .relation, value: "\u{21D0}"), + "Uparrow" : MTMathAtom(type: .relation, value: "\u{21D1}"), + "Rightarrow" : MTMathAtom(type: .relation, value: "\u{21D2}"), + "Downarrow" : MTMathAtom(type: .relation, value: "\u{21D3}"), + "Leftrightarrow" : MTMathAtom(type: .relation, value: "\u{21D4}"), + "Updownarrow" : MTMathAtom(type: .relation, value: "\u{21D5}"), + "longleftarrow" : MTMathAtom(type: .relation, value: "\u{27F5}"), + "longrightarrow" : MTMathAtom(type: .relation, value: "\u{27F6}"), + "longleftrightarrow" : MTMathAtom(type: .relation, value: "\u{27F7}"), + "Longleftarrow" : MTMathAtom(type: .relation, value: "\u{27F8}"), + "Longrightarrow" : MTMathAtom(type: .relation, value: "\u{27F9}"), + "Longleftrightarrow" : MTMathAtom(type: .relation, value: "\u{27FA}"), + "longmapsto" : MTMathAtom(type: .relation, value: "\u{27FC}"), + "hookrightarrow" : MTMathAtom(type: .relation, value: "\u{21AA}"), + "hookleftarrow" : MTMathAtom(type: .relation, value: "\u{21A9}"), + + + // Relations + "leq" : MTMathAtom(type: .relation, value: UnicodeSymbol.lessEqual), + "geq" : MTMathAtom(type: .relation, value: UnicodeSymbol.greaterEqual), + "leqslant" : MTMathAtom(type: .relation, value: "\u{2A7D}"), + "geqslant" : MTMathAtom(type: .relation, value: "\u{2A7E}"), + "neq" : MTMathAtom(type: .relation, value: UnicodeSymbol.notEqual), + "in" : MTMathAtom(type: .relation, value: "\u{2208}"), + "notin" : MTMathAtom(type: .relation, value: "\u{2209}"), + "ni" : MTMathAtom(type: .relation, value: "\u{220B}"), + "propto" : MTMathAtom(type: .relation, value: "\u{221D}"), + "mid" : MTMathAtom(type: .relation, value: "\u{2223}"), + "parallel" : MTMathAtom(type: .relation, value: "\u{2225}"), + "sim" : MTMathAtom(type: .relation, value: "\u{223C}"), + "simeq" : MTMathAtom(type: .relation, value: "\u{2243}"), + "cong" : MTMathAtom(type: .relation, value: "\u{2245}"), + "approx" : MTMathAtom(type: .relation, value: "\u{2248}"), + "asymp" : MTMathAtom(type: .relation, value: "\u{224D}"), + "doteq" : MTMathAtom(type: .relation, value: "\u{2250}"), + "equiv" : MTMathAtom(type: .relation, value: "\u{2261}"), + "gg" : MTMathAtom(type: .relation, value: "\u{226B}"), + "ll" : MTMathAtom(type: .relation, value: "\u{226A}"), + "prec" : MTMathAtom(type: .relation, value: "\u{227A}"), + "succ" : MTMathAtom(type: .relation, value: "\u{227B}"), + "preceq" : MTMathAtom(type: .relation, value: "\u{2AAF}"), + "succeq" : MTMathAtom(type: .relation, value: "\u{2AB0}"), + "subset" : MTMathAtom(type: .relation, value: "\u{2282}"), + "supset" : MTMathAtom(type: .relation, value: "\u{2283}"), + "subseteq" : MTMathAtom(type: .relation, value: "\u{2286}"), + "supseteq" : MTMathAtom(type: .relation, value: "\u{2287}"), + "sqsubset" : MTMathAtom(type: .relation, value: "\u{228F}"), + "sqsupset" : MTMathAtom(type: .relation, value: "\u{2290}"), + "sqsubseteq" : MTMathAtom(type: .relation, value: "\u{2291}"), + "sqsupseteq" : MTMathAtom(type: .relation, value: "\u{2292}"), + "models" : MTMathAtom(type: .relation, value: "\u{22A7}"), + "vdash" : MTMathAtom(type: .relation, value: "\u{22A2}"), + "dashv" : MTMathAtom(type: .relation, value: "\u{22A3}"), + "bowtie" : MTMathAtom(type: .relation, value: "\u{22C8}"), + "perp" : MTMathAtom(type: .relation, value: "\u{27C2}"), + "implies" : MTMathAtom(type: .relation, value: "\u{27F9}"), + + // Negated relations (amssymb) + // Inequality negations + "nless" : MTMathAtom(type: .relation, value: "\u{226E}"), + "ngtr" : MTMathAtom(type: .relation, value: "\u{226F}"), + "nleq" : MTMathAtom(type: .relation, value: "\u{2270}"), + "ngeq" : MTMathAtom(type: .relation, value: "\u{2271}"), + "nleqslant" : MTMathAtom(type: .relation, value: "\u{2A87}"), + "ngeqslant" : MTMathAtom(type: .relation, value: "\u{2A88}"), + "lneq" : MTMathAtom(type: .relation, value: "\u{2A87}"), + "gneq" : MTMathAtom(type: .relation, value: "\u{2A88}"), + "lneqq" : MTMathAtom(type: .relation, value: "\u{2268}"), + "gneqq" : MTMathAtom(type: .relation, value: "\u{2269}"), + "lnsim" : MTMathAtom(type: .relation, value: "\u{22E6}"), + "gnsim" : MTMathAtom(type: .relation, value: "\u{22E7}"), + "lnapprox" : MTMathAtom(type: .relation, value: "\u{2A89}"), + "gnapprox" : MTMathAtom(type: .relation, value: "\u{2A8A}"), + + // Ordering negations + "nprec" : MTMathAtom(type: .relation, value: "\u{2280}"), + "nsucc" : MTMathAtom(type: .relation, value: "\u{2281}"), + "npreceq" : MTMathAtom(type: .relation, value: "\u{22E0}"), + "nsucceq" : MTMathAtom(type: .relation, value: "\u{22E1}"), + "precneqq" : MTMathAtom(type: .relation, value: "\u{2AB5}"), + "succneqq" : MTMathAtom(type: .relation, value: "\u{2AB6}"), + "precnsim" : MTMathAtom(type: .relation, value: "\u{22E8}"), + "succnsim" : MTMathAtom(type: .relation, value: "\u{22E9}"), + "precnapprox" : MTMathAtom(type: .relation, value: "\u{2AB9}"), + "succnapprox" : MTMathAtom(type: .relation, value: "\u{2ABA}"), + + // Similarity/congruence negations + "nsim" : MTMathAtom(type: .relation, value: "\u{2241}"), + "ncong" : MTMathAtom(type: .relation, value: "\u{2247}"), + "nmid" : MTMathAtom(type: .relation, value: "\u{2224}"), + "nshortmid" : MTMathAtom(type: .relation, value: "\u{2224}"), + "nparallel" : MTMathAtom(type: .relation, value: "\u{2226}"), + "nshortparallel" : MTMathAtom(type: .relation, value: "\u{2226}"), + + // Set relation negations + "nsubseteq" : MTMathAtom(type: .relation, value: "\u{2288}"), + "nsupseteq" : MTMathAtom(type: .relation, value: "\u{2289}"), + "subsetneq" : MTMathAtom(type: .relation, value: "\u{228A}"), + "supsetneq" : MTMathAtom(type: .relation, value: "\u{228B}"), + "subsetneqq" : MTMathAtom(type: .relation, value: "\u{2ACB}"), + "supsetneqq" : MTMathAtom(type: .relation, value: "\u{2ACC}"), + "varsubsetneq" : MTMathAtom(type: .relation, value: "\u{228A}"), + "varsupsetneq" : MTMathAtom(type: .relation, value: "\u{228B}"), + "varsubsetneqq" : MTMathAtom(type: .relation, value: "\u{2ACB}"), + "varsupsetneqq" : MTMathAtom(type: .relation, value: "\u{2ACC}"), + "notni" : MTMathAtom(type: .relation, value: "\u{220C}"), + "nni" : MTMathAtom(type: .relation, value: "\u{220C}"), + + // Triangle negations + "ntriangleleft" : MTMathAtom(type: .relation, value: "\u{22EA}"), + "ntriangleright" : MTMathAtom(type: .relation, value: "\u{22EB}"), + "ntrianglelefteq" : MTMathAtom(type: .relation, value: "\u{22EC}"), + "ntrianglerighteq" : MTMathAtom(type: .relation, value: "\u{22ED}"), + + // Turnstile negations + "nvdash" : MTMathAtom(type: .relation, value: "\u{22AC}"), + "nvDash" : MTMathAtom(type: .relation, value: "\u{22AD}"), + "nVdash" : MTMathAtom(type: .relation, value: "\u{22AE}"), + "nVDash" : MTMathAtom(type: .relation, value: "\u{22AF}"), + + // Square subset negations + "nsqsubseteq" : MTMathAtom(type: .relation, value: "\u{22E2}"), + "nsqsupseteq" : MTMathAtom(type: .relation, value: "\u{22E3}"), + + // operators + "times" : MTMathAtomFactory.times(), + "div" : MTMathAtomFactory.divide(), + "pm" : MTMathAtom(type: .binaryOperator, value: "\u{00B1}"), + "dagger" : MTMathAtom(type: .binaryOperator, value: "\u{2020}"), + "ddagger" : MTMathAtom(type: .binaryOperator, value: "\u{2021}"), + "mp" : MTMathAtom(type: .binaryOperator, value: "\u{2213}"), + "setminus" : MTMathAtom(type: .binaryOperator, value: "\u{2216}"), + "ast" : MTMathAtom(type: .binaryOperator, value: "\u{2217}"), + "circ" : MTMathAtom(type: .binaryOperator, value: "\u{2218}"), + "bullet" : MTMathAtom(type: .binaryOperator, value: "\u{2219}"), + "wedge" : MTMathAtom(type: .binaryOperator, value: "\u{2227}"), + "vee" : MTMathAtom(type: .binaryOperator, value: "\u{2228}"), + "cap" : MTMathAtom(type: .binaryOperator, value: "\u{2229}"), + "cup" : MTMathAtom(type: .binaryOperator, value: "\u{222A}"), + "wr" : MTMathAtom(type: .binaryOperator, value: "\u{2240}"), + "uplus" : MTMathAtom(type: .binaryOperator, value: "\u{228E}"), + "sqcap" : MTMathAtom(type: .binaryOperator, value: "\u{2293}"), + "sqcup" : MTMathAtom(type: .binaryOperator, value: "\u{2294}"), + "oplus" : MTMathAtom(type: .binaryOperator, value: "\u{2295}"), + "ominus" : MTMathAtom(type: .binaryOperator, value: "\u{2296}"), + "otimes" : MTMathAtom(type: .binaryOperator, value: "\u{2297}"), + "oslash" : MTMathAtom(type: .binaryOperator, value: "\u{2298}"), + "odot" : MTMathAtom(type: .binaryOperator, value: "\u{2299}"), + "star" : MTMathAtom(type: .binaryOperator, value: "\u{22C6}"), + "cdot" : MTMathAtom(type: .binaryOperator, value: "\u{22C5}"), + "diamond" : MTMathAtom(type: .binaryOperator, value: "\u{22C4}"), + "amalg" : MTMathAtom(type: .binaryOperator, value: "\u{2A3F}"), + + // Additional binary operators (amssymb) + "ltimes" : MTMathAtom(type: .binaryOperator, value: "\u{22C9}"), // left semidirect product + "rtimes" : MTMathAtom(type: .binaryOperator, value: "\u{22CA}"), // right semidirect product + "circledast" : MTMathAtom(type: .binaryOperator, value: "\u{229B}"), + "circledcirc" : MTMathAtom(type: .binaryOperator, value: "\u{229A}"), + "circleddash" : MTMathAtom(type: .binaryOperator, value: "\u{229D}"), + "boxdot" : MTMathAtom(type: .binaryOperator, value: "\u{22A1}"), + "boxminus" : MTMathAtom(type: .binaryOperator, value: "\u{229F}"), + "boxplus" : MTMathAtom(type: .binaryOperator, value: "\u{229E}"), + "boxtimes" : MTMathAtom(type: .binaryOperator, value: "\u{22A0}"), + "divideontimes" : MTMathAtom(type: .binaryOperator, value: "\u{22C7}"), + "dotplus" : MTMathAtom(type: .binaryOperator, value: "\u{2214}"), + "lhd" : MTMathAtom(type: .binaryOperator, value: "\u{22B2}"), // left normal subgroup + "rhd" : MTMathAtom(type: .binaryOperator, value: "\u{22B3}"), // right normal subgroup + "unlhd" : MTMathAtom(type: .binaryOperator, value: "\u{22B4}"), // left normal subgroup or equal + "unrhd" : MTMathAtom(type: .binaryOperator, value: "\u{22B5}"), // right normal subgroup or equal + "intercal" : MTMathAtom(type: .binaryOperator, value: "\u{22BA}"), + "barwedge" : MTMathAtom(type: .binaryOperator, value: "\u{22BC}"), + "veebar" : MTMathAtom(type: .binaryOperator, value: "\u{22BB}"), + "curlywedge" : MTMathAtom(type: .binaryOperator, value: "\u{22CF}"), + "curlyvee" : MTMathAtom(type: .binaryOperator, value: "\u{22CE}"), + "doublebarwedge" : MTMathAtom(type: .binaryOperator, value: "\u{2A5E}"), + "centerdot" : MTMathAtom(type: .binaryOperator, value: "\u{22C5}"), // alias for cdot + + // No limit operators + "log" : MTMathAtomFactory.operatorWithName( "log", limits: false), + "lg" : MTMathAtomFactory.operatorWithName( "lg", limits: false), + "ln" : MTMathAtomFactory.operatorWithName( "ln", limits: false), + "sin" : MTMathAtomFactory.operatorWithName( "sin", limits: false), + "arcsin" : MTMathAtomFactory.operatorWithName( "arcsin", limits: false), + "sinh" : MTMathAtomFactory.operatorWithName( "sinh", limits: false), + "cos" : MTMathAtomFactory.operatorWithName( "cos", limits: false), + "arccos" : MTMathAtomFactory.operatorWithName( "arccos", limits: false), + "cosh" : MTMathAtomFactory.operatorWithName( "cosh", limits: false), + "tan" : MTMathAtomFactory.operatorWithName( "tan", limits: false), + "arctan" : MTMathAtomFactory.operatorWithName( "arctan", limits: false), + "tanh" : MTMathAtomFactory.operatorWithName( "tanh", limits: false), + "cot" : MTMathAtomFactory.operatorWithName( "cot", limits: false), + "coth" : MTMathAtomFactory.operatorWithName( "coth", limits: false), + "sec" : MTMathAtomFactory.operatorWithName( "sec", limits: false), + "csc" : MTMathAtomFactory.operatorWithName( "csc", limits: false), + // Additional inverse trig functions + "arccot" : MTMathAtomFactory.operatorWithName( "arccot", limits: false), + "arcsec" : MTMathAtomFactory.operatorWithName( "arcsec", limits: false), + "arccsc" : MTMathAtomFactory.operatorWithName( "arccsc", limits: false), + // Additional hyperbolic functions + "sech" : MTMathAtomFactory.operatorWithName( "sech", limits: false), + "csch" : MTMathAtomFactory.operatorWithName( "csch", limits: false), + // Inverse hyperbolic functions + "arcsinh" : MTMathAtomFactory.operatorWithName( "arcsinh", limits: false), + "arccosh" : MTMathAtomFactory.operatorWithName( "arccosh", limits: false), + "arctanh" : MTMathAtomFactory.operatorWithName( "arctanh", limits: false), + "arccoth" : MTMathAtomFactory.operatorWithName( "arccoth", limits: false), + "arcsech" : MTMathAtomFactory.operatorWithName( "arcsech", limits: false), + "arccsch" : MTMathAtomFactory.operatorWithName( "arccsch", limits: false), + "arg" : MTMathAtomFactory.operatorWithName( "arg", limits: false), + "ker" : MTMathAtomFactory.operatorWithName( "ker", limits: false), + "dim" : MTMathAtomFactory.operatorWithName( "dim", limits: false), + "hom" : MTMathAtomFactory.operatorWithName( "hom", limits: false), + "exp" : MTMathAtomFactory.operatorWithName( "exp", limits: false), + "deg" : MTMathAtomFactory.operatorWithName( "deg", limits: false), + "mod" : MTMathAtomFactory.operatorWithName("mod", limits: false), + + // Limit operators + "lim" : MTMathAtomFactory.operatorWithName( "lim", limits: true), + "limsup" : MTMathAtomFactory.operatorWithName( "lim sup", limits: true), + "liminf" : MTMathAtomFactory.operatorWithName( "lim inf", limits: true), + "max" : MTMathAtomFactory.operatorWithName( "max", limits: true), + "min" : MTMathAtomFactory.operatorWithName( "min", limits: true), + "sup" : MTMathAtomFactory.operatorWithName( "sup", limits: true), + "inf" : MTMathAtomFactory.operatorWithName( "inf", limits: true), + "det" : MTMathAtomFactory.operatorWithName( "det", limits: true), + "Pr" : MTMathAtomFactory.operatorWithName( "Pr", limits: true), + "gcd" : MTMathAtomFactory.operatorWithName( "gcd", limits: true), + + // Large operators + "prod" : MTMathAtomFactory.operatorWithName( "\u{220F}", limits: true), + "coprod" : MTMathAtomFactory.operatorWithName( "\u{2210}", limits: true), + "sum" : MTMathAtomFactory.operatorWithName( "\u{2211}", limits: true), + "int" : MTMathAtomFactory.operatorWithName( "\u{222B}", limits: false), + "iint" : MTMathAtomFactory.operatorWithName( "\u{222C}", limits: false), + "iiint" : MTMathAtomFactory.operatorWithName( "\u{222D}", limits: false), + "iiiint" : MTMathAtomFactory.operatorWithName( "\u{2A0C}", limits: false), + "oint" : MTMathAtomFactory.operatorWithName( "\u{222E}", limits: false), + "bigwedge" : MTMathAtomFactory.operatorWithName( "\u{22C0}", limits: true), + "bigvee" : MTMathAtomFactory.operatorWithName( "\u{22C1}", limits: true), + "bigcap" : MTMathAtomFactory.operatorWithName( "\u{22C2}", limits: true), + "bigcup" : MTMathAtomFactory.operatorWithName( "\u{22C3}", limits: true), + "bigodot" : MTMathAtomFactory.operatorWithName( "\u{2A00}", limits: true), + "bigoplus" : MTMathAtomFactory.operatorWithName( "\u{2A01}", limits: true), + "bigotimes" : MTMathAtomFactory.operatorWithName( "\u{2A02}", limits: true), + "biguplus" : MTMathAtomFactory.operatorWithName( "\u{2A04}", limits: true), + "bigsqcup" : MTMathAtomFactory.operatorWithName( "\u{2A06}", limits: true), + + // Latex command characters + "{" : MTMathAtom(type: .open, value: "{"), + "}" : MTMathAtom(type: .close, value: "}"), + "$" : MTMathAtom(type: .ordinary, value: "$"), + "&" : MTMathAtom(type: .ordinary, value: "&"), + "#" : MTMathAtom(type: .ordinary, value: "#"), + "%" : MTMathAtom(type: .ordinary, value: "%"), + "_" : MTMathAtom(type: .ordinary, value: "_"), + " " : MTMathAtom(type: .ordinary, value: " "), + "backslash" : MTMathAtom(type: .ordinary, value: "\\"), + + // Punctuation + // Note: \colon is different from : which is a relation + "colon" : MTMathAtom(type: .punctuation, value: ":"), + "cdotp" : MTMathAtom(type: .punctuation, value: "\u{00B7}"), + + // Other symbols + "degree" : MTMathAtom(type: .ordinary, value: "\u{00B0}"), + "neg" : MTMathAtom(type: .ordinary, value: "\u{00AC}"), + "angstrom" : MTMathAtom(type: .ordinary, value: "\u{00C5}"), + "aa" : MTMathAtom(type: .ordinary, value: "\u{00E5}"), // NEW å + "ae" : MTMathAtom(type: .ordinary, value: "\u{00E6}"), // NEW æ + "o" : MTMathAtom(type: .ordinary, value: "\u{00F8}"), // NEW ø + "oe" : MTMathAtom(type: .ordinary, value: "\u{0153}"), // NEW œ + "ss" : MTMathAtom(type: .ordinary, value: "\u{00DF}"), // NEW ß + "cc" : MTMathAtom(type: .ordinary, value: "\u{00E7}"), // NEW ç + "CC" : MTMathAtom(type: .ordinary, value: "\u{00C7}"), // NEW Ç + "O" : MTMathAtom(type: .ordinary, value: "\u{00D8}"), // NEW Ø + "AE" : MTMathAtom(type: .ordinary, value: "\u{00C6}"), // NEW Æ + "OE" : MTMathAtom(type: .ordinary, value: "\u{0152}"), // NEW Œ + "|" : MTMathAtom(type: .ordinary, value: "\u{2016}"), + "vert" : MTMathAtom(type: .ordinary, value: "|"), + "ldots" : MTMathAtom(type: .ordinary, value: "\u{2026}"), + "prime" : MTMathAtom(type: .ordinary, value: "\u{2032}"), + "hbar" : MTMathAtom(type: .ordinary, value: "\u{210F}"), + "lbar" : MTMathAtom(type: .ordinary, value: "\u{019B}"), // NEW ƛ + "Im" : MTMathAtom(type: .ordinary, value: "\u{2111}"), + "ell" : MTMathAtom(type: .ordinary, value: "\u{2113}"), + "wp" : MTMathAtom(type: .ordinary, value: "\u{2118}"), + "Re" : MTMathAtom(type: .ordinary, value: "\u{211C}"), + "mho" : MTMathAtom(type: .ordinary, value: "\u{2127}"), + "aleph" : MTMathAtom(type: .ordinary, value: "\u{2135}"), + "beth" : MTMathAtom(type: .ordinary, value: "\u{2136}"), + "gimel" : MTMathAtom(type: .ordinary, value: "\u{2137}"), + "daleth" : MTMathAtom(type: .ordinary, value: "\u{2138}"), + "forall" : MTMathAtom(type: .ordinary, value: "\u{2200}"), + "exists" : MTMathAtom(type: .ordinary, value: "\u{2203}"), + "nexists" : MTMathAtom(type: .ordinary, value: "\u{2204}"), + "emptyset" : MTMathAtom(type: .ordinary, value: "\u{2205}"), + "varnothing" : MTMathAtom(type: .ordinary, value: "\u{2205}"), + "nabla" : MTMathAtom(type: .ordinary, value: "\u{2207}"), + "infty" : MTMathAtom(type: .ordinary, value: "\u{221E}"), + "angle" : MTMathAtom(type: .ordinary, value: "\u{2220}"), + "measuredangle" : MTMathAtom(type: .ordinary, value: "\u{2221}"), + "top" : MTMathAtom(type: .ordinary, value: "\u{22A4}"), + "bot" : MTMathAtom(type: .ordinary, value: "\u{22A5}"), + "vdots" : MTMathAtom(type: .ordinary, value: "\u{22EE}"), + "cdots" : MTMathAtom(type: .ordinary, value: "\u{22EF}"), + "ddots" : MTMathAtom(type: .ordinary, value: "\u{22F1}"), + "triangle" : MTMathAtom(type: .ordinary, value: "\u{25B3}"), + "Box" : MTMathAtom(type: .ordinary, value: "\u{25A1}"), + "imath" : MTMathAtom(type: .ordinary, value: "\u{0001D6A4}"), + "jmath" : MTMathAtom(type: .ordinary, value: "\u{0001D6A5}"), + "upquote" : MTMathAtom(type: .ordinary, value: "\u{0027}"), + "partial" : MTMathAtom(type: .ordinary, value: "\u{0001D715}"), + + // Spacing + "," : MTMathSpace(space: 3), + ">" : MTMathSpace(space: 4), + ";" : MTMathSpace(space: 5), + "!" : MTMathSpace(space: -3), + "quad" : MTMathSpace(space: 18), // quad = 1em = 18mu + "qquad" : MTMathSpace(space: 36), // qquad = 2em + + // Style + "displaystyle" : MTMathStyle(style: .display), + "textstyle" : MTMathStyle(style: .text), + "scriptstyle" : MTMathStyle(style: .script), + "scriptscriptstyle" : MTMathStyle(style: .scriptOfScript), + ] + + static var supportedAccentedCharacters: [Character: (String, String)] = [ + // Acute accents + "á": ("acute", "a"), "é": ("acute", "e"), "í": ("acute", "i"), + "ó": ("acute", "o"), "ú": ("acute", "u"), "ý": ("acute", "y"), + + // Grave accents + "à": ("grave", "a"), "è": ("grave", "e"), "ì": ("grave", "i"), + "ò": ("grave", "o"), "ù": ("grave", "u"), + + // Circumflex + "â": ("hat", "a"), "ê": ("hat", "e"), "î": ("hat", "i"), + "ĵ": ("hat", "j"), // j with circumflex (Esperanto) + "ô": ("hat", "o"), "û": ("hat", "u"), + + // Umlaut/dieresis + "ä": ("ddot", "a"), "ë": ("ddot", "e"), "ï": ("ddot", "i"), + "ö": ("ddot", "o"), "ü": ("ddot", "u"), "ÿ": ("ddot", "y"), + + // Tilde + "ã": ("tilde", "a"), "ñ": ("tilde", "n"), "õ": ("tilde", "o"), + + // Special characters + "ç": ("cc", ""), "ø": ("o", ""), "å": ("aa", ""), "æ": ("ae", ""), + "œ": ("oe", ""), "ß": ("ss", ""), + "'": ("upquote", ""), // this may be dangerous in math mode + + // Upper case variants + "Á": ("acute", "A"), "É": ("acute", "E"), "Í": ("acute", "I"), + "Ó": ("acute", "O"), "Ú": ("acute", "U"), "Ý": ("acute", "Y"), + "À": ("grave", "A"), "È": ("grave", "E"), "Ì": ("grave", "I"), + "Ò": ("grave", "O"), "Ù": ("grave", "U"), + "Â": ("hat", "A"), "Ê": ("hat", "E"), "Î": ("hat", "I"), + "Ô": ("hat", "O"), "Û": ("hat", "U"), + "Ä": ("ddot", "A"), "Ë": ("ddot", "E"), "Ï": ("ddot", "I"), + "Ö": ("ddot", "O"), "Ü": ("ddot", "U"), + "Ã": ("tilde", "A"), "Ñ": ("tilde", "N"), "Õ": ("tilde", "O"), + "Ç": ("CC", ""), + "Ø": ("O", ""), + "Å": ("AA", ""), + "Æ": ("AE", ""), + "Œ": ("OE", ""), + ] + + private static let textToLatexLock = NSLock() + static var _textToLatexSymbolName: [String: String]? = nil + public static var textToLatexSymbolName: [String: String] { + get { + if self._textToLatexSymbolName == nil { + var output = [String: String]() + for (key, atom) in Self.supportedLatexSymbols { + if atom.nucleus.count == 0 { + continue + } + if let existingText = output[atom.nucleus] { + // If there are 2 key for the same symbol, choose one deterministically. + if key.count > existingText.count { + // Keep the shorter command + continue + } else if key.count == existingText.count { + // If the length is the same, keep the alphabetically first + if key.compare(existingText) == .orderedDescending { + continue + } + } + } + output[atom.nucleus] = key + } + // protect lazily loading table in a multi-thread concurrent environment + textToLatexLock.lock() + defer { textToLatexLock.unlock() } + if self._textToLatexSymbolName == nil { + self._textToLatexSymbolName = output + } + } + return self._textToLatexSymbolName! + } + // make textToLatexSymbolName readonly (allows internal load) + // entries can be lazily added with NSLock protection. + // set { + // self._textToLatexSymbolName = newValue + // } + } + + // public static let sharedInstance = MTMathAtomFactory() + + static let fontStyles : [String: MTFontStyle] = [ + "mathnormal" : .defaultStyle, + "mathrm": .roman, + "textrm": .roman, + "rm": .roman, + "mathbf": .bold, + "bf": .bold, + "textbf": .bold, + "mathcal": .caligraphic, + "cal": .caligraphic, + "mathtt": .typewriter, + "texttt": .typewriter, + "mathit": .italic, + "textit": .italic, + "mit": .italic, + "mathsf": .sansSerif, + "textsf": .sansSerif, + "mathfrak": .fraktur, + "frak": .fraktur, + "mathbb": .blackboard, + "mathbfit": .boldItalic, + "bm": .boldItalic, + "boldsymbol": .boldItalic, + "text": .roman, + // Note: operatorname is handled specially in MTMathListBuilder to create proper operators + ] + + public static func fontStyleWithName(_ fontName:String) -> MTFontStyle? { + fontStyles[fontName] + } + + public static func fontNameForStyle(_ fontStyle:MTFontStyle) -> String { + switch fontStyle { + case .defaultStyle: return "mathnormal" + case .roman: return "mathrm" + case .bold: return "mathbf" + case .fraktur: return "mathfrak" + case .caligraphic: return "mathcal" + case .italic: return "mathit" + case .sansSerif: return "mathsf" + case .blackboard: return "mathbb" + case .typewriter: return "mathtt" + case .boldItalic: return "bm" + } + } + + /// Returns an atom for the multiplication sign (i.e., \times or "*") + public static func times() -> MTMathAtom { + MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.multiplication) + } + + /// Returns an atom for the division sign (i.e., \div or "/") + public static func divide() -> MTMathAtom { + MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.division) + } + + /// Returns an atom which is a placeholder square + public static func placeholder() -> MTMathAtom { + MTMathAtom(type: .placeholder, value: UnicodeSymbol.whiteSquare) + } + + /** Returns a fraction with a placeholder for the numerator and denominator */ + public static func placeholderFraction() -> MTFraction { + let frac = MTFraction() + frac.numerator = MTMathList() + frac.numerator?.add(placeholder()) + frac.denominator = MTMathList() + frac.denominator?.add(placeholder()) + return frac + } + + /** Returns a square root with a placeholder as the radicand. */ + public static func placeholderSquareRoot() -> MTRadical { + let rad = MTRadical() + rad.radicand = MTMathList() + rad.radicand?.add(placeholder()) + return rad + } + + /** Returns a radical with a placeholder as the radicand. */ + public static func placeholderRadical() -> MTRadical { + let rad = MTRadical() + rad.radicand = MTMathList() + rad.degree = MTMathList() + rad.radicand?.add(placeholder()) + rad.degree?.add(placeholder()) + return rad + } + + /// Latin Small Letter Dotless I (U+0131) - base character that can be styled + private static let dotlessI: Character = "\u{0131}" + /// Latin Small Letter Dotless J (U+0237) - base character that can be styled + private static let dotlessJ: Character = "\u{0237}" + + public static func atom(fromAccentedCharacter ch: Character) -> MTMathAtom? { + if let symbol = supportedAccentedCharacters[ch] { + // first handle any special characters + if let atom = atom(forLatexSymbol: symbol.0) { + return atom + } + + if let accent = MTMathAtomFactory.accent(withName: symbol.0) { + // The command is an accent + let list = MTMathList() + let baseChar = Array(symbol.1)[0] + // Use dotless variants for 'i' and 'j' to avoid double dots with accents. + // We use the base Latin dotless characters (U+0131, U+0237) rather than + // the pre-styled mathematical italic versions (U+1D6A4, U+1D6A5) so that + // font style (roman, bold, etc.) is properly applied during rendering. + if baseChar == "i" { + list.add(MTMathAtom(type: .ordinary, value: String(dotlessI))) + } else if baseChar == "j" { + list.add(MTMathAtom(type: .ordinary, value: String(dotlessJ))) + } else { + list.add(atom(forCharacter: baseChar)) + } + accent.innerList = list + return accent + } + } + return nil + } + + // MARK: - + /** Gets the atom with the right type for the given character. If an atom + cannot be determined for a given character this returns nil. + This function follows latex conventions for assigning types to the atoms. + The following characters are not supported and will return nil: + - Any non-ascii character. + - Any control character or spaces (< 0x21) + - Latex control chars: $ % # & ~ ' + - Chars with special meaning in latex: ^ _ { } \ + All other characters, including those with accents, will have a non-nil atom returned. + */ + public static func atom(forCharacter ch: Character) -> MTMathAtom? { + let chStr = String(ch) + switch chStr { + case "\u{0410}"..."\u{044F}": + // Cyrillic alphabet + return MTMathAtom(type: .ordinary, value: chStr) + case _ where supportedAccentedCharacters.keys.contains(ch): + // support for áéíóúýàèìòùâêîôûäëïöüÿãñõçøåæœß'ÁÉÍÓÚÝÀÈÌÒÙÂÊÎÔÛÄËÏÖÜÃÑÕÇØÅÆŒ + return atom(fromAccentedCharacter: ch) + case _ where ch.utf32Char < 0x0021 || ch.utf32Char > 0x007E: + return nil + case "$", "%", "#", "&", "~", "\'", "^", "_", "{", "}", "\\": + return nil + case "(", "[": + return MTMathAtom(type: .open, value: chStr) + case ")", "]", "!", "?": + return MTMathAtom(type: .close, value: chStr) + case ",", ";": + return MTMathAtom(type: .punctuation, value: chStr) + case "=", ">", "<": + return MTMathAtom(type: .relation, value: chStr) + case ":": + // Math colon is ratio. Regular colon is \colon + return MTMathAtom(type: .relation, value: "\u{2236}") + case "-": + return MTMathAtom(type: .binaryOperator, value: "\u{2212}") + case "+", "*": + return MTMathAtom(type: .binaryOperator, value: chStr) + case ".", "0"..."9": + return MTMathAtom(type: .number, value: chStr) + case "a"..."z", "A"..."Z": + return MTMathAtom(type: .variable, value: chStr) + case "\"", "/", "@", "`", "|": + return MTMathAtom(type: .ordinary, value: chStr) + default: + assertionFailure("Unknown ASCII character '\(ch)'. Should have been handled earlier.") + return nil + } + } + + /** Returns a `MTMathList` with one atom per character in the given string. This function + does not do any LaTeX conversion or interpretation. It simply uses `atom(forCharacter:)` to + convert the characters to atoms. Any character that cannot be converted is ignored. */ + public static func atomList(for string: String) -> MTMathList { + let list = MTMathList() + for character in string { + if let newAtom = atom(forCharacter: character) { + list.add(newAtom) + } + } + return list + } + + /** Returns an atom with the right type for a given latex symbol (e.g. theta) + If the latex symbol is unknown this will return nil. This supports LaTeX aliases as well. + */ + public static func atom(forLatexSymbol name: String) -> MTMathAtom? { + var name = name + if let canonicalName = aliases[name] { + name = canonicalName + } + if let atom = supportedLatexSymbols[name] { + return atom.copy() + } + return nil + } + + /** Finds the name of the LaTeX symbol name for the given atom. This function is a reverse + of the above function. If no latex symbol name corresponds to the atom, then this returns `nil` + If nucleus of the atom is empty, then this will return `nil`. + Note: This is not an exact reverse of the above in the case of aliases. If an LaTeX alias + points to a given symbol, then this function will return the original symbol name and not the + alias. + Note: This function does not convert MathSpaces to latex command names either. + */ + public static func latexSymbolName(for atom: MTMathAtom) -> String? { + guard !atom.nucleus.isEmpty else { return nil } + return Self.textToLatexSymbolName[atom.nucleus] + } + + /** Define a latex symbol for rendering. This function allows defining custom symbols that are + not already present in the default set, or override existing symbols with new meaning. + e.g. to define a symbol for "lcm" one can call: + `MTMathAtomFactory.add(latexSymbol:"lcm", value:MTMathAtomFactory.operatorWithName("lcm", limits: false))` */ + public static func add(latexSymbol name: String, value: MTMathAtom) { + let _ = Self.textToLatexSymbolName + // above force textToLatexSymbolName to initialise first, _textToLatexSymbolName also initialized. + // protect lazily loading table in a multi-thread concurrent environment + textToLatexLock.lock() + defer { textToLatexLock.unlock() } + supportedLatexSymbols[name] = value + Self._textToLatexSymbolName?[value.nucleus] = name + } + + /** Returns a large opertor for the given name. If limits is true, limits are set up on + the operator and displayed differently. */ + public static func operatorWithName(_ name: String, limits: Bool) -> MTLargeOperator { + MTLargeOperator(value: name, limits: limits) + } + + /** Returns an accent with the given name. The name of the accent is the LaTeX name + such as `grave`, `hat` etc. If the name is not a recognized accent name, this + returns nil. The `innerList` of the returned `MTAccent` is nil. + */ + public static func accent(withName name: String) -> MTAccent? { + if let accentValue = accents[name] { + let accent = MTAccent(value: accentValue) + // Mark stretchy arrow accents (\overleftarrow, \overrightarrow, \overleftrightarrow) + // These should stretch to match content width + // \vec is NOT stretchy - it should use a small fixed-size arrow + let stretchyAccents: Set = ["overleftarrow", "overrightarrow", "overleftrightarrow"] + accent.isStretchy = stretchyAccents.contains(name) + + // Mark wide accents (\widehat, \widetilde, \widecheck) + // These should stretch horizontally to cover content width + // \hat, \tilde, \check are NOT wide - they use fixed-size accents + let wideAccents: Set = ["widehat", "widetilde", "widecheck"] + accent.isWide = wideAccents.contains(name) + + return accent + } + return nil + } + + /** Returns the accent name for the given accent. This is the reverse of the above + function. */ + public static func accentName(_ accent: MTAccent) -> String? { + accentValueToName[accent.nucleus] + } + + /** Creates a new boundary atom for the given delimiter name. If the delimiter name + is not recognized it returns nil. A delimiter name can be a single character such + as '(' or a latex command such as 'uparrow'. + @note In order to distinguish between the delimiter '|' and the delimiter '\|' the delimiter '\|' + the has been renamed to '||'. + */ + public static func boundary(forDelimiter name: String) -> MTMathAtom? { + if let delimValue = Self.delimiters[name] { + return MTMathAtom(type: .boundary, value: delimValue) + } + return nil + } + + /** Returns the delimiter name for a boundary atom. This is a reverse of the above function. + If the atom is not a boundary atom or if the delimiter value is unknown this returns `nil`. + @note This is not an exact reverse of the above function. Some delimiters have two names (e.g. + `<` and `langle`) and this function always returns the shorter name. + */ + public static func getDelimiterName(of boundary: MTMathAtom) -> String? { + guard boundary.type == .boundary else { return nil } + return Self.delimValueToName[boundary.nucleus] + } + + /** Returns a fraction with the given numerator and denominator. */ + public static func fraction(withNumerator num: MTMathList, denominator denom: MTMathList) -> MTFraction { + let frac = MTFraction() + frac.numerator = num + frac.denominator = denom + return frac + } + + public static func mathListForCharacters(_ chars:String) -> MTMathList? { + let list = MTMathList() + for ch in chars { + if let atom = self.atom(forCharacter: ch) { + list.add(atom) + } + } + return list + } + + /** Simplification of above function when numerator and denominator are simple strings. + This function converts the strings to a `MTFraction`. */ + public static func fraction(withNumeratorString numStr: String, denominatorString denomStr: String) -> MTFraction { + let num = Self.atomList(for: numStr) + let denom = Self.atomList(for: denomStr) + return Self.fraction(withNumerator: num, denominator: denom) + } + + + static let matrixEnvs = [ + "matrix": [], + "pmatrix": ["(", ")"], + "bmatrix": ["[", "]"], + "Bmatrix": ["{", "}"], + "vmatrix": ["vert", "vert"], + "Vmatrix": ["Vert", "Vert"], + "smallmatrix": [], + // Starred versions with optional alignment + "matrix*": [], + "pmatrix*": ["(", ")"], + "bmatrix*": ["[", "]"], + "Bmatrix*": ["{", "}"], + "vmatrix*": ["vert", "vert"], + "Vmatrix*": ["Vert", "Vert"] + ] + + /** Builds a table for a given environment with the given rows. Returns a `MTMathAtom` containing the + table and any other atoms necessary for the given environment. Returns nil and sets error + if the table could not be built. + @param env The environment to use to build the table. If the env is nil, then the default table is built. + @note The reason this function returns a `MTMathAtom` and not a `MTMathTable` is because some + matrix environments are have builtin delimiters added to the table and hence are returned as inner atoms. + + Column constraints by environment (matching KaTeX behavior): + - `aligned`, `eqalign`: Any number of columns (1, 2, 3, 4+) with r-l-r-l alignment pattern + - `split`: Maximum 2 columns with r-l alignment + - `gather`, `displaylines`: Exactly 1 column, centered + - `cases`: 1 or 2 columns, left-aligned + - `eqnarray`: Exactly 3 columns with r-c-l alignment + */ + public static func table(withEnvironment env: String?, alignment: MTColumnAlignment? = nil, rows: [[MTMathList]], error:inout NSError?) -> MTMathAtom? { + let table = MTMathTable(environment: env) + + for i in 0.. 2 { + let message = "split environment can have at most 2 columns" + if error == nil { + error = NSError(domain: MTParseError, code: MTParseErrors.invalidNumColumns.rawValue, userInfo: [NSLocalizedDescriptionKey:message]) + } + return nil + } + + let spacer = MTMathAtom(type: .ordinary, value: "") + + // Add spacer at beginning of odd-indexed columns (1, 3, 5, ...) + // This matches KaTeX behavior for binary operator spacing + for i in 0.. CGSize { + CGSize(width: displayList.width + contentInsets.left + contentInsets.right, + height: displayList.ascent + displayList.descent + contentInsets.top + contentInsets.bottom) + } + public func asImage() -> (NSError?, MTImage?) { + func layoutImage(size: CGSize, displayList: MTMathListDisplay) { + var textX = CGFloat(0) + switch self.textAlignment { + case .left: textX = contentInsets.left + case .center: textX = (size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left + case .right: textX = size.width - displayList.width - contentInsets.right + } + let availableHeight = size.height - contentInsets.bottom - contentInsets.top + + // center things vertically + var height = displayList.ascent + displayList.descent + if height < fontSize/2 { + height = fontSize/2 // set height to half the font size + } + let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom + displayList.position = CGPoint(x: textX, y: textY) + } + + var error: NSError? + guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil, + let displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: currentStyle) else { + return (error, nil) + } + + intrinsicContentSize = intrinsicContentSize(displayList) + displayList.textColor = textColor + + let size = intrinsicContentSize + layoutImage(size: size, displayList: displayList) + + #if os(iOS) || os(visionOS) + let renderer = UIGraphicsImageRenderer(size: size) + let image = renderer.image { rendererContext in + rendererContext.cgContext.saveGState() + rendererContext.cgContext.concatenate(.flippedVertically(size.height)) + displayList.draw(rendererContext.cgContext) + rendererContext.cgContext.restoreGState() + } + return (nil, image) + #endif + #if os(macOS) + let image = NSImage(size: size, flipped: false) { bounds in + guard let context = NSGraphicsContext.current?.cgContext else { return false } + context.saveGState() + displayList.draw(context) + context.restoreGState() + return true + } + return (nil, image) + #endif + } +} +private extension CGAffineTransform { + static func flippedVertically(_ height: CGFloat) -> CGAffineTransform { + var transform = CGAffineTransform(scaleX: 1, y: -1) + transform = transform.translatedBy(x: 0, y: -height) + return transform + } +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathList.swift b/third-party/SwiftMath/Sources/MathRender/MTMathList.swift new file mode 100644 index 0000000000..9feb02db5c --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTMathList.swift @@ -0,0 +1,1058 @@ +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Kostub Deshmukh. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation + +/** + The type of atom in a `MTMathList`. + + The type of the atom determines how it is rendered, and spacing between the atoms. + */ +public enum MTMathAtomType: Int, CustomStringConvertible, Comparable { + /// A number or text in ordinary format - Ord in TeX + case ordinary = 1 + /// A number - Does not exist in TeX + case number + /// A variable (i.e. text in italic format) - Does not exist in TeX + case variable + /// A large operator such as (sin/cos, integral etc.) - Op in TeX + case largeOperator + /// A binary operator - Bin in TeX + case binaryOperator + /// A unary operator - Does not exist in TeX. + case unaryOperator + /// A relation, e.g. = > < etc. - Rel in TeX + case relation + /// Open brackets - Open in TeX + case open + /// Close brackets - Close in TeX + case close + /// A fraction e.g 1/2 - generalized fraction node in TeX + case fraction + /// A radical operator e.g. sqrt(2) + case radical + /// Punctuation such as , - Punct in TeX + case punctuation + /// A placeholder square for future input. Does not exist in TeX + case placeholder + /// An inner atom, i.e. an embedded math list - Inner in TeX + case inner + /// An underlined atom - Under in TeX + case underline + /// An overlined atom - Over in TeX + case overline + /// An accented atom - Accent in TeX + case accent + + // Atoms after this point do not support subscripts or superscripts + + /// A left atom - Left & Right in TeX. We don't need two since we track boundaries separately. + case boundary = 101 + + // Atoms after this are non-math TeX nodes that are still useful in math mode. They do not have + // the usual structure. + + /// Spacing between math atoms. This denotes both glue and kern for TeX. We do not + /// distinguish between glue and kern. + case space = 201 + + /// Denotes style changes during rendering. + case style + case color + case textcolor + case colorBox + + // Atoms after this point are not part of TeX and do not have the usual structure. + + /// An table atom. This atom does not exist in TeX. It is equivalent to the TeX command + /// halign which is handled outside of the TeX math rendering engine. We bring it into our + /// math typesetting to handle matrices and other tables. + case table = 1001 + + func isNotBinaryOperator() -> Bool { + switch self { + case .binaryOperator, .relation, .open, .punctuation, .largeOperator: return true + default: return false + } + } + + func isScriptAllowed() -> Bool { self < .boundary } + + // we want string representations to be capitalized + public var description: String { + switch self { + case .ordinary: return "Ordinary" + case .number: return "Number" + case .variable: return "Variable" + case .largeOperator: return "Large Operator" + case .binaryOperator: return "Binary Operator" + case .unaryOperator: return "Unary Operator" + case .relation: return "Relation" + case .open: return "Open" + case .close: return "Close" + case .fraction: return "Fraction" + case .radical: return "Radical" + case .punctuation: return "Punctuation" + case .placeholder: return "Placeholder" + case .inner: return "Inner" + case .underline: return "Underline" + case .overline: return "Overline" + case .accent: return "Accent" + case .boundary: return "Boundary" + case .space: return "Space" + case .style: return "Style" + case .color: return "Color" + case .textcolor: return "TextColor" + case .colorBox: return "Colorbox" + case .table: return "Table" + } + } + + // comparable support + public static func < (lhs: MTMathAtomType, rhs: MTMathAtomType) -> Bool { lhs.rawValue < rhs.rawValue } +} + +/** + The font style of a character. + + The fontstyle of the atom determines what style the character is rendered in. This only applies to atoms + of type kMTMathAtomVariable and kMTMathAtomNumber. None of the other atom types change their font style. + */ +public enum MTFontStyle:Int { + /// The default latex rendering style. i.e. variables are italic and numbers are roman. + case defaultStyle = 0, + /// Roman font style i.e. \mathrm + roman, + /// Bold font style i.e. \mathbf + bold, + /// Caligraphic font style i.e. \mathcal + caligraphic, + /// Typewriter (monospace) style i.e. \mathtt + typewriter, + /// Italic style i.e. \mathit + italic, + /// San-serif font i.e. \mathss + sansSerif, + /// Fractur font i.e \mathfrak + fraktur, + /// Blackboard font i.e. \mathbb + blackboard, + /// Bold italic + boldItalic +} + +// MARK: - MTMathAtom + +/** A `MTMathAtom` is the basic unit of a math list. Each atom represents a single character + or mathematical operator in a list. However certain atoms can represent more complex structures + such as fractions and radicals. Each atom has a type which determines how the atom is rendered and + a nucleus. The nucleus contains the character(s) that need to be rendered. However the nucleus may + be empty for certain types of atoms. An atom has an optional subscript or superscript which represents + the subscript or superscript that is to be rendered. + + Certain types of atoms inherit from `MTMathAtom` and may have additional fields. + */ +public class MTMathAtom: NSObject { + /** The type of the atom. */ + public var type = MTMathAtomType.ordinary + /** An optional subscript. */ + public var subScript: MTMathList? { + didSet { + if subScript != nil && !self.isScriptAllowed() { + subScript = nil + NSException(name: NSExceptionName(rawValue: "Error"), reason: "Subscripts not allowed for atom of type \(self.type)").raise() + } + } + } + /** An optional superscript. */ + public var superScript: MTMathList? { + didSet { + if superScript != nil && !self.isScriptAllowed() { + superScript = nil + NSException(name: NSExceptionName(rawValue: "Error"), reason: "Superscripts not allowed for atom of type \(self.type)").raise() + } + } + } + + /** The nucleus of the atom. */ + public var nucleus: String = "" + + /// The index range in the MTMathList this MTMathAtom tracks. This is used by the finalizing and preprocessing steps + /// which fuse MTMathAtoms to track the position of the current MTMathAtom in the original list. + public var indexRange = NSRange(location: 0, length: 0) // indexRange in list that this atom tracks: + + /** The font style to be used for the atom. */ + var fontStyle: MTFontStyle = .defaultStyle + + /// If this atom was formed by fusion of multiple atoms, then this stores the list of atoms that were fused to create this one. + /// This is used in the finalizing and preprocessing steps. + var fusedAtoms = [MTMathAtom]() + + init(_ atom:MTMathAtom?) { + guard let atom = atom else { return } + self.type = atom.type + self.nucleus = atom.nucleus + self.subScript = MTMathList(atom.subScript) + self.superScript = MTMathList(atom.superScript) + self.indexRange = atom.indexRange + self.fontStyle = atom.fontStyle + self.fusedAtoms = atom.fusedAtoms + } + + override init() { } + + /// Factory function to create an atom with a given type and value. + /// - parameter type: The type of the atom to instantiate. + /// - parameter value: The value of the atoms nucleus. The value is ignored for fractions and radicals. + init(type:MTMathAtomType, value:String) { + self.type = type + self.nucleus = type == .radical ? "" : value + } + + /// Returns a copy of `self`. + public func copy() -> MTMathAtom { + switch self.type { + case .largeOperator: + return MTLargeOperator(self as? MTLargeOperator) + case .fraction: + return MTFraction(self as? MTFraction) + case .radical: + return MTRadical(self as? MTRadical) + case .style: + return MTMathStyle(self as? MTMathStyle) + case .inner: + return MTInner(self as? MTInner) + case .underline: + return MTUnderLine(self as? MTUnderLine) + case .overline: + return MTOverLine(self as? MTOverLine) + case .accent: + return MTAccent(self as? MTAccent) + case .space: + return MTMathSpace(self as? MTMathSpace) + case .color: + return MTMathColor(self as? MTMathColor) + case .textcolor: + return MTMathTextColor(self as? MTMathTextColor) + case .colorBox: + return MTMathColorbox(self as? MTMathColorbox) + case .table: + return MTMathTable(self as! MTMathTable) + default: + return MTMathAtom(self) + } + } + + public override var description: String { + var string = "" + string += self.nucleus + if self.superScript != nil { + string += "^{\(self.superScript!.description)}" + } + if self.subScript != nil { + string += "_{\(self.subScript!.description)}" + } + return string + } + + /// Returns a finalized copy of the atom + public var finalized: MTMathAtom { + let finalized : MTMathAtom = self.copy() + finalized.superScript = finalized.superScript?.finalized + finalized.subScript = finalized.subScript?.finalized + return finalized + } + + public var string:String { + var str = self.nucleus + if let superScript = self.superScript { + str.append("^{\(superScript.string)}") + } + if let subScript = self.subScript { + str.append("_{\(subScript.string)}") + } + return str + } + + // Fuse the given atom with this one by combining their nucleii. + func fuse(with atom: MTMathAtom) { + assert(self.subScript == nil, "Cannot fuse into an atom which has a subscript: \(self)"); + assert(self.superScript == nil, "Cannot fuse into an atom which has a superscript: \(self)"); + assert(atom.type == self.type, "Only atoms of the same type can be fused. \(self), \(atom)"); + guard self.subScript == nil, self.superScript == nil, self.type == atom.type + else { return } + + // Update the fused atoms list + if self.fusedAtoms.isEmpty { + self.fusedAtoms.append(MTMathAtom(self)) + } + if atom.fusedAtoms.count > 0 { + self.fusedAtoms.append(contentsOf: atom.fusedAtoms) + } else { + self.fusedAtoms.append(atom) + } + + // Update nucleus: + self.nucleus += atom.nucleus + + // Update range: + self.indexRange.length += atom.indexRange.length + + // Update super/subscript: + self.superScript = atom.superScript + self.subScript = atom.subScript + } + + /** Returns true if this atom allows scripts (sub or super). */ + func isScriptAllowed() -> Bool { self.type.isScriptAllowed() } + + func isNotBinaryOperator() -> Bool { self.type.isNotBinaryOperator() } + +} + +func isNotBinaryOperator(_ prevNode:MTMathAtom?) -> Bool { + guard let prevNode = prevNode else { return true } + return prevNode.type.isNotBinaryOperator() +} + +// MARK: - MTFraction + +public class MTFraction: MTMathAtom { + public var hasRule: Bool = true + public var leftDelimiter = "" + public var rightDelimiter = "" + public var numerator: MTMathList? + public var denominator: MTMathList? + + // Continued fraction properties + public var isContinuedFraction: Bool = false + public var alignment: String = "c" // "l", "r", "c" for left, right, center + + init(_ frac: MTFraction?) { + super.init(frac) + self.type = .fraction + if let frac = frac { + self.numerator = MTMathList(frac.numerator) + self.denominator = MTMathList(frac.denominator) + self.hasRule = frac.hasRule + self.leftDelimiter = frac.leftDelimiter + self.rightDelimiter = frac.rightDelimiter + self.isContinuedFraction = frac.isContinuedFraction + self.alignment = frac.alignment + } + } + + init(hasRule rule:Bool = true) { + super.init() + self.type = .fraction + self.hasRule = rule + } + + override public var description: String { + var string = self.hasRule ? "\\frac" : "\\atop" + if !self.leftDelimiter.isEmpty { + string += "[\(self.leftDelimiter)]" + } + if !self.rightDelimiter.isEmpty { + string += "[\(self.rightDelimiter)]" + } + string += "{\(self.numerator?.description ?? "placeholder")}{\(self.denominator?.description ?? "placeholder")}" + if self.superScript != nil { + string += "^{\(self.superScript!.description)}" + } + if self.subScript != nil { + string += "_{\(self.subScript!.description)}" + } + return string + } + + override public var finalized: MTMathAtom { + let newFrac = super.finalized as! MTFraction + newFrac.numerator = newFrac.numerator?.finalized + newFrac.denominator = newFrac.denominator?.finalized + return newFrac + } + +} + +// MARK: - MTRadical +/** An atom of type radical (square root). */ +public class MTRadical: MTMathAtom { + /// Denotes the term under the square root sign + public var radicand: MTMathList? + + /// Denotes the degree of the radical, i.e. the value to the top left of the radical sign + /// This can be null if there is no degree. + public var degree: MTMathList? + + init(_ rad:MTRadical?) { + super.init(rad) + self.type = .radical + self.radicand = MTMathList(rad?.radicand) + self.degree = MTMathList(rad?.degree) + self.nucleus = "" + } + + override init() { + super.init() + self.type = .radical + self.nucleus = "" + } + + override public var description: String { + var string = "\\sqrt" + if self.degree != nil { + string += "[\(self.degree!.description)]" + } + if self.radicand != nil { + string += "{\(self.radicand?.description ?? "placeholder")}" + } + if self.superScript != nil { + string += "^{\(self.superScript!.description)}" + } + if self.subScript != nil { + string += "_{\(self.subScript!.description)}" + } + return string + } + + override public var finalized: MTMathAtom { + let newRad = super.finalized as! MTRadical + newRad.radicand = newRad.radicand?.finalized + newRad.degree = newRad.degree?.finalized + return newRad + } +} + +// MARK: - MTLargeOperator +/** A `MTMathAtom` of type `kMTMathAtom.largeOperator`. */ +public class MTLargeOperator: MTMathAtom { + + /** Indicates whether the limits (if present) should be displayed + above and below the operator in display mode. If limits is false + then the limits (if present) are displayed like a regular subscript/superscript. + */ + public var limits: Bool = false + + init(_ op:MTLargeOperator?) { + super.init(op) + self.type = .largeOperator + self.limits = op!.limits + } + + init(value: String, limits: Bool) { + super.init(type: .largeOperator, value: value) + self.limits = limits + } +} + +// MARK: - MTInner +/** An inner atom. This denotes an atom which contains a math list inside it. An inner atom + has optional boundaries. Note: Only one boundary may be present, it is not required to have + both. */ +public class MTInner: MTMathAtom { + /// The inner math list + public var innerList: MTMathList? + /// The left boundary atom. This must be a node of type kMTMathAtomBoundary + public var leftBoundary: MTMathAtom? { + didSet { + if let left = leftBoundary, left.type != .boundary { + leftBoundary = nil + NSException(name: NSExceptionName(rawValue: "Error"), reason: "Left boundary must be of type .boundary").raise() + } + } + } + /// The right boundary atom. This must be a node of type kMTMathAtomBoundary + public var rightBoundary: MTMathAtom? { + didSet { + if let right = rightBoundary, right.type != .boundary { + rightBoundary = nil + NSException(name: NSExceptionName(rawValue: "Error"), reason: "Right boundary must be of type .boundary").raise() + } + } + } + + /// Optional explicit delimiter height (in points). When set, this overrides the automatic + /// delimiter sizing based on inner content. Used by \big, \Big, \bigg, \Bigg commands. + public var delimiterHeight: CGFloat? + + init(_ inner:MTInner?) { + super.init(inner) + self.type = .inner + self.innerList = MTMathList(inner?.innerList) + self.leftBoundary = MTMathAtom(inner?.leftBoundary) + self.rightBoundary = MTMathAtom(inner?.rightBoundary) + self.delimiterHeight = inner?.delimiterHeight + } + + override init() { + super.init() + self.type = .inner + } + + override public var description: String { + var string = "\\inner" + if self.leftBoundary != nil { + string += "[\(self.leftBoundary!.nucleus)]" + } + string += "{\(self.innerList!.description)}" + if self.rightBoundary != nil { + string += "[\(self.rightBoundary!.nucleus)]" + } + if self.superScript != nil { + string += "^{\(self.superScript!.description)}" + } + if self.subScript != nil { + string += "_{\(self.subScript!.description)}" + } + return string + } + + override public var finalized: MTMathAtom { + let newInner = super.finalized as! MTInner + newInner.innerList = newInner.innerList?.finalized + return newInner + } +} + +// MARK: - MTOverLIne +/** An atom with a line over the contained math list. */ +public class MTOverLine: MTMathAtom { + public var innerList: MTMathList? + + override public var finalized: MTMathAtom { + let newOverline = MTOverLine(self) + newOverline.innerList = newOverline.innerList?.finalized + return newOverline + } + + init(_ over: MTOverLine?) { + super.init(over) + self.type = .overline + self.innerList = MTMathList(over!.innerList) + } + + override init() { + super.init() + self.type = .overline + } +} + +// MARK: - MTUnderLine +/** An atom with a line under the contained math list. */ +public class MTUnderLine: MTMathAtom { + public var innerList: MTMathList? + + override public var finalized: MTMathAtom { + let newUnderline = super.finalized as! MTUnderLine + newUnderline.innerList = newUnderline.innerList?.finalized + return newUnderline + } + + init(_ under: MTUnderLine?) { + super.init(under) + self.type = .underline + self.innerList = MTMathList(under?.innerList) + } + + override init() { + super.init() + self.type = .underline + } +} + +// MARK: - MTAccent + +public class MTAccent: MTMathAtom { + public var innerList: MTMathList? + /// Indicates if this accent should use stretchy arrow behavior (for \overrightarrow, etc.) + /// vs short accent behavior (for \vec). Only applies to arrow accents. + public var isStretchy: Bool = false + /// Indicates if this accent should use wide stretching behavior (for \widehat, \widetilde) + /// vs regular fixed-size accent behavior (for \hat, \tilde). + public var isWide: Bool = false + + override public var finalized: MTMathAtom { + let newAccent = super.finalized as! MTAccent + newAccent.innerList = newAccent.innerList?.finalized + newAccent.isStretchy = self.isStretchy + newAccent.isWide = self.isWide + return newAccent + } + + init(_ accent: MTAccent?) { + super.init(accent) + self.type = .accent + self.innerList = MTMathList(accent?.innerList) + self.isStretchy = accent?.isStretchy ?? false + self.isWide = accent?.isWide ?? false + } + + init(value: String) { + super.init() + self.type = .accent + self.nucleus = value + } +} + +// MARK: - MTMathSpace +/** An atom representing space. + Note: None of the usual fields of the `MTMathAtom` apply even though this + class inherits from `MTMathAtom`. i.e. it is meaningless to have a value + in the nucleus, subscript or superscript fields. */ +public class MTMathSpace: MTMathAtom { + /** The amount of space represented by this object in mu units. */ + public var space: CGFloat = 0 + + /// Creates a new `MTMathSpace` with the given spacing. + /// - parameter space: The amount of space in mu units. + init(_ space: MTMathSpace?) { + super.init(space) + self.type = .space + self.space = space?.space ?? 0 + } + + init(space:CGFloat) { + super.init() + self.type = .space + self.space = space + } +} + +/** + Styling of a line of math + */ +public enum MTLineStyle:Int, Comparable { + /// Display style + case display + /// Text style (inline) + case text + /// Script style (for sub/super scripts) + case script + /// Script script style (for scripts of scripts) + case scriptOfScript + + public func inc() -> MTLineStyle { + let raw = self.rawValue + 1 + if let style = MTLineStyle(rawValue: raw) { return style } + return .display + } + + public var isNotScript:Bool { self < .script } + public static func < (lhs: MTLineStyle, rhs: MTLineStyle) -> Bool { lhs.rawValue < rhs.rawValue } +} + +// MARK: - MTMathStyle +/** An atom representing a style change. + Note: None of the usual fields of the `MTMathAtom` apply even though this + class inherits from `MTMathAtom`. i.e. it is meaningless to have a value + in the nucleus, subscript or superscript fields. */ +public class MTMathStyle: MTMathAtom { + public var style: MTLineStyle = .display + + init(_ style:MTMathStyle?) { + super.init(style) + self.type = .style + self.style = style!.style + } + + init(style:MTLineStyle) { + super.init() + self.type = .style + self.style = style + } +} + +// MARK: - MTMathColor +/** An atom representing an color element. + Note: None of the usual fields of the `MTMathAtom` apply even though this + class inherits from `MTMathAtom`. i.e. it is meaningless to have a value + in the nucleus, subscript or superscript fields. */ +public class MTMathColor: MTMathAtom { + public var colorString:String="" + public var innerList:MTMathList? + + init(_ color: MTMathColor?) { + super.init(color) + self.type = .color + self.colorString = color?.colorString ?? "" + self.innerList = MTMathList(color?.innerList) + } + + override init() { + super.init() + self.type = .color + } + + public override var string: String { + "\\color{\(self.colorString)}{\(self.innerList!.string)}" + } + + override public var finalized: MTMathAtom { + let newColor = super.finalized as! MTMathColor + newColor.innerList = newColor.innerList?.finalized + return newColor + } +} + +// MARK: - MTMathTextColor +/** An atom representing an textcolor element. + Note: None of the usual fields of the `MTMathAtom` apply even though this + class inherits from `MTMathAtom`. i.e. it is meaningless to have a value + in the nucleus, subscript or superscript fields. */ +public class MTMathTextColor: MTMathAtom { + public var colorString:String="" + public var innerList:MTMathList? + + init(_ color: MTMathTextColor?) { + super.init(color) + self.type = .textcolor + self.colorString = color?.colorString ?? "" + self.innerList = MTMathList(color?.innerList) + } + + override init() { + super.init() + self.type = .textcolor + } + + public override var string: String { + "\\textcolor{\(self.colorString)}{\(self.innerList!.string)}" + } + + override public var finalized: MTMathAtom { + let newColor = super.finalized as! MTMathTextColor + newColor.innerList = newColor.innerList?.finalized + return newColor + } +} + +// MARK: - MTMathColorbox +/** An atom representing an colorbox element. + Note: None of the usual fields of the `MTMathAtom` apply even though this + class inherits from `MTMathAtom`. i.e. it is meaningless to have a value + in the nucleus, subscript or superscript fields. */ +public class MTMathColorbox: MTMathAtom { + public var colorString="" + public var innerList:MTMathList? + + init(_ cbox: MTMathColorbox?) { + super.init(cbox) + self.type = .colorBox + self.colorString = cbox?.colorString ?? "" + self.innerList = MTMathList(cbox?.innerList) + } + + override init() { + super.init() + self.type = .colorBox + } + + public override var string: String { + "\\colorbox{\(self.colorString)}{\(self.innerList!.string)}" + } + + override public var finalized: MTMathAtom { + let newColor = super.finalized as! MTMathColorbox + newColor.innerList = newColor.innerList?.finalized + return newColor + } +} + +/** + Alignment for a column of MTMathTable + */ +public enum MTColumnAlignment { + case left + case center + case right +} + +// MARK: - MTMathTable +/** An atom representing an table element. This atom is not like other + atoms and is not present in TeX. We use it to represent the `\halign` command + in TeX with some simplifications. This is used for matrices, equation + alignments and other uses of multiline environments. + + The cells in the table are represented as a two dimensional array of + `MTMathList` objects. The `MTMathList`s could be empty to denote a missing + value in the cell. Additionally an array of alignments indicates how each + column will be aligned. + */ +public class MTMathTable: MTMathAtom { + /// The alignment for each column (left, right, center). The default alignment + /// for a column (if not set) is center. + public var alignments = [MTColumnAlignment]() + /// The cells in the table as a two dimensional array. + public var cells = [[MTMathList]]() + /// The name of the environment that this table denotes. + public var environment = "" + /// Spacing between each column in mu units. + public var interColumnSpacing: CGFloat = 0 + /// Additional spacing between rows in jots (one jot is 0.3 times font size). + /// If the additional spacing is 0, then normal row spacing is used are used. + public var interRowAdditionalSpacing: CGFloat = 0 + + override public var finalized: MTMathAtom { + let table = super.finalized as! MTMathTable + for var row in table.cells { + for i in 0.. MTColumnAlignment { + if self.alignments.count <= col { + return MTColumnAlignment.center + } else { + return self.alignments[col] + } + } + + public var numColumns: Int { + var numberOfCols = 0 + for row in self.cells { + numberOfCols = max(numberOfCols, row.count) + } + return numberOfCols + } + + public var numRows: Int { self.cells.count } +} + +// MARK: - MTMathList + +extension MTMathList { + public override var description: String { self.atoms.description } + /// converts the MTMathList to a string form. Note: This is not the LaTeX form. + public var string: String { self.description } +} + +/** A representation of a list of math objects. + + This list can be constructed directly or built with + the help of the MTMathListBuilder. It is not required that the mathematics represented make sense + (i.e. this can represent something like "x 2 = +". This list can be used for display using MTLine + or can be a list of tokens to be used by a parser after finalizedMathList is called. + + Note: This class is for **advanced** usage only. + */ +public class MTMathList : NSObject { + + init?(_ list:MTMathList?) { + guard let list = list else { return nil } + for atom in list.atoms { + self.atoms.append(atom.copy()) + } + } + + /// A list of MathAtoms + public var atoms = [MTMathAtom]() + + /// Create a new math list as a final expression and update atoms + /// by combining like atoms that occur together and converting unary operators to binary operators. + /// This function does not modify the current MTMathList + public var finalized: MTMathList { + let finalizedList = MTMathList() + let zeroRange = NSMakeRange(0, 0) + + var prevNode: MTMathAtom? = nil + for atom in self.atoms { + let newNode = atom.finalized + + if NSEqualRanges(zeroRange, atom.indexRange) { + // CRITICAL FIX: Check if prevNode has a valid range location before using it + // If location is NSNotFound, treat as if there's no prevNode + // This prevents negative overflow when creating NSMakeRange + let index: Int + if prevNode == nil || prevNode!.indexRange.location == NSNotFound { + index = 0 + } else { + // Additional safety: check for potential overflow + let location = prevNode!.indexRange.location + let length = prevNode!.indexRange.length + // If either value is suspicious (negative or too large), reset to 0 + if location < 0 || length < 0 || location > Int.max - length { + index = 0 + } else { + index = location + length + } + } + newNode.indexRange = NSMakeRange(index, 1) + } + + switch newNode.type { + case .binaryOperator: + if isNotBinaryOperator(prevNode) { + newNode.type = .unaryOperator + } + case .relation, .punctuation, .close: + if prevNode != nil && prevNode!.type == .binaryOperator { + prevNode!.type = .unaryOperator + } + case .number: + if prevNode != nil && prevNode!.type == .number && prevNode!.subScript == nil && prevNode!.superScript == nil { + prevNode!.fuse(with: newNode) + continue // skip the current node, we are done here. + } + default: break + } + finalizedList.add(newNode) + prevNode = newNode + } + if prevNode != nil && prevNode!.type == .binaryOperator { + prevNode!.type = .unaryOperator + } + return finalizedList + } + + public init(atoms: [MTMathAtom]) { + self.atoms.append(contentsOf: atoms) + } + + public init(atom: MTMathAtom) { + self.atoms.append(atom) + } + + public override init() { super.init() } + + func NSParamException(_ param:Any?) { + if param == nil { + NSException(name: NSExceptionName(rawValue: "Error"), reason: "Parameter cannot be nil").raise() + } + } + + func NSIndexException(_ array:[Any], index: Int) { + guard !array.indices.contains(index) else { return } + NSException(name: NSExceptionName(rawValue: "Error"), reason: "Index \(index) out of bounds").raise() + } + + /// Add an atom to the end of the list. + /// - parameter atom: The atom to be inserted. This cannot be `nil` and cannot have the type `kMTMathAtomBoundary`. + /// - throws NSException if the atom is of type `kMTMathAtomBoundary` + public func add(_ atom: MTMathAtom?) { + guard let atom = atom else { return } + if self.isAtomAllowed(atom) { + self.atoms.append(atom) + } else { + NSException(name: NSExceptionName(rawValue: "Error"), reason: "Cannot add atom of type \(atom.type.rawValue) into mathlist").raise() + } + } + + /// Inserts an atom at the given index. If index is already occupied, the objects at index and beyond are + /// shifted by adding 1 to their indices to make room. An insert to an `index` greater than the number of atoms + /// is ignored. Insertions of nil atoms is ignored. + /// - parameter atom: The atom to be inserted. This cannot be `nil` and cannot have the type `kMTMathAtom.boundary`. + /// - parameter index: The index where the atom is to be inserted. The index should be less than or equal to the + /// number of elements in the math list. + /// - throws NSException if the atom is of type kMTMathAtomBoundary + public func insert(_ atom: MTMathAtom?, at index: Int) { + // NSParamException(atom) + guard let atom = atom else { return } + guard self.atoms.indices.contains(index) || index == self.atoms.endIndex else { return } + // guard self.atoms.endIndex >= index else { NSIndexException(); return } + if self.isAtomAllowed(atom) { + // NSIndexException(self.atoms, index: index) + self.atoms.insert(atom, at: index) + } else { + NSException(name: NSExceptionName(rawValue: "Error"), reason: "Cannot add atom of type \(atom.type.rawValue) into mathlist").raise() + } + } + + /// Append the given list to the end of the current list. + /// - parameter list: The list to append. + public func append(_ list: MTMathList?) { + guard let list = list else { return } + self.atoms += list.atoms + } + + /** Removes the last atom from the math list. If there are no atoms in the list this does nothing. */ + public func removeLastAtom() { + if !self.atoms.isEmpty { + self.atoms.removeLast() + } + } + + /// Removes the atom at the given index. + /// - parameter index: The index at which to remove the atom. Must be less than the number of atoms + /// in the list. + public func removeAtom(at index: Int) { + NSIndexException(self.atoms, index:index) + self.atoms.remove(at: index) + } + + /** Removes all the atoms within the given range. */ + public func removeAtoms(in range: ClosedRange) { + NSIndexException(self.atoms, index: range.lowerBound) + NSIndexException(self.atoms, index: range.upperBound) + self.atoms.removeSubrange(range) + } + + func isAtomAllowed(_ atom: MTMathAtom?) -> Bool { atom?.type != .boundary } +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathListBuilder.swift b/third-party/SwiftMath/Sources/MathRender/MTMathListBuilder.swift new file mode 100644 index 0000000000..4083fd90d9 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTMathListBuilder.swift @@ -0,0 +1,1613 @@ +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Kostub Deshmukh. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation + +/** `MTMathListBuilder` is a class for parsing LaTeX into an `MTMathList` that + can be rendered and processed mathematically. + */ +struct MTEnvProperties { + var envName: String? + var ended: Bool + var numRows: Int + var alignment: MTColumnAlignment? // Optional alignment for starred matrix environments + + init(name: String?, alignment: MTColumnAlignment? = nil) { + self.envName = name + self.numRows = 0 + self.ended = false + self.alignment = alignment + } +} + +/** + The error encountered when parsing a LaTeX string. + + The `code` in the `NSError` is one of the following indicating why the LaTeX string + could not be parsed. + */ +enum MTParseErrors:Int { + /// The braces { } do not match. + case mismatchBraces = 1 + /// A command in the string is not recognized. + case invalidCommand + /// An expected character such as ] was not found. + case characterNotFound + /// The \left or \right command was not followed by a delimiter. + case missingDelimiter + /// The delimiter following \left or \right was not a valid delimiter. + case invalidDelimiter + /// There is no \right corresponding to the \left command. + case missingRight + /// There is no \left corresponding to the \right command. + case missingLeft + /// The environment given to the \begin command is not recognized + case invalidEnv + /// A command is used which is only valid inside a \begin,\end environment + case missingEnv + /// There is no \begin corresponding to the \end command. + case missingBegin + /// There is no \end corresponding to the \begin command. + case missingEnd + /// The number of columns do not match the environment + case invalidNumColumns + /// Internal error, due to a programming mistake. + case internalError + /// Limit control applied incorrectly + case invalidLimits +} + +let MTParseError = "ParseError" + +/** `MTMathListBuilder` is a class for parsing LaTeX into an `MTMathList` that + can be rendered and processed mathematically. + */ +public struct MTMathListBuilder { + /// The math mode determines rendering style (inline vs display) + enum MathMode { + /// Display style - larger operators, limits above/below (e.g., $$...$$, \[...\]) + case display + /// Inline/text style - compact operators, limits to the side (e.g., $...$, \(...\)) + case inline + + /// Convert MathMode to MTLineStyle for rendering + func toLineStyle() -> MTLineStyle { + switch self { + case .display: + return .display + case .inline: + return .text + } + } + } + + var string: String + var currentCharIndex: String.Index + var currentInnerAtom: MTInner? + var currentEnv: MTEnvProperties? + var currentFontStyle:MTFontStyle + var spacesAllowed:Bool + var mathMode: MathMode = .display + + /** Contains any error that occurred during parsing. */ + var error:NSError? + + // MARK: - Character-handling routines + + var hasCharacters: Bool { currentCharIndex < string.endIndex } + + // gets the next character and increments the index + mutating func getNextCharacter() -> Character { + assert(self.hasCharacters, "Retrieving character at index \(self.currentCharIndex) beyond length \(self.string.count)") + let ch = string[currentCharIndex] + currentCharIndex = string.index(after: currentCharIndex) + return ch + } + + mutating func unlookCharacter() { + assert(currentCharIndex > string.startIndex, "Unlooking when at the first character.") + if currentCharIndex > string.startIndex { + currentCharIndex = string.index(before: currentCharIndex) + } + } + + // Peek at next command without consuming it (for \not lookahead) + mutating func peekNextCommand() -> String { + let savedIndex = currentCharIndex + skipSpaces() + + guard hasCharacters else { + currentCharIndex = savedIndex + return "" + } + + let char = getNextCharacter() + let command: String + + if char == "\\" { + command = readCommand() + } else { + command = "" + } + + // Restore position + currentCharIndex = savedIndex + return command + } + + // Consume the next command (after peeking) + mutating func consumeNextCommand() { + skipSpaces() + + guard hasCharacters else { return } + + let char = getNextCharacter() + if char == "\\" { + _ = readCommand() + } + } + + + + mutating func expectCharacter(_ ch: Character) -> Bool { + MTAssertNotSpace(ch) + self.skipSpaces() + + if self.hasCharacters { + let nextChar = self.getNextCharacter() + MTAssertNotSpace(nextChar) + if nextChar == ch { + return true + } else { + self.unlookCharacter() + return false + } + } + return false + } + + public static let spaceToCommands: [CGFloat: String] = [ + 3 : ",", + 4 : ">", + 5 : ";", + (-3) : "!", + 18 : "quad", + 36 : "qquad", + ] + + public static let styleToCommands: [MTLineStyle: String] = [ + .display: "displaystyle", + .text: "textstyle", + .script: "scriptstyle", + .scriptOfScript: "scriptscriptstyle" + ] + + // Comprehensive mapping of \not command combinations to Unicode negated symbols + public static let notCombinations: [String: String] = [ + // Primary targets (user requested) + "equiv": "\u{2262}", // ≢ Not equivalent + "subset": "\u{2284}", // ⊄ Not subset + "in": "\u{2209}", // ∉ Not element of + + // Additional standard negations + "sim": "\u{2241}", // ≁ Not similar + "approx": "\u{2249}", // ≉ Not approximately equal + "cong": "\u{2247}", // ≇ Not congruent + "parallel": "\u{2226}", // ∦ Not parallel + "subseteq": "\u{2288}", // ⊈ Not subset or equal + "supset": "\u{2285}", // ⊅ Not superset + "supseteq": "\u{2289}", // ⊉ Not superset or equal + "=": "\u{2260}", // ≠ Not equal (alternative to \neq) + ] + + /// Delimiter sizing commands with their size multipliers (relative to font size). + /// Values based on standard TeX: at 10pt, \big=8.5pt, \Big=11.5pt, \bigg=14.5pt, \Bigg=17.5pt + /// These translate to approximately 0.85x, 1.15x, 1.45x, 1.75x of font size. + /// We use slightly larger values to ensure visible size differences. + public static let delimiterSizeCommands: [String: CGFloat] = [ + // Basic sizing commands + "big": 1.0, + "Big": 1.4, + "bigg": 1.8, + "Bigg": 2.2, + // Left variants (same sizes, just semantic distinction in LaTeX) + "bigl": 1.0, + "Bigl": 1.4, + "biggl": 1.8, + "Biggl": 2.2, + // Right variants + "bigr": 1.0, + "Bigr": 1.4, + "biggr": 1.8, + "Biggr": 2.2, + // Middle variants (used between delimiters) + "bigm": 1.0, + "Bigm": 1.4, + "biggm": 1.8, + "Biggm": 2.2, + ] + + init(string: String) { + self.error = nil + self.string = string + self.currentCharIndex = string.startIndex + self.currentFontStyle = .defaultStyle + self.spacesAllowed = false + } + + // MARK: - Delimiter Detection + + /// Detects and strips LaTeX math delimiters from the input string. + /// Returns the cleaned content and the detected math mode. + /// Supports: $...$ \(...\) $$...$$ \[...\] and environments + func detectAndStripDelimiters(from str: String) -> (String, MathMode) { + let trimmed = str.trimmingCharacters(in: .whitespacesAndNewlines) + + // Check display delimiters first (more specific patterns) + + // \[...\] - LaTeX display math + if trimmed.hasPrefix("\\[") && trimmed.hasSuffix("\\]") && trimmed.count > 4 { + let content = String(trimmed.dropFirst(2).dropLast(2)) + return (content, .display) + } + + // $$...$$ - TeX display math (check before single $) + if trimmed.hasPrefix("$$") && trimmed.hasSuffix("$$") && trimmed.count > 4 { + let content = String(trimmed.dropFirst(2).dropLast(2)) + return (content, .display) + } + + // Check inline delimiters + + // \(...\) - LaTeX inline math + if trimmed.hasPrefix("\\(") && trimmed.hasSuffix("\\)") && trimmed.count > 4 { + let content = String(trimmed.dropFirst(2).dropLast(2)) + return (content, .inline) + } + + // $...$ - TeX inline math (must check after $$) + if trimmed.hasPrefix("$") && trimmed.hasSuffix("$") && trimmed.count > 2 && !trimmed.hasPrefix("$$") { + let content = String(trimmed.dropFirst(1).dropLast(1)) + return (content, .inline) + } + + // Check if it's an environment (\begin{...}\end{...}) + // These are handled by existing logic and are display mode by default + if trimmed.hasPrefix("\\begin{") { + return (str, .display) + } + + // No delimiters found - default to display mode (current behavior for backward compatibility) + return (str, .display) + } + + // MARK: - MTMathList builder functions + + /// Builds a mathlist from the internal `string`. Returns nil if there is an error. + public mutating func build() -> MTMathList? { + // Detect and strip delimiters, updating the string and mode + let (cleanedString, mode) = detectAndStripDelimiters(from: self.string) + self.string = cleanedString + self.currentCharIndex = cleanedString.startIndex + self.mathMode = mode + + // If inline mode, we could optionally prepend a \textstyle command + // to force inline rendering of operators. For now, just track the mode. + + let list = self.buildInternal(false) + if self.hasCharacters && error == nil { + self.setError(.mismatchBraces, message: "Mismatched braces: \(self.string)") + return nil + } + if error != nil { + return nil + } + + // Note: For inline mode, we insert \textstyle to match LaTeX behavior. + // However, fractionStyle() has been modified to keep fractions at the + // same font size in both display and text modes (not one level smaller). + // Large operators show limits above/below in text style due to the updated + // condition in makeLargeOp() that checks both .display and .text styles. + if mode == .inline && list != nil && !list!.atoms.isEmpty { + // Prepend \textstyle to force inline rendering + let styleAtom = MTMathStyle(style: .text) + list!.atoms.insert(styleAtom, at: 0) + } + + return list + } + + /** Construct a math list from a given string. If there is parse error, returns + nil. To retrieve the error use the function `MTMathListBuilder.build(fromString:error:)`. + */ + public static func build(fromString string: String) -> MTMathList? { + var builder = MTMathListBuilder(string: string) + return builder.build() + } + + /** Construct a math list from a given string. If there is an error while + constructing the string, this returns nil. The error is returned in the + `error` parameter. + */ + public static func build(fromString string: String, error:inout NSError?) -> MTMathList? { + var builder = MTMathListBuilder(string: string) + let output = builder.build() + if builder.error != nil { + error = builder.error + return nil + } + return output + } + + /** Construct a math list from a given string and return the detected style. + This method detects LaTeX delimiters like \[...\], $$...$$, $...$, \(...\) + and returns the appropriate rendering style (.display or .text). + + If there is a parse error, returns nil for the MathList. + + - Parameter string: The LaTeX string to parse + - Returns: A tuple containing the parsed MathList and the detected MTLineStyle + */ + public static func buildWithStyle(fromString string: String) -> (mathList: MTMathList?, style: MTLineStyle) { + var builder = MTMathListBuilder(string: string) + let mathList = builder.build() + let style = builder.mathMode.toLineStyle() + return (mathList, style) + } + + /** Construct a math list from a given string and return the detected style. + This method detects LaTeX delimiters like \[...\], $$...$$, $...$, \(...\) + and returns the appropriate rendering style (.display or .text). + + If there is an error while constructing the string, this returns nil for the MathList. + The error is returned in the `error` parameter. + + - Parameters: + - string: The LaTeX string to parse + - error: An inout parameter that will contain any parse error + - Returns: A tuple containing the parsed MathList and the detected MTLineStyle + */ + public static func buildWithStyle(fromString string: String, error: inout NSError?) -> (mathList: MTMathList?, style: MTLineStyle) { + var builder = MTMathListBuilder(string: string) + let output = builder.build() + let style = builder.mathMode.toLineStyle() + if builder.error != nil { + error = builder.error + return (nil, style) + } + return (output, style) + } + + public mutating func buildInternal(_ oneCharOnly: Bool) -> MTMathList? { + self.buildInternal(oneCharOnly, stopChar: nil) + } + + public mutating func buildInternal(_ oneCharOnly: Bool, stopChar stop: Character?) -> MTMathList? { + let list = MTMathList() + assert(!(oneCharOnly && stop != nil), "Cannot set both oneCharOnly and stopChar.") + var prevAtom: MTMathAtom? = nil + while self.hasCharacters { + if error != nil { return nil } // If there is an error thus far then bail out. + + var atom: MTMathAtom? = nil + let char = self.getNextCharacter() + + if oneCharOnly { + if char == "^" || char == "}" || char == "_" || char == "&" { + // this is not the character we are looking for. + // They are meant for the caller to look at. + self.unlookCharacter() + return list + } + } + // If there is a stop character, keep scanning 'til we find it + if stop != nil && char == stop! { + return list + } + + if char == "^" { + assert(!oneCharOnly, "This should have been handled before") + if (prevAtom == nil || prevAtom!.superScript != nil || !prevAtom!.isScriptAllowed()) { + // If there is no previous atom, or if it already has a superscript + // or if scripts are not allowed for it, then add an empty node. + prevAtom = MTMathAtom(type: .ordinary, value: "") + list.add(prevAtom!) + } + // this is a superscript for the previous atom + // note: if the next char is the stopChar it will be consumed by the ^ and so it doesn't count as stop + prevAtom!.superScript = self.buildInternal(true) + continue + } else if char == "_" { + assert(!oneCharOnly, "This should have been handled before") + if (prevAtom == nil || prevAtom!.subScript != nil || !prevAtom!.isScriptAllowed()) { + // If there is no previous atom, or if it already has a subcript + // or if scripts are not allowed for it, then add an empty node. + prevAtom = MTMathAtom(type: .ordinary, value: "") + list.add(prevAtom!) + } + // this is a subscript for the previous atom + // note: if the next char is the stopChar it will be consumed by the _ and so it doesn't count as stop + prevAtom!.subScript = self.buildInternal(true) + continue + } else if char == "{" { + // this puts us in a recursive routine, and sets oneCharOnly to false and no stop character + if let subList = self.buildInternal(false, stopChar: "}") { + prevAtom = subList.atoms.last + list.append(subList) + if oneCharOnly { + return list + } + } + continue + } else if char == "}" { + // \ means a command + assert(!oneCharOnly, "This should have been handled before") + assert(stop == nil, "This should have been handled before") + // Special case: } terminates implicit table (envName == nil) created by \\ + // This happens when \\ is used inside braces: \substack{a \\ b} + if self.currentEnv != nil && self.currentEnv!.envName == nil { + // Mark environment as ended, don't consume the } + self.currentEnv!.ended = true + return list + } + // We encountered a closing brace when there is no stop set, that means there was no + // corresponding opening brace. + self.setError(.mismatchBraces, message:"Mismatched braces.") + return nil + } else if char == "\\" { + let command = readCommand() + let done = stopCommand(command, list:list, stopChar:stop) + if done != nil { + return done + } else if error != nil { + return nil + } + if self.applyModifier(command, atom:prevAtom) { + continue + } + + if let fontStyle = MTMathAtomFactory.fontStyleWithName(command) { + let oldSpacesAllowed = spacesAllowed + // Text has special consideration where it allows spaces without escaping. + spacesAllowed = command == "text" + let oldFontStyle = currentFontStyle + currentFontStyle = fontStyle + if let sublist = self.buildInternal(true) { + // Restore the font style. + currentFontStyle = oldFontStyle + spacesAllowed = oldSpacesAllowed + + prevAtom = sublist.atoms.last + list.append(sublist) + if oneCharOnly { + return list + } + } + continue + } + atom = self.atomForCommand(command) + if atom == nil { + // this was an unknown command, + // we flag an error and return + // (note setError will not set the error if there is already one, so we flag internal error + // in the odd case that an _error is not set. + self.setError(.internalError, message:"Internal error") + return nil + } + } else if char == "&" { + // used for column separation in tables + assert(!oneCharOnly, "This should have been handled before") + if self.currentEnv != nil { + return list + } else { + // Create a new table with the current list and a default env + if let table = self.buildTable(env: nil, firstList: list, isRow: false) { + return MTMathList(atom: table) + } else { + return nil + } + } + } else if spacesAllowed && char == " " { + // If spaces are allowed then spaces do not need escaping with a \ before being used. + atom = MTMathAtomFactory.atom(forLatexSymbol: " ") + } else { + atom = MTMathAtomFactory.atom(forCharacter: char) + if atom == nil { + // Not a recognized character in standard math mode + // In text mode (spacesAllowed && roman style), accept any Unicode character for fallback font support + // This enables Chinese, Japanese, Korean, emoji, etc. in \text{} commands + if spacesAllowed && currentFontStyle == .roman { + atom = MTMathAtom(type: .ordinary, value: String(char)) + } else { + // In math mode or non-text commands, skip unrecognized characters + continue + } + } + } + + assert(atom != nil, "Atom shouldn't be nil") + atom?.fontStyle = currentFontStyle + // If this is an accent atom (e.g., from an accented character like "é"), + // propagate the font style to the inner list atoms that don't already have + // an explicit font style. This handles Unicode accented characters which are + // converted to accents by atom(fromAccentedCharacter:) without font style context. + // We only set font style on atoms with .defaultStyle to avoid overriding + // explicit font style commands like \textbf inside accents. + if let accent = atom as? MTAccent, let innerList = accent.innerList { + for innerAtom in innerList.atoms { + if innerAtom.fontStyle == .defaultStyle { + innerAtom.fontStyle = currentFontStyle + } + } + } + list.add(atom) + prevAtom = atom + + if oneCharOnly { + return list + } + } + if stop != nil { + if stop == "}" { + // We did not find a corresponding closing brace. + self.setError(.mismatchBraces, message:"Missing closing brace") + } else { + // we never found our stop character + let errorMessage = "Expected character not found: \(stop!)" + self.setError(.characterNotFound, message:errorMessage) + } + } + return list + } + + + // MARK: - MTMathList to LaTeX conversion + + /// This converts the MTMathList to LaTeX. + public static func mathListToString(_ ml: MTMathList?) -> String { + var str = "" + var currentfontStyle = MTFontStyle.defaultStyle + if let atomList = ml { + for atom in atomList.atoms { + if currentfontStyle != atom.fontStyle { + if currentfontStyle != .defaultStyle { + str += "}" + } + if atom.fontStyle != .defaultStyle { + let fontStyleName = MTMathAtomFactory.fontNameForStyle(atom.fontStyle) + str += "\\\(fontStyleName){" + } + currentfontStyle = atom.fontStyle + } + if atom.type == .fraction { + if let frac = atom as? MTFraction { + if frac.isContinuedFraction { + // Generate \cfrac with optional alignment + if frac.alignment != "c" { + str += "\\cfrac[\(frac.alignment)]{\(mathListToString(frac.numerator!))}{\(mathListToString(frac.denominator!))}" + } else { + str += "\\cfrac{\(mathListToString(frac.numerator!))}{\(mathListToString(frac.denominator!))}" + } + } else if frac.hasRule { + str += "\\frac{\(mathListToString(frac.numerator!))}{\(mathListToString(frac.denominator!))}" + } else { + let command: String + if frac.leftDelimiter.isEmpty && frac.rightDelimiter.isEmpty { + command = "atop" + } else if frac.leftDelimiter == "(" && frac.rightDelimiter == ")" { + command = "choose" + } else if frac.leftDelimiter == "{" && frac.rightDelimiter == "}" { + command = "brace" + } else if frac.leftDelimiter == "[" && frac.rightDelimiter == "]" { + command = "brack" + } else { + command = "atopwithdelims\(frac.leftDelimiter)\(frac.rightDelimiter)" + } + str += "{\(mathListToString(frac.numerator!)) \\\(command) \(mathListToString(frac.denominator!))}" + } + } + } else if atom.type == .radical { + str += "\\sqrt" + if let rad = atom as? MTRadical { + if rad.degree != nil { + str += "[\(mathListToString(rad.degree!))]" + } + str += "{\(mathListToString(rad.radicand!))}" + } + } else if atom.type == .inner { + if let inner = atom as? MTInner { + if inner.leftBoundary != nil || inner.rightBoundary != nil { + if inner.leftBoundary != nil { + str += "\\left\(delimToString(delim: inner.leftBoundary!)) " + } else { + str += "\\left. " + } + + str += mathListToString(inner.innerList!) + + if inner.rightBoundary != nil { + str += "\\right\(delimToString(delim: inner.rightBoundary!)) " + } else { + str += "\\right. " + } + } else { + str += "{\(mathListToString(inner.innerList!))}" + } + } + } else if atom.type == .table { + if let table = atom as? MTMathTable { + if !table.environment.isEmpty { + str += "\\begin{\(table.environment)}" + } + + for i in 0..= 1 && cell.atoms[0].type == .style { + // remove first atom + cell.atoms.removeFirst() + } + } + if table.environment == "eqalign" || table.environment == "aligned" || table.environment == "split" { + if j == 1 && cell.atoms.count >= 1 && cell.atoms[0].type == .ordinary && cell.atoms[0].nucleus.count == 0 { + // remove empty nucleus added for spacing + cell.atoms.removeFirst() + } + } + str += mathListToString(cell) + if j < row.count - 1 { + str += "&" + } + } + if i < table.numRows - 1 { + str += "\\\\ " + } + } + if !table.environment.isEmpty { + str += "\\end{\(table.environment)}" + } + } + } else if atom.type == .overline { + if let overline = atom as? MTOverLine { + str += "\\overline" + str += "{\(mathListToString(overline.innerList!))}" + } + } else if atom.type == .underline { + if let underline = atom as? MTUnderLine { + str += "\\underline" + str += "{\(mathListToString(underline.innerList!))}" + } + } else if atom.type == .accent { + if let accent = atom as? MTAccent { + str += "\\\(MTMathAtomFactory.accentName(accent)!){\(mathListToString(accent.innerList!))}" + } + } else if atom.type == .largeOperator { + let op = atom as! MTLargeOperator + let command = MTMathAtomFactory.latexSymbolName(for: atom) + let originalOp = MTMathAtomFactory.atom(forLatexSymbol: command!) as! MTLargeOperator + str += "\\\(command!) " + if originalOp.limits != op.limits { + if op.limits { + str += "\\limits " + } else { + str += "\\nolimits " + } + } + } else if atom.type == .space { + if let space = atom as? MTMathSpace { + if let command = Self.spaceToCommands[space.space] { + str += "\\\(command) " + } else { + str += String(format: "\\mkern%.1fmu", space.space) + } + } + } else if atom.type == .style { + if let style = atom as? MTMathStyle { + if let command = Self.styleToCommands[style.style] { + str += "\\\(command) " + } + } + } else if atom.nucleus.isEmpty { + str += "{}" + } else if atom.nucleus == "\u{2236}" { + // math colon + str += ":" + } else if atom.nucleus == "\u{2212}" { + // math minus + str += "-" + } else { + if let command = MTMathAtomFactory.latexSymbolName(for: atom) { + str += "\\\(command) " + } else { + str += "\(atom.nucleus)" + } + } + + if atom.superScript != nil { + str += "^{\(mathListToString(atom.superScript!))}" + } + + if atom.subScript != nil { + str += "_{\(mathListToString(atom.subScript!))}" + } + } + } + if currentfontStyle != .defaultStyle { + str += "}" + } + return str + } + + public static func delimToString(delim: MTMathAtom) -> String { + if let command = MTMathAtomFactory.getDelimiterName(of: delim) { + let singleChars = [ "(", ")", "[", "]", "<", ">", "|", ".", "/"] + if singleChars.contains(command) { + return command + } else if command == "||" { + return "\\|" + } else { + return "\\\(command)" + } + } + return "" + } + + mutating func atomForCommand(_ command:String) -> MTMathAtom? { + if let atom = MTMathAtomFactory.atom(forLatexSymbol: command) { + return atom + } + if let accent = MTMathAtomFactory.accent(withName: command) { + // The command is an accent + accent.innerList = self.buildInternal(true) + return accent; + } else if command == "frac" { + // A fraction command has 2 arguments + let frac = MTFraction() + frac.numerator = self.buildInternal(true) + frac.denominator = self.buildInternal(true) + return frac; + } else if command == "cfrac" { + // A continued fraction command with optional alignment and 2 arguments + let frac = MTFraction() + frac.isContinuedFraction = true + + // Parse optional alignment parameter [l], [r], [c] + skipSpaces() + if hasCharacters && string[currentCharIndex] == "[" { + _ = getNextCharacter() // consume '[' + if hasCharacters { + let alignmentChar = getNextCharacter() + if alignmentChar == "l" || alignmentChar == "r" || alignmentChar == "c" { + frac.alignment = String(alignmentChar) + } + } + // Consume closing ']' + if hasCharacters && string[currentCharIndex] == "]" { + _ = getNextCharacter() + } + } + + frac.numerator = self.buildInternal(true) + frac.denominator = self.buildInternal(true) + return frac; + } else if command == "dfrac" { + // Display-style fraction command has 2 arguments + let frac = MTFraction() + let numerator = self.buildInternal(true) + let denominator = self.buildInternal(true) + + // Prepend \displaystyle to force display mode rendering + let displayStyle = MTMathStyle(style: .display) + numerator?.insert(displayStyle, at: 0) + denominator?.insert(displayStyle, at: 0) + + frac.numerator = numerator + frac.denominator = denominator + return frac; + } else if command == "tfrac" { + // Text-style fraction command has 2 arguments + let frac = MTFraction() + let numerator = self.buildInternal(true) + let denominator = self.buildInternal(true) + + // Prepend \textstyle to force text mode rendering + let textStyle = MTMathStyle(style: .text) + numerator?.insert(textStyle, at: 0) + denominator?.insert(textStyle, at: 0) + + frac.numerator = numerator + frac.denominator = denominator + return frac; + } else if command == "binom" { + // A binom command has 2 arguments + let frac = MTFraction(hasRule: false) + frac.numerator = self.buildInternal(true) + frac.denominator = self.buildInternal(true) + frac.leftDelimiter = "("; + frac.rightDelimiter = ")"; + return frac; + } else if command == "bra" { + // Dirac bra notation: \bra{psi} -> ⟨psi| + let inner = MTInner() + inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "langle") + inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: "|") + inner.innerList = self.buildInternal(true) + return inner + } else if command == "ket" { + // Dirac ket notation: \ket{psi} -> |psi⟩ + let inner = MTInner() + inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "|") + inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: "rangle") + inner.innerList = self.buildInternal(true) + return inner + } else if command == "braket" { + // Dirac braket notation: \braket{phi}{psi} -> ⟨phi|psi⟩ + let inner = MTInner() + inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "langle") + inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: "rangle") + // Build the inner content: phi | psi + let phi = self.buildInternal(true) + let psi = self.buildInternal(true) + let content = MTMathList() + if let phiList = phi { + for atom in phiList.atoms { + content.add(atom) + } + } + // Add the | separator + content.add(MTMathAtom(type: .ordinary, value: "|")) + if let psiList = psi { + for atom in psiList.atoms { + content.add(atom) + } + } + inner.innerList = content + return inner + } else if command == "operatorname" || command == "operatorname*" { + // \operatorname{name} creates a custom operator with proper spacing + // \operatorname*{name} creates an operator with limits above/below + let hasLimits = command.hasSuffix("*") + + // Parse the operator name + let content = self.buildInternal(true) + + // Convert the parsed content to a string + var operatorName = "" + if let atoms = content?.atoms { + for atom in atoms { + operatorName += atom.nucleus + } + } + + if operatorName.isEmpty { + let errorMessage = "Missing operator name for \\operatorname" + self.setError(.invalidCommand, message: errorMessage) + return nil + } + + return MTLargeOperator(value: operatorName, limits: hasLimits) + } else if command == "sqrt" { + // A sqrt command with one argument + let rad = MTRadical() + guard self.hasCharacters else { + rad.radicand = self.buildInternal(true) + return rad + } + let ch = self.getNextCharacter() + if (ch == "[") { + // special handling for sqrt[degree]{radicand} + rad.degree = self.buildInternal(false, stopChar:"]") + rad.radicand = self.buildInternal(true) + } else { + self.unlookCharacter() + rad.radicand = self.buildInternal(true) + } + return rad; + } else if command == "left" { + // Save the current inner while a new one gets built. + let oldInner = currentInnerAtom + currentInnerAtom = MTInner() + currentInnerAtom!.leftBoundary = self.getBoundaryAtom("left") + if currentInnerAtom!.leftBoundary == nil { + return nil; + } + currentInnerAtom!.innerList = self.buildInternal(false) + if currentInnerAtom!.rightBoundary == nil { + // A right node would have set the right boundary so we must be missing the right node. + let errorMessage = "Missing \\right" + self.setError(.missingRight, message:errorMessage) + return nil + } + // reinstate the old inner atom. + let newInner = currentInnerAtom; + currentInnerAtom = oldInner; + return newInner; + } else if command == "overline" { + // The overline command has 1 arguments + let over = MTOverLine() + over.innerList = self.buildInternal(true) + return over + } else if command == "underline" { + // The underline command has 1 arguments + let under = MTUnderLine() + under.innerList = self.buildInternal(true) + return under + } else if command == "substack" { + // \substack reads ONE braced argument containing rows separated by \\ + // Similar to how \frac reads {numerator}{denominator} + + // Read the braced content using standard pattern + let content = self.buildInternal(true) + + if content == nil { + return nil + } + + // The content may already be a table if \\ was encountered + // Check if we got a table from the \\ parsing + if content!.atoms.count == 1, let tableAtom = content!.atoms.first as? MTMathTable { + return tableAtom + } + + // Otherwise, single row - wrap in table + var rows = [[MTMathList]]() + rows.append([content!]) + + var error: NSError? = self.error + let table = MTMathAtomFactory.table(withEnvironment: nil, rows: rows, error: &error) + if table == nil && self.error == nil { + self.error = error + return nil + } + + return table + } else if command == "begin" { + let env = self.readEnvironment() + if env == nil { + return nil; + } + let table = self.buildTable(env: env, firstList:nil, isRow:false) + return table + } else if command == "color" { + // A color command has 2 arguments + let mathColor = MTMathColor() + let color = self.readColor() + if color == nil { + return nil; + } + mathColor.colorString = color! + mathColor.innerList = self.buildInternal(true) + return mathColor + } else if command == "textcolor" { + // A textcolor command has 2 arguments + let mathColor = MTMathTextColor() + let color = self.readColor() + if color == nil { + return nil; + } + mathColor.colorString = color! + mathColor.innerList = self.buildInternal(true) + return mathColor + } else if command == "colorbox" { + // A color command has 2 arguments + let mathColorbox = MTMathColorbox() + let color = self.readColor() + if color == nil { + return nil; + } + mathColorbox.colorString = color! + mathColorbox.innerList = self.buildInternal(true) + return mathColorbox + } else if command == "pmod" { + // A pmod command has 1 argument - creates (mod n) + let inner = MTInner() + inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "(") + inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: ")") + + let innerList = MTMathList() + + // Add the "mod" operator (upright text) + let modOperator = MTMathAtomFactory.atom(forLatexSymbol: "mod")! + innerList.add(modOperator) + + // Add medium space between "mod" and argument (6mu) + let space = MTMathSpace(space: 6.0) + innerList.add(space) + + // Parse the argument from braces + let argument = self.buildInternal(true) + if let argList = argument { + innerList.append(argList) + } + + inner.innerList = innerList + return inner + } else if command == "not" { + // Handle \not command with lookahead for comprehensive negation support + let nextCommand = self.peekNextCommand() + + if let negatedUnicode = Self.notCombinations[nextCommand] { + self.consumeNextCommand() // Remove base symbol from stream + return MTMathAtom(type: .relation, value: negatedUnicode) + } else { + let errorMessage = "Unsupported \\not\\\(nextCommand) combination" + self.setError(.invalidCommand, message: errorMessage) + return nil + } + } else if let sizeMultiplier = Self.delimiterSizeCommands[command] { + // Handle \big, \Big, \bigg, \Bigg and their variants + let delim = self.readDelimiter() + if delim == nil { + let errorMessage = "Missing delimiter for \\\(command)" + self.setError(.missingDelimiter, message: errorMessage) + return nil + } + let boundary = MTMathAtomFactory.boundary(forDelimiter: delim!) + if boundary == nil { + let errorMessage = "Invalid delimiter for \\\(command): \(delim!)" + self.setError(.invalidDelimiter, message: errorMessage) + return nil + } + + // Create MTInner with explicit delimiter height + let inner = MTInner() + + // Determine if this is a left, right, or middle delimiter based on command suffix + let isLeft = command.hasSuffix("l") + let isRight = command.hasSuffix("r") + // let isMiddle = command.hasSuffix("m") // For future use + + if isLeft { + inner.leftBoundary = boundary + inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: ".") + } else if isRight { + inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: ".") + inner.rightBoundary = boundary + } else { + // For \big, \Big, \bigg, \Bigg and \bigm variants, use the delimiter on both sides + // but with empty inner content - it's just a sized delimiter + inner.leftBoundary = boundary + inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: ".") + } + + inner.innerList = MTMathList() + inner.delimiterHeight = sizeMultiplier // Store multiplier, typesetter will compute actual height + + return inner + } else { + let errorMessage = "Invalid command \\\(command)" + self.setError(.invalidCommand, message:errorMessage) + return nil; + } + } + + mutating func readColor() -> String? { + if !self.expectCharacter("{") { + // We didn't find an opening brace, so no env found. + self.setError(.characterNotFound, message:"Missing {") + return nil; + } + + // Ignore spaces and nonascii. + self.skipSpaces() + + // a string of all upper and lower case characters. + var mutable = "" + while self.hasCharacters { + let ch = self.getNextCharacter() + if ch == "#" || (ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9") { + mutable.append(ch) // appendString:[NSString stringWithCharacters:&ch length:1]]; + } else { + // we went too far + self.unlookCharacter() + break; + } + } + + if !self.expectCharacter("}") { + // We didn't find an closing brace, so invalid format. + self.setError(.characterNotFound, message:"Missing }") + return nil; + } + return mutable; + } + + mutating func skipSpaces() { + while self.hasCharacters { + let ch = self.getNextCharacter().utf32Char + if ch < 0x21 || ch > 0x7E { + // skip non ascii characters and spaces + continue; + } else { + self.unlookCharacter() + return; + } + } + } + + static var fractionCommands: [String:[Character]] { + [ + "over": [], + "atop" : [], + "choose" : [ "(", ")"], + "brack" : [ "[", "]"], + "brace" : [ "{", "}"] + ] + } + + mutating func stopCommand(_ command: String, list:MTMathList, stopChar:Character?) -> MTMathList? { + if command == "right" { + if currentInnerAtom == nil { + let errorMessage = "Missing \\left"; + self.setError(.missingLeft, message:errorMessage) + return nil; + } + currentInnerAtom!.rightBoundary = self.getBoundaryAtom("right") + if currentInnerAtom!.rightBoundary == nil { + return nil; + } + // return the list read so far. + return list + } else if let delims = Self.fractionCommands[command] { + var frac:MTFraction! = nil; + if command == "over" { + frac = MTFraction() + } else { + frac = MTFraction(hasRule: false) + } + if delims.count == 2 { + frac.leftDelimiter = String(delims[0]) + frac.rightDelimiter = String(delims[1]) + } + frac.numerator = list; + frac.denominator = self.buildInternal(false, stopChar: stopChar) + if error != nil { + return nil; + } + let fracList = MTMathList() + fracList.add(frac) + return fracList + } else if command == "\\" || command == "cr" { + if currentEnv != nil { + // Stop the current list and increment the row count + currentEnv!.numRows+=1 + return list + } else { + // Create a new table with the current list and a default env + if let table = self.buildTable(env: nil, firstList:list, isRow:true) { + return MTMathList(atom: table) + } + } + } else if command == "end" { + if currentEnv == nil { + let errorMessage = "Missing \\begin"; + self.setError(.missingBegin, message:errorMessage) + return nil + } + let env = self.readEnvironment() + if env == nil { + return nil + } + if env! != currentEnv!.envName { + let errorMessage = "Begin environment name \(currentEnv!.envName ?? "(none)") does not match end name: \(env!)" + self.setError(.invalidEnv, message:errorMessage) + return nil + } + // Finish the current environment. + currentEnv!.ended = true + return list + } + return nil + } + + // Applies the modifier to the atom. Returns true if modifier applied. + mutating func applyModifier(_ modifier:String, atom:MTMathAtom?) -> Bool { + if modifier == "limits" { + if atom?.type != .largeOperator { + let errorMessage = "Limits can only be applied to an operator." + self.setError(.invalidLimits, message:errorMessage) + } else { + let op = atom as! MTLargeOperator + op.limits = true + } + return true + } else if modifier == "nolimits" { + if atom?.type != .largeOperator { + let errorMessage = "No limits can only be applied to an operator." + self.setError(.invalidLimits, message:errorMessage) + } else { + let op = atom as! MTLargeOperator + op.limits = false + } + return true + } + return false + } + + mutating func setError(_ code:MTParseErrors, message:String) { + // Only record the first error. + if error == nil { + error = NSError(domain: MTParseError, code: code.rawValue, userInfo: [ NSLocalizedDescriptionKey : message ]) + } + } + + mutating func atom(forCommand command: String) -> MTMathAtom? { + if let atom = MTMathAtomFactory.atom(forLatexSymbol: command) { + return atom + } + if let accent = MTMathAtomFactory.accent(withName: command) { + accent.innerList = self.buildInternal(true) + return accent + } else if command == "frac" { + let frac = MTFraction() + frac.numerator = self.buildInternal(true) + frac.denominator = self.buildInternal(true) + return frac + } else if command == "cfrac" { + let frac = MTFraction() + frac.isContinuedFraction = true + + // Parse optional alignment parameter [l], [r], [c] + skipSpaces() + if hasCharacters && string[currentCharIndex] == "[" { + _ = getNextCharacter() // consume '[' + if hasCharacters { + let alignmentChar = getNextCharacter() + if alignmentChar == "l" || alignmentChar == "r" || alignmentChar == "c" { + frac.alignment = String(alignmentChar) + } + } + // Consume closing ']' + if hasCharacters && string[currentCharIndex] == "]" { + _ = getNextCharacter() + } + } + + frac.numerator = self.buildInternal(true) + frac.denominator = self.buildInternal(true) + return frac + } else if command == "dfrac" { + // Display-style fraction command has 2 arguments + let frac = MTFraction() + let numerator = self.buildInternal(true) + let denominator = self.buildInternal(true) + + // Prepend \displaystyle to force display mode rendering + let displayStyle = MTMathStyle(style: .display) + numerator?.insert(displayStyle, at: 0) + denominator?.insert(displayStyle, at: 0) + + frac.numerator = numerator + frac.denominator = denominator + return frac + } else if command == "tfrac" { + // Text-style fraction command has 2 arguments + let frac = MTFraction() + let numerator = self.buildInternal(true) + let denominator = self.buildInternal(true) + + // Prepend \textstyle to force text mode rendering + let textStyle = MTMathStyle(style: .text) + numerator?.insert(textStyle, at: 0) + denominator?.insert(textStyle, at: 0) + + frac.numerator = numerator + frac.denominator = denominator + return frac + } else if command == "binom" { + let frac = MTFraction(hasRule: false) + frac.numerator = self.buildInternal(true) + frac.denominator = self.buildInternal(true) + frac.leftDelimiter = "(" + frac.rightDelimiter = ")" + return frac + } else if command == "bra" { + // Dirac bra notation: \bra{psi} -> ⟨psi| + let inner = MTInner() + inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "langle") + inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: "|") + inner.innerList = self.buildInternal(true) + return inner + } else if command == "ket" { + // Dirac ket notation: \ket{psi} -> |psi⟩ + let inner = MTInner() + inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "|") + inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: "rangle") + inner.innerList = self.buildInternal(true) + return inner + } else if command == "braket" { + // Dirac braket notation: \braket{phi}{psi} -> ⟨phi|psi⟩ + let inner = MTInner() + inner.leftBoundary = MTMathAtomFactory.boundary(forDelimiter: "langle") + inner.rightBoundary = MTMathAtomFactory.boundary(forDelimiter: "rangle") + let phi = self.buildInternal(true) + let psi = self.buildInternal(true) + let content = MTMathList() + if let phiList = phi { + for atom in phiList.atoms { + content.add(atom) + } + } + content.add(MTMathAtom(type: .ordinary, value: "|")) + if let psiList = psi { + for atom in psiList.atoms { + content.add(atom) + } + } + inner.innerList = content + return inner + } else if command == "operatorname" || command == "operatorname*" { + // \operatorname{name} creates a custom operator with proper spacing + // \operatorname*{name} creates an operator with limits above/below + let hasLimits = command.hasSuffix("*") + + let content = self.buildInternal(true) + var operatorName = "" + if let atoms = content?.atoms { + for atom in atoms { + operatorName += atom.nucleus + } + } + if operatorName.isEmpty { + self.setError(.invalidCommand, message: "Missing operator name for \\operatorname") + return nil + } + return MTLargeOperator(value: operatorName, limits: hasLimits) + } else if command == "sqrt" { + let rad = MTRadical() + guard self.hasCharacters else { + rad.radicand = self.buildInternal(true) + return rad + } + let char = self.getNextCharacter() + if char == "[" { + rad.degree = self.buildInternal(false, stopChar: "]") + rad.radicand = self.buildInternal(true) + } else { + self.unlookCharacter() + rad.radicand = self.buildInternal(true) + } + return rad + } else if command == "left" { + let oldInner = self.currentInnerAtom + self.currentInnerAtom = MTInner() + self.currentInnerAtom?.leftBoundary = self.getBoundaryAtom("left") + if self.currentInnerAtom?.leftBoundary == nil { + return nil + } + self.currentInnerAtom!.innerList = self.buildInternal(false) + if self.currentInnerAtom?.rightBoundary == nil { + self.setError(.missingRight, message: "Missing \\right") + return nil + } + let newInner = self.currentInnerAtom + currentInnerAtom = oldInner + return newInner + } else if command == "overline" { + let over = MTOverLine() + over.innerList = self.buildInternal(true) + + return over + } else if command == "underline" { + let under = MTUnderLine() + under.innerList = self.buildInternal(true) + + return under + } else if command == "begin" { + if let env = self.readEnvironment() { + // Check if this is a starred matrix environment and read optional alignment + var alignment: MTColumnAlignment? = nil + if env.hasSuffix("*") { + alignment = self.readOptionalAlignment() + if self.error != nil { + return nil + } + } + + let table = self.buildTable(env: env, alignment: alignment, firstList: nil, isRow: false) + return table + } else { + return nil + } + } else if command == "color" { + // A color command has 2 arguments + let mathColor = MTMathColor() + mathColor.colorString = self.readColor()! + mathColor.innerList = self.buildInternal(true) + return mathColor + } else if command == "colorbox" { + // A color command has 2 arguments + let mathColorbox = MTMathColorbox() + mathColorbox.colorString = self.readColor()! + mathColorbox.innerList = self.buildInternal(true) + return mathColorbox + } else { + self.setError(.invalidCommand, message: "Invalid command \\\(command)") + return nil + } + } + + mutating func readEnvironment() -> String? { + if !self.expectCharacter("{") { + // We didn't find an opening brace, so no env found. + self.setError(.characterNotFound, message: "Missing {") + return nil + } + + self.skipSpaces() + let env = self.readString() + + if !self.expectCharacter("}") { + // We didn"t find an closing brace, so invalid format. + self.setError(.characterNotFound, message: "Missing }") + return nil; + } + return env + } + + /// Reads optional alignment parameter for starred matrix environments: [r], [l], or [c] + mutating func readOptionalAlignment() -> MTColumnAlignment? { + self.skipSpaces() + + // Check if there's an opening bracket + guard hasCharacters && string[currentCharIndex] == "[" else { + return nil + } + + _ = getNextCharacter() // consume '[' + self.skipSpaces() + + guard hasCharacters else { + self.setError(.characterNotFound, message: "Missing alignment specifier after [") + return nil + } + + let alignChar = getNextCharacter() + let alignment: MTColumnAlignment? + + switch alignChar { + case "l": + alignment = .left + case "c": + alignment = .center + case "r": + alignment = .right + default: + self.setError(.invalidEnv, message: "Invalid alignment specifier: \(alignChar). Must be l, c, or r") + return nil + } + + self.skipSpaces() + + if !self.expectCharacter("]") { + self.setError(.characterNotFound, message: "Missing ] after alignment specifier") + return nil + } + + return alignment + } + + func MTAssertNotSpace(_ ch: Character) { + assert(ch >= "\u{21}" && ch <= "\u{7E}", "Expected non-space character \(ch)") + } + + mutating func buildTable(env: String?, alignment: MTColumnAlignment? = nil, firstList: MTMathList?, isRow: Bool) -> MTMathAtom? { + // Save the current env till an new one gets built. + let oldEnv = self.currentEnv + + currentEnv = MTEnvProperties(name: env, alignment: alignment) + + var currentRow = 0 + var currentCol = 0 + + var rows = [[MTMathList]]() + rows.append([MTMathList]()) + if firstList != nil { + rows[currentRow].append(firstList!) + if isRow { + currentEnv!.numRows+=1 + currentRow+=1 + rows.append([MTMathList]()) + } else { + currentCol+=1 + } + } + while !currentEnv!.ended && self.hasCharacters { + let list = self.buildInternal(false) + if list == nil { + // If there is an error building the list, bail out early. + return nil + } + rows[currentRow].append(list!) + currentCol+=1 + if currentEnv!.numRows > currentRow { + currentRow = currentEnv!.numRows + rows.append([MTMathList]()) + currentCol = 0 + } + } + + if !currentEnv!.ended && currentEnv!.envName != nil { + self.setError(.missingEnd, message: "Missing \\end") + return nil + } + + var error:NSError? = self.error + let table = MTMathAtomFactory.table(withEnvironment: currentEnv?.envName, alignment: currentEnv?.alignment, rows: rows, error: &error) + if table == nil && self.error == nil { + self.error = error + return nil + } + self.currentEnv = oldEnv + return table + } + + mutating func getBoundaryAtom(_ delimiterType: String) -> MTMathAtom? { + let delim = self.readDelimiter() + if delim == nil { + let errorMessage = "Missing delimiter for \\\(delimiterType)" + self.setError(.missingDelimiter, message:errorMessage) + return nil + } + let boundary = MTMathAtomFactory.boundary(forDelimiter: delim!) + if boundary == nil { + let errorMessage = "Invalid delimiter for \(delimiterType): \(delim!)" + self.setError(.invalidDelimiter, message:errorMessage) + return nil + } + return boundary + } + + mutating func readDelimiter() -> String? { + self.skipSpaces() + while self.hasCharacters { + let char = self.getNextCharacter() + MTAssertNotSpace(char) + if char == "\\" { + let command = self.readCommand() + if command == "|" { + return "||" + } + return command + } else { + return String(char) + } + } + return nil + } + + mutating func readCommand() -> String { + let singleChars = "{}$#%_| ,>;!\\" + if self.hasCharacters { + let char = self.getNextCharacter() + if let _ = singleChars.firstIndex(of: char) { + return String(char) + } else { + self.unlookCharacter() + } + } + return self.readString() + } + + mutating func readString() -> String { + // a string of all upper and lower case characters (and asterisks for starred environments) + var output = "" + while self.hasCharacters { + let char = self.getNextCharacter() + if char.isLowercase || char.isUppercase || char == "*" { + output.append(char) + } else { + self.unlookCharacter() + break + } + } + return output + } +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathListDisplay.swift b/third-party/SwiftMath/Sources/MathRender/MTMathListDisplay.swift new file mode 100755 index 0000000000..a0fa47c879 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTMathListDisplay.swift @@ -0,0 +1,881 @@ +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Kostub Deshmukh. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation +import QuartzCore +import CoreText +import SwiftUI + +func isIos6Supported() -> Bool { + if !MTDisplay.initialized { +#if os(iOS) || os(visionOS) + let reqSysVer = "6.0" + let currSysVer = UIDevice.current.systemVersion + if currSysVer.compare(reqSysVer, options: .numeric) != .orderedAscending { + MTDisplay.supported = true + } +#else + MTDisplay.supported = true +#endif + MTDisplay.initialized = true + } + return MTDisplay.supported +} + +// The Downshift protocol allows an MTDisplay to be shifted down by a given amount. +protocol DownShift { + var shiftDown:CGFloat { set get } +} + +// MARK: - MTDisplay + +/// The base class for rendering a math equation. +public class MTDisplay:NSObject { + + // needed for isIos6Supported() func above + static var initialized = false + static var supported = false + + /// Draws itself in the given graphics context. + public func draw(_ context:CGContext) { + if self.localBackgroundColor != nil { + context.saveGState() + context.setBlendMode(.normal) + context.setFillColor(self.localBackgroundColor!.cgColor) + context.fill(self.displayBounds()) + context.restoreGState() + } + } + + /// Gets the bounding rectangle for the MTDisplay + func displayBounds() -> CGRect { + CGRectMake(self.position.x, self.position.y - self.descent, self.width, self.ascent + self.descent) + } + + /// For debugging. Shows the object in quick look in Xcode. +#if os(iOS) || os(visionOS) + func debugQuickLookObject() -> Any { + let size = CGSizeMake(self.width, self.ascent + self.descent); + UIGraphicsBeginImageContext(size); + + // get a reference to that context we created + let context = UIGraphicsGetCurrentContext()! + // translate/flip the graphics context (for transforming from CG* coords to UI* coords + context.translateBy(x: 0, y: size.height); + context.scaleBy(x: 1.0, y: -1.0); + // move the position to (0,0) + context.translateBy(x: -self.position.x, y: -self.position.y); + + // Move the line up by self.descent + context.translateBy(x: 0, y: self.descent); + // Draw self on context + self.draw(context) + + // generate a new UIImage from the graphics context we drew onto + let img = UIGraphicsGetImageFromCurrentImageContext() + return img as Any + } +#endif + + /// The distance from the axis to the top of the display + public var ascent:CGFloat = 0 + /// The distance from the axis to the bottom of the display + public var descent:CGFloat = 0 + /// The width of the display + public var width:CGFloat = 0 + /// Position of the display with respect to the parent view or display. + var position = CGPoint.zero + /// The range of characters supported by this item + public var range:NSRange=NSMakeRange(0, 0) + /// Whether the display has a subscript/superscript following it. + public var hasScript:Bool = false + /// The text color for this display + var textColor: MTColor? + /// The local color, if the color was mutated local with the color command + var localTextColor: MTColor? + /// The background color for this display + var localBackgroundColor: MTColor? + +} + +/// Special class to be inherited from that implements the DownShift protocol +class MTDisplayDS : MTDisplay, DownShift { + + var shiftDown: CGFloat = 0 + +} + +// MARK: - MTCTLineDisplay + +/// A rendering of a single CTLine as an MTDisplay +public class MTCTLineDisplay : MTDisplay { + + /// The CTLine being displayed + public var line:CTLine! + /// The attributed string used to generate the CTLineRef. Note setting this does not reset the dimensions of + /// the display. So set only when + var attributedString:NSAttributedString? { + didSet { + line = CTLineCreateWithAttributedString(attributedString!) + } + } + + /// An array of MTMathAtoms that this CTLine displays. Used for indexing back into the MTMathList + public fileprivate(set) var atoms = [MTMathAtom]() + + init(withString attrString:NSAttributedString?, position:CGPoint, range:NSRange, font:MTFont?, atoms:[MTMathAtom]) { + super.init() + self.position = position + self.attributedString = attrString + self.line = CTLineCreateWithAttributedString(attrString!) + self.range = range + self.atoms = atoms + // We can't use typographic bounds here as the ascent and descent returned are for the font and not for the line. + // CRITICAL FIX for accented character clipping: + // Use the MAXIMUM of typographic width and visual width to account for glyph overhang. + // - Typographic width = advance width (how far the cursor moves) + // - Visual width = actual glyph extent (CGRectGetMaxX of glyph path bounds) + // Some glyphs (especially italic/oblique accented characters) extend beyond their advance width. + // Using max() ensures we account for overhang while maintaining proper spacing for normal glyphs. + let typographicWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil)) + if isIos6Supported() { + let bounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) + self.ascent = max(0, CGRectGetMaxY(bounds) - 0); + self.descent = max(0, 0 - CGRectGetMinY(bounds)); + // Use the maximum of visual and typographic width to handle both: + // 1. Overhanging glyphs (visual > typographic) - prevents clipping + // 2. Normal glyphs (typographic >= visual) - maintains correct spacing + let visualWidth = CGRectGetMaxX(bounds) + self.width = max(typographicWidth, visualWidth); + } else { + // Our own implementation of the ios6 function to get glyph path bounds. + self.computeDimensions(font, typographicWidth: typographicWidth) + } + } + + override var textColor: MTColor? { + set { + super.textColor = newValue + let attrStr = attributedString!.mutableCopy() as! NSMutableAttributedString + let foregroundColor = NSAttributedString.Key(kCTForegroundColorAttributeName as String) + attrStr.addAttribute(foregroundColor, value:self.textColor!.cgColor, range:NSMakeRange(0, attrStr.length)) + self.attributedString = attrStr + } + get { super.textColor } + } + + func computeDimensions(_ font:MTFont?, typographicWidth: CGFloat) { + let runs = CTLineGetGlyphRuns(line) as NSArray + var maxVisualWidth: CGFloat = 0 + var currentX: CGFloat = 0 + + for obj in runs { + let run = obj as! CTRun? + let numGlyphs = CTRunGetGlyphCount(run!) + var glyphs = [CGGlyph]() + glyphs.reserveCapacity(numGlyphs) + CTRunGetGlyphs(run!, CFRangeMake(0, numGlyphs), &glyphs); + let bounds = CTFontGetBoundingRectsForGlyphs(font!.ctFont, .horizontal, glyphs, nil, numGlyphs); + let ascent = max(0, CGRectGetMaxY(bounds) - 0); + // Descent is how much the line goes below the origin. However if the line is all above the origin, then descent can't be negative. + let descent = max(0, 0 - CGRectGetMinY(bounds)); + if (ascent > self.ascent) { + self.ascent = ascent; + } + if (descent > self.descent) { + self.descent = descent; + } + + // Calculate visual width using glyph extent + // Get the rightmost edge of this run's glyphs + let runVisualWidth = CGRectGetMaxX(bounds) + let runRightEdge = currentX + runVisualWidth + + // Get advances to know where next run starts + var advances = [CGSize](repeating: CGSize.zero, count: numGlyphs) + CTRunGetAdvances(run!, CFRangeMake(0, numGlyphs), &advances) + for advance in advances { + currentX += advance.width + } + + if (runRightEdge > maxVisualWidth) { + maxVisualWidth = runRightEdge + } + } + + // Use maximum of typographic and visual width + self.width = max(typographicWidth, maxVisualWidth) + } + + override public func draw(_ context: CGContext) { + super.draw(context) + context.saveGState() + + context.textPosition = self.position + CTLineDraw(line, context) + + context.restoreGState() + } + +} + +// MARK: - MTMathListDisplay + +/// An MTLine is a rendered form of MTMathList in one line. +/// It can render itself using the draw method. +public class MTMathListDisplay : MTDisplay { + + /** + The type of position for a line, i.e. subscript/superscript or regular. + */ + public enum LinePosition : Int { + /// Regular + case regular + /// Positioned at a subscript + case ssubscript + /// Positioned at a superscript + case superscript + } + + /// Where the line is positioned + public var type:LinePosition = .regular + /// An array of MTDisplays which are positioned relative to the position of the + /// the current display. + public fileprivate(set) var subDisplays = [MTDisplay]() + /// If a subscript or superscript this denotes the location in the parent MTList. For a + /// regular list this is NSNotFound + public var index: Int = 0 + + init(withDisplays displays:[MTDisplay], range:NSRange) { + super.init() + self.subDisplays = displays + self.position = CGPoint.zero + self.type = .regular + self.index = NSNotFound + self.range = range + self.recomputeDimensions() + } + + override var textColor: MTColor? { + set { + super.textColor = newValue + for displayAtom in self.subDisplays { + if displayAtom.localTextColor == nil { + displayAtom.textColor = newValue + } else { + displayAtom.textColor = displayAtom.localTextColor + } + } + } + get { super.textColor } + } + + override public func draw(_ context: CGContext) { + super.draw(context) + context.saveGState() + + // Make the current position the origin as all the positions of the sub atoms are relative to the origin. + context.translateBy(x: self.position.x, y: self.position.y) + context.textPosition = CGPoint.zero + + // draw each atom separately + for displayAtom in self.subDisplays { + displayAtom.draw(context) + } + + context.restoreGState() + } + + func recomputeDimensions() { + var max_ascent:CGFloat = 0 + var max_descent:CGFloat = 0 + var max_width:CGFloat = 0 + for atom in self.subDisplays { + let ascent = max(0, atom.position.y + atom.ascent); + if (ascent > max_ascent) { + max_ascent = ascent; + } + + let descent = max(0, 0 - (atom.position.y - atom.descent)); + if (descent > max_descent) { + max_descent = descent; + } + let width = atom.width + atom.position.x; + if (width > max_width) { + max_width = width; + } + } + self.ascent = max_ascent; + self.descent = max_descent; + self.width = max_width; + } + +} + +// MARK: - MTFractionDisplay + +/// Rendering of an MTFraction as an MTDisplay +public class MTFractionDisplay : MTDisplay { + + /** A display representing the numerator of the fraction. Its position is relative + to the parent and is not treated as a sub-display. + */ + public fileprivate(set) var numerator:MTMathListDisplay? + /** A display representing the denominator of the fraction. Its position is relative + to the parent is not treated as a sub-display. + */ + public fileprivate(set) var denominator:MTMathListDisplay? + + var numeratorUp:CGFloat=0 { didSet { self.updateNumeratorPosition() } } + var denominatorDown:CGFloat=0 { didSet { self.updateDenominatorPosition() } } + var linePosition:CGFloat=0 + var lineThickness:CGFloat=0 + + init(withNumerator numerator:MTMathListDisplay?, denominator:MTMathListDisplay?, position:CGPoint, range:NSRange) { + super.init() + self.numerator = numerator; + self.denominator = denominator; + self.position = position; + + // CRITICAL FIX: Handle invalid ranges gracefully + // When table cells are typeset independently with maxWidth, atoms may have + // ranges that are invalid in the cell's context (e.g., (0,0) or other issues) + // Instead of crashing with assertion, normalize the range + if range.length == 0 { + // Create a dummy range with length 1 at the given location + self.range = NSMakeRange(range.location, 1) + } else { + self.range = range; + // Still assert for debugging if range length is something unexpected + assert(self.range.length == 1, "Fraction range length not 1 - range (\(range.location), \(range.length)") + } + } + + override public var ascent:CGFloat { + set { super.ascent = newValue } + get { (numerator?.ascent ?? 0) + self.numeratorUp } + } + + override public var descent:CGFloat { + set { super.descent = newValue } + get { (denominator?.descent ?? 0) + self.denominatorDown } + } + + override public var width:CGFloat { + set { super.width = newValue } + get { max(numerator?.width ?? 0, denominator?.width ?? 0) } + } + + func updateDenominatorPosition() { + guard denominator != nil else { return } + denominator!.position = CGPointMake(self.position.x + (self.width - denominator!.width)/2, self.position.y - self.denominatorDown) + } + + func updateNumeratorPosition() { + guard numerator != nil else { return } + numerator!.position = CGPointMake(self.position.x + (self.width - numerator!.width)/2, self.position.y + self.numeratorUp) + } + + override var position: CGPoint { + set { + super.position = newValue + self.updateDenominatorPosition() + self.updateNumeratorPosition() + } + get { super.position } + } + + override var textColor: MTColor? { + set { + super.textColor = newValue + numerator?.textColor = newValue + denominator?.textColor = newValue + } + get { super.textColor } + } + + override public func draw(_ context:CGContext) { + super.draw(context) + numerator?.draw(context) + denominator?.draw(context) + + context.saveGState() + + self.textColor?.setStroke() + + // draw the horizontal line + // Note: line thickness of 0 draws the thinnest possible line - we want no line so check for 0s + if self.lineThickness > 0 { + let path = MTBezierPath() + path.move(to: CGPointMake(self.position.x, self.position.y + self.linePosition)) + path.addLine(to: CGPointMake(self.position.x + self.width, self.position.y + self.linePosition)) + path.lineWidth = self.lineThickness + path.stroke() + } + + context.restoreGState() + } + +} + +// MARK: - MTRadicalDisplay + +/// Rendering of an MTRadical as an MTDisplay +class MTRadicalDisplay : MTDisplay { + + /** A display representing the radicand of the radical. Its position is relative + to the parent is not treated as a sub-display. + */ + public fileprivate(set) var radicand:MTMathListDisplay? + /** A display representing the degree of the radical. Its position is relative + to the parent is not treated as a sub-display. + */ + public fileprivate(set) var degree:MTMathListDisplay? + + override var position: CGPoint { + set { + super.position = newValue + self.updateRadicandPosition() + } + get { super.position } + } + + override var textColor: MTColor? { + set { + super.textColor = newValue + self.radicand?.textColor = newValue + self.degree?.textColor = newValue + } + get { super.textColor } + } + + private var _radicalGlyph:MTDisplay? + private var _radicalShift:CGFloat=0 + + var topKern:CGFloat=0 + var lineThickness:CGFloat=0 + + init(withRadicand radicand:MTMathListDisplay?, glyph:MTDisplay, position:CGPoint, range:NSRange) { + super.init() + self.radicand = radicand + _radicalGlyph = glyph + _radicalShift = 0 + + self.position = position + self.range = range + } + + func setDegree(_ degree:MTMathListDisplay?, fontMetrics:MTFontMathTable?) { + // sets up the degree of the radical + var kernBefore = fontMetrics!.radicalKernBeforeDegree; + let kernAfter = fontMetrics!.radicalKernAfterDegree; + let raise = fontMetrics!.radicalDegreeBottomRaisePercent * (self.ascent - self.descent); + + // The layout is: + // kernBefore, raise, degree, kernAfter, radical + self.degree = degree; + + // the radical is now shifted by kernBefore + degree.width + kernAfter + _radicalShift = kernBefore + degree!.width + kernAfter; + if _radicalShift < 0 { + // we can't have the radical shift backwards, so instead we increase the kernBefore such + // that _radicalShift will be 0. + kernBefore -= _radicalShift; + _radicalShift = 0; + } + + // Note: position of degree is relative to parent. + self.degree!.position = CGPointMake(self.position.x + kernBefore, self.position.y + raise); + // Update the width by the _radicalShift + self.width = _radicalShift + _radicalGlyph!.width + self.radicand!.width; + // update the position of the radicand + self.updateRadicandPosition() + } + + func updateRadicandPosition() { + // The position of the radicand includes the position of the MTRadicalDisplay + // This is to make the positioning of the radical consistent with fractions and + // have the cursor position finding algorithm work correctly. + // move the radicand by the width of the radical sign + self.radicand!.position = CGPointMake(self.position.x + _radicalShift + _radicalGlyph!.width, self.position.y); + } + + override public func draw(_ context: CGContext) { + super.draw(context) + + // draw the radicand & degree at its position + self.radicand?.draw(context) + self.degree?.draw(context) + + context.saveGState(); + self.textColor?.setStroke() + self.textColor?.setFill() + + // Make the current position the origin as all the positions of the sub atoms are relative to the origin. + context.translateBy(x: self.position.x + _radicalShift, y: self.position.y); + context.textPosition = CGPoint.zero + + // Draw the glyph. + _radicalGlyph?.draw(context) + + // Draw the VBOX + // for the kern of, we don't need to draw anything. + let heightFromTop = topKern; + + // draw the horizontal line with the given thickness + let path = MTBezierPath() + let lineStart = CGPointMake(_radicalGlyph!.width, self.ascent - heightFromTop - self.lineThickness / 2); // subtract half the line thickness to center the line + let lineEnd = CGPointMake(lineStart.x + self.radicand!.width, lineStart.y); + path.move(to: lineStart) + path.addLine(to: lineEnd) + path.lineWidth = lineThickness + path.lineCapStyle = .round + path.stroke() + + context.restoreGState(); + } +} + +// MARK: - MTGlyphDisplay + +/// Rendering a glyph as a display +class MTGlyphDisplay : MTDisplayDS { + + var glyph:CGGlyph! + var font:MTFont? + /// Horizontal scale factor for stretching glyphs (1.0 = no scaling) + var scaleX: CGFloat = 1.0 + + init(withGlpyh glyph:CGGlyph, range:NSRange, font:MTFont?) { + super.init() + self.font = font + self.glyph = glyph + + self.position = CGPoint.zero + self.range = range + } + + override public func draw(_ context: CGContext) { + super.draw(context) + context.saveGState() + + self.textColor?.setFill() + + // Make the current position the origin as all the positions of the sub atoms are relative to the origin. + + context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown); + + // Apply horizontal scaling if needed (for stretchy arrows) + if scaleX != 1.0 { + context.scaleBy(x: scaleX, y: 1.0) + } + + context.textPosition = CGPoint.zero + + var pos = CGPoint.zero + CTFontDrawGlyphs(font!.ctFont, &glyph, &pos, 1, context); + + context.restoreGState(); + } + + override var ascent:CGFloat { + set { super.ascent = newValue } + get { super.ascent - self.shiftDown } + } + + override var descent:CGFloat { + set { super.descent = newValue } + get { super.descent + self.shiftDown } + } +} + +// MARK: - MTGlyphConstructionDisplay + +class MTGlyphConstructionDisplay:MTDisplayDS { + var glyphs = [CGGlyph]() + var positions = [CGPoint]() + var font:MTFont? + var numGlyphs:Int=0 + + init(withGlyphs glyphs:[NSNumber?], offsets:[NSNumber?], font:MTFont?) { + super.init() + assert(glyphs.count == offsets.count, "Glyphs and offsets need to match") + self.numGlyphs = glyphs.count; + self.glyphs = [CGGlyph](repeating: CGGlyph(), count: self.numGlyphs) //malloc(sizeof(CGGlyph) * _numGlyphs); + self.positions = [CGPoint](repeating: CGPoint.zero, count: self.numGlyphs) //malloc(sizeof(CGPoint) * _numGlyphs); + for i in 0 ..< self.numGlyphs { + self.glyphs[i] = glyphs[i]!.uint16Value + self.positions[i] = CGPointMake(0, CGFloat(offsets[i]!.floatValue)) + } + self.font = font + self.position = CGPoint.zero + } + + override public func draw(_ context: CGContext) { + super.draw(context) + context.saveGState() + + self.textColor?.setFill() + + // Make the current position the origin as all the positions of the sub atoms are relative to the origin. + context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown) + context.textPosition = CGPoint.zero + + // Draw the glyphs. + CTFontDrawGlyphs(font!.ctFont, glyphs, positions, numGlyphs, context) + + context.restoreGState() + } + + override var ascent:CGFloat { + set { super.ascent = newValue } + get { super.ascent - self.shiftDown } + } + + override var descent:CGFloat { + set { super.descent = newValue } + get { super.descent + self.shiftDown } + } + +} + +// MARK: - MTLargeOpLimitsDisplay + +/// Rendering a large operator with limits as an MTDisplay +class MTLargeOpLimitsDisplay : MTDisplay { + + /** A display representing the upper limit of the large operator. Its position is relative + to the parent is not treated as a sub-display. + */ + var upperLimit:MTMathListDisplay? + /** A display representing the lower limit of the large operator. Its position is relative + to the parent is not treated as a sub-display. + */ + var lowerLimit:MTMathListDisplay? + + var limitShift:CGFloat=0 + var upperLimitGap:CGFloat=0 { didSet { self.updateUpperLimitPosition() } } + var lowerLimitGap:CGFloat=0 { didSet { self.updateLowerLimitPosition() } } + var extraPadding:CGFloat=0 + + var nucleus:MTDisplay? + + init(withNucleus nucleus:MTDisplay?, upperLimit:MTMathListDisplay?, lowerLimit:MTMathListDisplay?, limitShift:CGFloat, extraPadding:CGFloat) { + super.init() + self.upperLimit = upperLimit; + self.lowerLimit = lowerLimit; + self.nucleus = nucleus; + + var maxWidth = max(nucleus!.width, upperLimit?.width ?? 0) + maxWidth = max(maxWidth, lowerLimit?.width ?? 0) + + self.limitShift = limitShift; + self.upperLimitGap = 0; + self.lowerLimitGap = 0; + self.extraPadding = extraPadding; // corresponds to \xi_13 in TeX + self.width = maxWidth; + } + + override var ascent:CGFloat { + set { super.ascent = newValue } + get { + if self.upperLimit != nil { + return nucleus!.ascent + extraPadding + self.upperLimit!.ascent + upperLimitGap + self.upperLimit!.descent + } else { + return nucleus!.ascent + } + } + } + + override var descent:CGFloat { + set { super.descent = newValue } + get { + if self.lowerLimit != nil { + return nucleus!.descent + extraPadding + lowerLimitGap + self.lowerLimit!.descent + self.lowerLimit!.ascent; + } else { + return nucleus!.descent; + } + } + } + + override var position: CGPoint { + set { + super.position = newValue + self.updateLowerLimitPosition() + self.updateUpperLimitPosition() + self.updateNucleusPosition() + } + get { super.position } + } + + func updateLowerLimitPosition() { + if self.lowerLimit != nil { + // The position of the lower limit includes the position of the MTLargeOpLimitsDisplay + // This is to make the positioning of the radical consistent with fractions and radicals + // Move the starting point to below the nucleus leaving a gap of _lowerLimitGap and subtract + // the ascent to to get the baseline. Also center and shift it to the left by _limitShift. + self.lowerLimit!.position = CGPointMake(self.position.x - limitShift + (self.width - lowerLimit!.width)/2, + self.position.y - nucleus!.descent - lowerLimitGap - self.lowerLimit!.ascent); + } + } + + func updateUpperLimitPosition() { + if self.upperLimit != nil { + // The position of the upper limit includes the position of the MTLargeOpLimitsDisplay + // This is to make the positioning of the radical consistent with fractions and radicals + // Move the starting point to above the nucleus leaving a gap of _upperLimitGap and add + // the descent to to get the baseline. Also center and shift it to the right by _limitShift. + self.upperLimit!.position = CGPointMake(self.position.x + limitShift + (self.width - self.upperLimit!.width)/2, + self.position.y + nucleus!.ascent + upperLimitGap + self.upperLimit!.descent); + } + } + + func updateNucleusPosition() { + // Center the nucleus + nucleus?.position = CGPointMake(self.position.x + (self.width - nucleus!.width)/2, self.position.y); + } + + override var textColor: MTColor? { + set { + super.textColor = newValue + self.upperLimit?.textColor = newValue + self.lowerLimit?.textColor = newValue + nucleus?.textColor = newValue + } + get { super.textColor } + } + + override func draw(_ context:CGContext) { + super.draw(context) + // Draw the elements. + self.upperLimit?.draw(context) + self.lowerLimit?.draw(context) + nucleus?.draw(context) + } + +} + +// MARK: - MTLineDisplay + +/// Rendering of an list with an overline or underline +class MTLineDisplay : MTDisplay { + + /** A display representing the inner list that is underlined. Its position is relative + to the parent is not treated as a sub-display. + */ + var inner:MTMathListDisplay? + var lineShiftUp:CGFloat=0 + var lineThickness:CGFloat=0 + + init(withInner inner:MTMathListDisplay?, position:CGPoint, range:NSRange) { + super.init() + self.inner = inner; + + self.position = position; + self.range = range; + } + + override var textColor: MTColor? { + set { + super.textColor = newValue + inner?.textColor = newValue + } + get { super.textColor } + } + + override var position: CGPoint { + set { + super.position = newValue + self.updateInnerPosition() + } + get { super.position } + } + + override func draw(_ context:CGContext) { + super.draw(context) + self.inner?.draw(context) + + context.saveGState(); + + self.textColor?.setStroke() + + // draw the horizontal line + let path = MTBezierPath() + let lineStart = CGPointMake(self.position.x, self.position.y + self.lineShiftUp); + let lineEnd = CGPointMake(lineStart.x + self.inner!.width, lineStart.y); + path.move(to:lineStart) + path.addLine(to: lineEnd) + path.lineWidth = self.lineThickness; + path.stroke() + + context.restoreGState(); + } + + func updateInnerPosition() { + self.inner?.position = CGPointMake(self.position.x, self.position.y); + } + +} + +// MARK: - MTAccentDisplay + +/// Rendering an accent as a display +class MTAccentDisplay : MTDisplay { + + /** A display representing the inner list that is accented. Its position is relative + to the parent is not treated as a sub-display. + */ + var accentee:MTMathListDisplay? + + /** A display representing the accent. Its position is relative to the current display. + */ + var accent:MTGlyphDisplay? + + init(withAccent glyph:MTGlyphDisplay?, accentee:MTMathListDisplay?, range:NSRange) { + super.init() + self.accent = glyph + self.accentee = accentee + self.accentee?.position = CGPoint.zero + self.range = range + } + + override var textColor: MTColor? { + set { + super.textColor = newValue + accentee?.textColor = newValue + accent?.textColor = newValue + } + get { super.textColor } + } + + override var position: CGPoint { + set { + super.position = newValue + self.updateAccenteePosition() + } + get { super.position } + } + + func updateAccenteePosition() { + self.accentee?.position = CGPointMake(self.position.x, self.position.y); + } + + override func draw(_ context:CGContext) { + super.draw(context) + self.accentee?.draw(context) + + context.saveGState(); + context.translateBy(x: self.position.x, y: self.position.y); + context.textPosition = CGPoint.zero + + self.accent?.draw(context) + + context.restoreGState(); + } + +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathListIndex.swift b/third-party/SwiftMath/Sources/MathRender/MTMathListIndex.swift new file mode 100755 index 0000000000..34bd6bfbb0 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTMathListIndex.swift @@ -0,0 +1,197 @@ +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Kostub Deshmukh. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation + +/** + * An index that points to a particular character in the MTMathList. The index is a LinkedList that represents + * a path from the beginning of the MTMathList to reach a particular atom in the list. The next node of the path + * is represented by the subIndex. The path terminates when the subIndex is nil. + * + * If there is a subIndex, the subIndexType denotes what branch the path takes (i.e. superscript, subscript, + * numerator, denominator etc.). + * e.g in the expression 25^{2/4} the index of the character 4 is represented as: + * (1, superscript) -> (0, denominator) -> (0, none) + * This can be interpreted as start at index 1 (i.e. the 5) go up to the superscript. + * Then look at index 0 (i.e. 2/4) and go to the denominator. Then look up index 0 (i.e. the 4) which this final + * index. + * + * The level of an index is the number of nodes in the LinkedList to get to the final path. + */ +public class MTMathListIndex { + + /** + The type of the subindex. + + The type of the subindex denotes what branch the path to the atom that this index points to takes. + */ + public enum MTMathListSubIndexType: Int { + /// The index denotes the whole atom, subIndex is nil. + case none = 0 + /// The position in the subindex is an index into the nucleus + case nucleus + /// The subindex indexes into the superscript. + case superscript + /// The subindex indexes into the subscript + case ssubscript + /// The subindex indexes into the numerator (only valid for fractions) + case numerator + /// The subindex indexes into the denominator (only valid for fractions) + case denominator + /// The subindex indexes into the radicand (only valid for radicals) + case radicand + /// The subindex indexes into the degree (only valid for radicals) + case degree + } + + /// The index of the associated atom. + var atomIndex: Int + + /// The type of subindex, e.g. superscript, numerator etc. + var subIndexType: MTMathListSubIndexType = .none + + /// The index into the sublist. + var subIndex: MTMathListIndex? + + var finalIndex: Int { + if self.subIndexType == .none { + return self.atomIndex + } else { + return self.subIndex?.finalIndex ?? 0 + } + } + + /// Returns the previous index if present. Returns `nil` if there is no previous index. + func prevIndex() -> MTMathListIndex? { + if self.subIndexType == .none { + if self.atomIndex > 0 { + return MTMathListIndex(level0Index: self.atomIndex - 1) + } + } else { + if let prevSubIndex = self.subIndex?.prevIndex() { + return MTMathListIndex(at: self.atomIndex, with: prevSubIndex, type: self.subIndexType) + } + } + return nil + } + + /// Returns the next index. + func nextIndex() -> MTMathListIndex { + if self.subIndexType == .none { + return MTMathListIndex(level0Index: self.atomIndex + 1) + } else if self.subIndexType == .nucleus { + return MTMathListIndex(at: self.atomIndex + 1, with: self.subIndex, type: self.subIndexType) + } else { + return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.nextIndex(), type: self.subIndexType) + } + } + + /** + * Returns true if this index represents the beginning of a line. Note there may be multiple lines in a MTMathList, + * e.g. a superscript or a fraction numerator. This returns true if the innermost subindex points to the beginning of a + * line. + */ + func isBeginningOfLine() -> Bool { self.finalIndex == 0 } + + func isAtSameLevel(with index: MTMathListIndex?) -> Bool { + if self.subIndexType != index?.subIndexType { + return false + } else if self.subIndexType == .none { + // No subindexes, they are at the same level. + return true + } else if (self.atomIndex != index?.atomIndex) { + return false + } else { + return self.subIndex?.isAtSameLevel(with: index?.subIndex) ?? false + } + } + + /** Returns the type of the innermost sub index. */ + func finalSubIndexType() -> MTMathListSubIndexType { + if self.subIndex?.subIndex != nil { + return self.subIndex!.finalSubIndexType() + } else { + return self.subIndexType + } + } + + /** Returns true if any of the subIndexes of this index have the given type. */ + func hasSubIndex(ofType type: MTMathListSubIndexType) -> Bool { + if self.subIndexType == type { + return true + } else { + return self.subIndex?.hasSubIndex(ofType: type) ?? false + } + } + + func levelUp(with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) -> MTMathListIndex { + if self.subIndexType == .none { + return MTMathListIndex(at: self.atomIndex, with: subIndex, type: type) + } + + return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.levelUp(with: subIndex, type: type), type: self.subIndexType) + } + + func levelDown() -> MTMathListIndex? { + if self.subIndexType == .none { + return nil + } + + if let subIndexDown = self.subIndex?.levelDown() { + return MTMathListIndex(at: self.atomIndex, with: subIndexDown, type: self.subIndexType) + } else { + return MTMathListIndex(level0Index: self.atomIndex) + } + } + + /** Factory function to create a `MTMathListIndex` with no subindexes. + @param index The index of the atom that the `MTMathListIndex` points at. + */ + public init(level0Index: Int) { + self.atomIndex = level0Index + } + + public convenience init(at location: Int, with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) { + self.init(level0Index: location) + self.subIndexType = type + self.subIndex = subIndex + } +} + +extension MTMathListIndex: CustomStringConvertible { + public var description: String { + if self.subIndex != nil { + return "[\(self.atomIndex), \(self.subIndexType.rawValue):\(self.subIndex!)]" + } + return "[\(self.atomIndex)]" + } +} + +extension MTMathListIndex: Hashable { + + public func hash(into hasher: inout Hasher) { + hasher.combine(self.atomIndex) + hasher.combine(self.subIndexType) + hasher.combine(self.subIndex) + } + +} + +extension MTMathListIndex: Equatable { + public static func ==(lhs: MTMathListIndex, rhs: MTMathListIndex) -> Bool { + if lhs.atomIndex != rhs.atomIndex || lhs.subIndexType != rhs.subIndexType { + return false + } + + if rhs.subIndex != nil { + return rhs.subIndex == lhs.subIndex + } else { + return lhs.subIndex == nil + } + } +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathRenderer.swift b/third-party/SwiftMath/Sources/MathRender/MTMathRenderer.swift new file mode 100644 index 0000000000..8243148734 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTMathRenderer.swift @@ -0,0 +1,83 @@ +import Foundation + +#if os(iOS) || os(visionOS) +import UIKit +#endif + +#if os(macOS) +import AppKit +#endif + +public struct MTMathRenderedFormula { + public let image: MTImage + public let size: CGSize + public let width: CGFloat + public let ascent: CGFloat + public let descent: CGFloat + + public init(image: MTImage, size: CGSize, width: CGFloat, ascent: CGFloat, descent: CGFloat) { + self.image = image + self.size = size + self.width = width + self.ascent = ascent + self.descent = descent + } +} + +public enum MTMathRenderer { + public static func render(latex: String, fontSize: CGFloat, textColor: MTColor, mode: MTMathUILabelMode = .display) -> MTMathRenderedFormula? { + guard let font = MTFontManager.fontManager.defaultFont?.copy(withSize: fontSize) else { + return nil + } + + var error: NSError? + guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil else { + return nil + } + + let style: MTLineStyle + switch mode { + case .display: + style = .display + case .text: + style = .text + } + + guard let displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: style) else { + return nil + } + + displayList.textColor = textColor + + let width = max(1.0, ceil(displayList.width)) + let height = max(1.0, ceil(displayList.ascent + displayList.descent)) + let size = CGSize(width: width, height: height) + displayList.position = CGPoint(x: 0.0, y: displayList.descent) + + #if os(iOS) || os(visionOS) + let renderer = UIGraphicsImageRenderer(size: size) + let image = renderer.image { rendererContext in + rendererContext.cgContext.saveGState() + var transform = CGAffineTransform(scaleX: 1.0, y: -1.0) + transform = transform.translatedBy(x: 0.0, y: -size.height) + rendererContext.cgContext.concatenate(transform) + displayList.draw(rendererContext.cgContext) + rendererContext.cgContext.restoreGState() + } + return MTMathRenderedFormula(image: image, size: size, width: displayList.width, ascent: displayList.ascent, descent: displayList.descent) + #endif + + #if os(macOS) + let image = NSImage(size: size, flipped: false) { _ in + guard let context = NSGraphicsContext.current?.cgContext else { + return false + } + context.saveGState() + displayList.draw(context) + context.restoreGState() + return true + } + return MTMathRenderedFormula(image: image, size: size, width: displayList.width, ascent: displayList.ascent, descent: displayList.descent) + #endif + } +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTMathUILabel.swift b/third-party/SwiftMath/Sources/MathRender/MTMathUILabel.swift new file mode 100644 index 0000000000..b19aabba33 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTMathUILabel.swift @@ -0,0 +1,587 @@ +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Kostub Deshmukh. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation +import CoreText + +/** + Different display styles supported by the `MTMathUILabel`. + + The only significant difference between the two modes is how fractions + and limits on large operators are displayed. + */ +public enum MTMathUILabelMode { + /// Display mode. Equivalent to $$ in TeX + case display + /// Text mode. Equivalent to $ in TeX. + case text +} + +/** + Horizontal text alignment for `MTMathUILabel`. + */ +public enum MTTextAlignment : UInt { + /// Align left. + case left + /// Align center. + case center + /// Align right. + case right +} + +/** The main view for rendering math. + + `MTMathLabel` accepts either a string in LaTeX or an `MTMathList` to display. Use + `MTMathList` directly only if you are building it programmatically (e.g. using an + editor), otherwise using LaTeX is the preferable method. + + The math display is centered vertically in the label. The default horizontal alignment is + is left. This can be changed by setting `textAlignment`. The math is default displayed in + *Display* mode. This can be changed using `labelMode`. + + When created it uses `[MTFontManager defaultFont]` as its font. This can be changed using + the `font` parameter. + */ +@IBDesignable +public class MTMathUILabel : MTView { + + /** The `MTMathList` to render. Setting this will remove any + `latex` that has already been set. If `latex` has been set, this will + return the parsed `MTMathList` if the `latex` parses successfully. Use this + setting if the `MTMathList` has been programmatically constructed, otherwise it + is preferred to use `latex`. + */ + public var mathList:MTMathList? { + set { + _mathList = newValue + _error = nil + _latex = MTMathListBuilder.mathListToString(newValue) + self.invalidateIntrinsicContentSize() + self.setNeedsLayout() + } + get { _mathList } + } + private var _mathList:MTMathList? + + /** The latex string to be displayed. Setting this will remove any `mathList` that + has been set. If latex has not been set, this will return the latex output for the + `mathList` that is set. + @see error */ + @IBInspectable + public var latex:String { + set { + _latex = newValue + _error = nil + var error : NSError? = nil + _mathList = MTMathListBuilder.build(fromString: newValue, error: &error) + if error != nil { + _mathList = nil + _error = error + self.errorLabel?.text = error!.localizedDescription + self.errorLabel?.frame = self.bounds + self.errorLabel?.isHidden = !self.displayErrorInline + } else { + self.errorLabel?.isHidden = true + } + self.invalidateIntrinsicContentSize() + self.setNeedsLayout() + } + get { _latex } + } + private var _latex = "" + + /** This contains any error that occurred when parsing the latex. */ + public var error:NSError? { _error } + private var _error:NSError? + + /** If true, if there is an error it displays the error message inline. Default true. */ + public var displayErrorInline = true + + /** If true, enables detailed debug logging for intrinsicContentSize calculations. + This helps diagnose rendering issues like missing row breaks in aligned environments. + Default false. */ + public var debugLogging = false + + /** The MTFont to use for rendering. */ + public var font:MTFont? { + set { + guard newValue != nil else { return } + _font = newValue + self.invalidateIntrinsicContentSize() + self.setNeedsLayout() + } + get { _font } + } + private var _font:MTFont? + + /** Convenience method to just set the size of the font without changing the fontface. */ + @IBInspectable + public var fontSize:CGFloat { + set { + _fontSize = newValue + let font = font?.copy(withSize: newValue) + self.font = font // also forces an update + } + get { _fontSize } + } + private var _fontSize:CGFloat=0 + + /** This sets the text color of the rendered math formula. The default color is black. */ + @IBInspectable + public var textColor:MTColor? { + set { + guard newValue != nil else { return } + _textColor = newValue + self.displayList?.textColor = newValue + self.setNeedsDisplay() + } + get { _textColor } + } + private var _textColor:MTColor? + + /** The minimum distance from the margin of the view to the rendered math. This value is + `UIEdgeInsetsZero` by default. This is useful if you need some padding between the math and + the border/background color. sizeThatFits: will have its returned size increased by these insets. + */ + @IBInspectable + public var contentInsets:MTEdgeInsets { + set { + _contentInsets = newValue + self.invalidateIntrinsicContentSize() + self.setNeedsLayout() + } + get { _contentInsets } + } + private var _contentInsets = MTEdgeInsetsZero + + /** The Label mode for the label. The default mode is Display */ + public var labelMode:MTMathUILabelMode { + set { + _labelMode = newValue + self.invalidateIntrinsicContentSize() + self.setNeedsLayout() + } + get { _labelMode } + } + private var _labelMode = MTMathUILabelMode.display + + /** Horizontal alignment for the text. The default is align left. */ + public var textAlignment:MTTextAlignment { + set { + _textAlignment = newValue + self.invalidateIntrinsicContentSize() + self.setNeedsLayout() + } + get { _textAlignment } + } + private var _textAlignment = MTTextAlignment.left + + /** The internal display of the MTMathUILabel. This is for advanced use only. */ + public var displayList: MTMathListDisplay? { _displayList } + private var _displayList:MTMathListDisplay? + + /** The preferred maximum width (in points) for a multiline label. + Set this property to enable line wrapping based on available width. */ + public var preferredMaxLayoutWidth: CGFloat { + set { + _preferredMaxLayoutWidth = newValue + _displayList = nil // Clear cached display list when width constraint changes + self.invalidateIntrinsicContentSize() + self.setNeedsLayout() + } + get { _preferredMaxLayoutWidth } + } + private var _preferredMaxLayoutWidth: CGFloat = 0 + + public var currentStyle:MTLineStyle { + switch _labelMode { + case .display: return .display + case .text: return .text + } + } + + public var errorLabel: MTLabel? + + public override init(frame: CGRect) { + super.init(frame: frame) + self.initCommon() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + self.initCommon() + } + + func initCommon() { +#if os(macOS) + self.layer?.isGeometryFlipped = true +#else + self.layer.isGeometryFlipped = true + self.clipsToBounds = true +#endif + _fontSize = 20 + _contentInsets = MTEdgeInsetsZero + _labelMode = .display + let font = MTFontManager.fontManager.defaultFont + self.font = font + _textAlignment = .left + _displayList = nil + displayErrorInline = true + self.backgroundColor = MTColor.clear + + _textColor = MTColor.black + let label = MTLabel() + self.errorLabel = label +#if os(macOS) + label.layer?.isGeometryFlipped = true +#else + label.layer.isGeometryFlipped = true +#endif + label.isHidden = true + label.textColor = MTColor.red + self.addSubview(label) + } + + override public func draw(_ dirtyRect: MTRect) { + super.draw(dirtyRect) + if self.mathList == nil { return } + if self.font == nil { return } + + // Ensure display list is created before drawing + if _displayList == nil { + _layoutSubviews() + } + + guard let displayList = _displayList else { return } + + // drawing code + let context = MTGraphicsGetCurrentContext()! + context.saveGState() + + // CRITICAL FIX for clipping: If the displayList is wider than our bounds, + // expand the clipping rect to prevent content clipping. + // This handles cases where preferredMaxLayoutWidth is a hint but the content + // cannot fit within it even with line breaking. + let contentWidth = displayList.width + contentInsets.left + contentInsets.right + if contentWidth > bounds.size.width { + // Content is wider than bounds - expand clip rect + let expandedRect = CGRect( + x: bounds.origin.x, + y: bounds.origin.y, + width: contentWidth, + height: bounds.size.height + ) + context.clip(to: expandedRect) + } + + displayList.draw(context) + context.restoreGState() + } + + func _layoutSubviews() { + guard _mathList != nil && self.font != nil else { + _displayList = nil + errorLabel?.frame = self.bounds + self.setNeedsDisplay() + return + } + // Ensure we have a valid font before attempting to typeset + if self.font == nil { + // No valid font - try to get default font + if let defaultFont = MTFontManager.fontManager.defaultFont { + self._font = defaultFont + } else { + // Cannot typeset without a font, clear display list + _displayList = nil + errorLabel?.frame = self.bounds + self.setNeedsDisplay() + return + } + } + + // Use the effective width for layout + let effectiveWidth = _preferredMaxLayoutWidth > 0 ? _preferredMaxLayoutWidth : bounds.size.width + var availableWidth = effectiveWidth - contentInsets.left - contentInsets.right + // CRITICAL FIX: Ensure availableWidth is never negative + // Negative maxWidth passed to MTTypesetter can cause "Negative value is not representable" crashes + availableWidth = max(0, availableWidth) + + _displayList = MTTypesetter.createLineForMathList(_mathList, font: self.font, style: currentStyle, maxWidth: availableWidth) + + guard let displayList = _displayList else { + // Empty or invalid input - nothing to display + return + } + + displayList.textColor = textColor + var textX = CGFloat(0) + switch self.textAlignment { + case .left: textX = contentInsets.left + case .center: textX = (bounds.size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left + case .right: textX = bounds.size.width - displayList.width - contentInsets.right + } + let availableHeight = bounds.size.height - contentInsets.bottom - contentInsets.top + + // center things vertically + var height = displayList.ascent + displayList.descent + if height < fontSize/2 { + height = fontSize/2 // set height to half the font size + } + let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom + + displayList.position = CGPointMake(textX, textY) + errorLabel?.frame = self.bounds + self.setNeedsDisplay() + } + + func _sizeThatFits(_ size:CGSize) -> CGSize { + // Check if we have empty latex (empty string case) + if _latex.isEmpty { + // Empty latex - return zero size + return CGSize(width: 0, height: 0) + } + + guard _mathList != nil else { + // No content - return no-intrinsic-size marker + return CGSize(width: -1, height: -1) + } + + // Ensure we have a valid font before attempting to typeset + if self.font == nil { + // No valid font - try to get default font + if let defaultFont = MTFontManager.fontManager.defaultFont { + self._font = defaultFont + } else { + // Cannot typeset without a font + return CGSize(width: -1, height: -1) + } + } + + // Determine the maximum width to use + var maxWidth: CGFloat = 0 + if _preferredMaxLayoutWidth > 0 { + maxWidth = _preferredMaxLayoutWidth - contentInsets.left - contentInsets.right + // CRITICAL FIX: Ensure maxWidth is never negative + // If contentInsets exceed available width, clamp to 0 + maxWidth = max(0, maxWidth) + } else if size.width > 0 { + maxWidth = size.width - contentInsets.left - contentInsets.right + // CRITICAL FIX: Ensure maxWidth is never negative + maxWidth = max(0, maxWidth) + } + + var displayList:MTMathListDisplay? = nil + displayList = MTTypesetter.createLineForMathList(_mathList, font: self.font, style: currentStyle, maxWidth: maxWidth) + + guard displayList != nil else { + // Failed to create display list + return CGSize(width: -1, height: -1) + } + + var resultWidth = displayList!.width + contentInsets.left + contentInsets.right + var resultHeight = displayList!.ascent + displayList!.descent + contentInsets.top + contentInsets.bottom + + // DEBUG LOGGING for width calculation + if debugLogging { + print("\n=== MTMathUILabel intrinsicContentSize DEBUG ===") + print("LaTeX: \(self.latex)") + // Show raw bytes to help debug escaping issues + let latexBytes = Array(self.latex.utf8.prefix(100)) + print("LaTeX bytes (first 100): \(latexBytes)") + print("preferredMaxLayoutWidth: \(_preferredMaxLayoutWidth)") + print("size constraint: \(size)") + print("maxWidth passed to typesetter: \(maxWidth)") + print("displayList.width: \(displayList!.width)") + print("displayList.ascent: \(displayList!.ascent)") + print("displayList.descent: \(displayList!.descent)") + print("Number of subDisplays: \(displayList!.subDisplays.count)") + // Count lines by unique Y positions + let yPositions = Set(displayList!.subDisplays.map { $0.position.y }) + print("Number of lines (unique Y positions): \(yPositions.count)") + print("contentInsets: \(contentInsets)") + print("resultWidth (before clamping): \(resultWidth)") + print("resultHeight (before clamping): \(resultHeight)") + + // HELPFUL WARNING: Detect common escaping mistake + // Check for environments that typically have multiple rows but only 1 row is rendered + let multiRowEnvs = ["aligned", "align", "eqnarray", "gather", "split"] + let latexLower = self.latex.lowercased() + for env in multiRowEnvs { + if latexLower.contains("\\begin{\(env)}") { + // Check if there's a table with only 1 row + if let ml = _mathList { + for atom in ml.atoms { + if let table = atom as? MTMathTable, table.numRows == 1 { + // Check if latex contains single backslash (spacing) where double might be intended + // Pattern: \\ followed by non-backslash (like space or letter) + if self.latex.contains("\\ ") || self.latex.range(of: #"\\[a-zA-Z]"#, options: .regularExpression) != nil { + print("⚠️ WARNING: '\(env)' environment has only 1 row.") + print(" If you intended multiple rows, use '\\\\\\\\' (4 backslashes in Swift strings)") + print(" or use raw strings: #\"...42\\\\ \\\\dfrac...\"#") + print(" Current input has '\\\\ ' which is a SPACING command, not a row break.") + } + } + } + } + } + } + } + + // CRITICAL FIX: Ensure dimensions are never negative + // Negative values cause crashes in NSRange calculations and SwiftUI layout + resultWidth = max(0, resultWidth) + resultHeight = max(0, resultHeight) + + // CRITICAL FIX for accented character clipping: + // The preferredMaxLayoutWidth is a HINT for line breaking, NOT a hard constraint. + // If the typesetter cannot fit content within that width (even with line breaking), + // we MUST return the actual content width to prevent clipping. + // + // ONLY clamp in extreme cases to prevent layout explosion (>50% over or >100pt over) + if _preferredMaxLayoutWidth > 0 && resultWidth > _preferredMaxLayoutWidth { + let overflow = resultWidth - _preferredMaxLayoutWidth + let overflowPercent = (overflow / _preferredMaxLayoutWidth) * 100 + + if debugLogging { + print(" Content exceeds preferredMaxLayoutWidth:") + print(" preferredMaxLayoutWidth: \(_preferredMaxLayoutWidth)") + print(" resultWidth: \(resultWidth)") + print(" overflow: \(overflow) (\(String(format: "%.1f", overflowPercent))%)") + + // Check line breaking + let yPositions = Set(displayList!.subDisplays.map { $0.position.y }) + print(" hasMultipleLines: \(yPositions.count > 1) (yPositions: \(yPositions.count))") + + // Check if any content would be clipped at different widths + print(" SubDisplay analysis:") + for (i, sub) in displayList!.subDisplays.enumerated() { + let rightEdge = sub.position.x + sub.width + let clippedAtPreferred = rightEdge > _preferredMaxLayoutWidth + let clippedAtResult = rightEdge > resultWidth + print(" Sub[\(i)]: rightEdge=\(rightEdge) clippedAt\(_preferredMaxLayoutWidth)=\(clippedAtPreferred) clippedAt\(resultWidth)=\(clippedAtResult)") + } + } + + // ONLY clamp for truly excessive overflow (>50% or >100pt) + // This prevents layout explosion while allowing normal overflow + let extremeOverflowThreshold: CGFloat = max(_preferredMaxLayoutWidth * 0.5, 100.0) + + if overflow > extremeOverflowThreshold { + // Extreme overflow - clamp to prevent layout issues + let clampedWidth = _preferredMaxLayoutWidth + extremeOverflowThreshold + if debugLogging { + print(" ⚠️ EXTREME OVERFLOW - clamping from \(resultWidth) to \(clampedWidth)") + print(" ⚠️ WARNING: This will cause content clipping!") + } + resultWidth = clampedWidth + } else { + // Normal overflow - keep actual content width to prevent clipping + if debugLogging { + print(" ✓ Normal overflow - keeping actual width \(resultWidth) to prevent clipping") + } + // resultWidth stays as is - NO CLAMPING + } + } else if _preferredMaxLayoutWidth == 0 && size.width > 0 && resultWidth > size.width { + // Similar tolerance for size.width constraint + let tolerance = max(size.width * 0.05, 10.0) + let maxAllowedWidth = size.width + tolerance + + if debugLogging { + print(" Exceeds size.width constraint:") + print(" tolerance: \(tolerance)") + print(" maxAllowedWidth: \(maxAllowedWidth)") + print(" overflow amount: \(resultWidth - size.width)") + } + + if resultWidth <= maxAllowedWidth { + // Within tolerance - use actual content width + // resultWidth stays as is + if debugLogging { + print(" ✓ Within tolerance - keeping actual width") + } + } else { + if debugLogging { + print(" ⚠️ CLAMPING to maxAllowedWidth (may clip content!)") + } + resultWidth = maxAllowedWidth + } + } + + if debugLogging { + print(" Final resultWidth: \(resultWidth)") + print(" Final resultHeight: \(resultHeight)") + + // Check if any display elements would be clipped + if let display = displayList { + print(" Display subdisplays: \(display.subDisplays.count)") + let yPositions = Set(display.subDisplays.map { $0.position.y }).sorted() + print(" Unique Y positions (lines): \(yPositions.count) -> \(yPositions)") + + for (i, sub) in display.subDisplays.enumerated() { + let rightEdge = sub.position.x + sub.width + let clipped = rightEdge > resultWidth + + // Extract text content if this is a CTLineDisplay + var textContent = "" + if let ctLineDisplay = sub as? MTCTLineDisplay, + let attrString = ctLineDisplay.attributedString { + textContent = " text=\"\(attrString.string)\"" + } + + print(" Sub[\(i)]: type=\(type(of: sub)), y=\(sub.position.y), x=\(sub.position.x), width=\(sub.width), rightEdge=\(rightEdge)\(textContent)\(clipped ? " ⚠️ CLIPPED" : "")") + + // Show internal structure for MTMathListDisplay + if let mathListDisplay = sub as? MTMathListDisplay, !mathListDisplay.subDisplays.isEmpty { + print(" → Contains \(mathListDisplay.subDisplays.count) sub-displays:") + for (j, innerSub) in mathListDisplay.subDisplays.enumerated() { + var innerTextContent = "" + if let innerCTLineDisplay = innerSub as? MTCTLineDisplay, + let innerAttrString = innerCTLineDisplay.attributedString { + innerTextContent = " text=\"\(innerAttrString.string)\"" + } + print(" [\(j)]: type=\(type(of: innerSub)), y=\(innerSub.position.y), x=\(innerSub.position.x), width=\(innerSub.width)\(innerTextContent)") + } + } + + if clipped { + print(" ⚠️ CLIPPING: rightEdge \(rightEdge) > resultWidth \(resultWidth)") + print(" Clipped amount: \(rightEdge - resultWidth)") + } + } + } + print("=== END DEBUG ===\n") + } + + return CGSize(width: resultWidth, height: resultHeight) + } + + #if os(macOS) + public func sizeThatFits(_ size: CGSize) -> CGSize { + return _sizeThatFits(size) + } + #else + public override func sizeThatFits(_ size: CGSize) -> CGSize { + return _sizeThatFits(size) + } + #endif + +#if os(macOS) + func setNeedsDisplay() { self.needsDisplay = true } + func setNeedsLayout() { self.needsLayout = true } + public override var fittingSize: CGSize { _sizeThatFits(CGSizeZero) } + public override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) } + override public func layout() { + self._layoutSubviews() + super.layout() + } +#else + public override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) } + override public func layoutSubviews() { _layoutSubviews() } +#endif + +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTTypesetter+Tokenization.swift b/third-party/SwiftMath/Sources/MathRender/MTTypesetter+Tokenization.swift new file mode 100644 index 0000000000..3911df6499 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTTypesetter+Tokenization.swift @@ -0,0 +1,68 @@ +// +// MTTypesetter+Tokenization.swift +// SwiftMath +// +// Created by Claude Code on 2025-12-16. +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation +import CoreGraphics + +extension MTTypesetter { + + /// Create a line for a math list using the new tokenization approach + /// This is an alternative to the existing createLineForMathList that uses + /// pre-tokenization and greedy line fitting + static func createLineForMathListWithTokenization( + _ mathList: MTMathList?, + font: MTFont?, + style: MTLineStyle, + cramped: Bool, + spaced: Bool, + maxWidth: CGFloat + ) -> MTMathListDisplay? { + guard let mathList = mathList else { return nil } + guard let font = font else { return nil } + guard !mathList.atoms.isEmpty else { + // Return empty display instead of nil (matches KaTeX behavior) + return MTMathListDisplay(withDisplays: [], range: NSMakeRange(0, 0)) + } + + // Phase 0: Preprocess atoms to fuse ordinary characters + // This is critical for accents and other structures where multi-character + // text like "xyzw" should stay together as a single atom + let preprocessedAtoms = MTTypesetter.preprocessMathList(mathList) + + // Phase 1: Tokenize atoms into breakable elements + let tokenizer = MTAtomTokenizer(font: font, style: style, cramped: cramped, maxWidth: maxWidth) + let elements = tokenizer.tokenize(preprocessedAtoms) + + guard !elements.isEmpty else { return nil } + + // Phase 2: Fit elements into lines + let margin = spaced ? font.mathTable?.muUnit ?? 0 : 0 + let fitter = MTLineFitter(maxWidth: maxWidth, margin: margin) + let fittedLines = fitter.fitLines(elements) + + // Phase 3: Generate displays from fitted lines + let generator = MTDisplayGenerator(font: font, style: style) + let displays = generator.generateDisplays(from: fittedLines, startPosition: CGPoint.zero) + + // Determine range from atoms + let range: NSRange + if let firstAtom = mathList.atoms.first, let lastAtom = mathList.atoms.last { + let start = firstAtom.indexRange.location + let end = NSMaxRange(lastAtom.indexRange) + range = NSMakeRange(start, end - start) + } else { + range = NSMakeRange(0, 0) + } + + // Create and return the math list display + let mathListDisplay = MTMathListDisplay(withDisplays: displays, range: range) + + return mathListDisplay + } +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTTypesetter.swift b/third-party/SwiftMath/Sources/MathRender/MTTypesetter.swift new file mode 100644 index 0000000000..72d67a2315 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTTypesetter.swift @@ -0,0 +1,1927 @@ +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Kostub Deshmukh. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation +import CoreText + +// MARK: - Inter Element Spacing + +enum InterElementSpaceType : Int { + case invalid = -1 + case none = 0 + case thin + case nsThin // Thin but not in script mode + case nsMedium + case nsThick +} + +var interElementSpaceArray = [[InterElementSpaceType]]() +private let interElementLock = NSLock() + +func getInterElementSpaces() -> [[InterElementSpaceType]] { + if interElementSpaceArray.isEmpty { + + interElementLock.lock() + defer { interElementLock.unlock() } + guard interElementSpaceArray.isEmpty else { return interElementSpaceArray } + + interElementSpaceArray = + // ordinary operator binary relation open close punct fraction + [ [.none, .thin, .nsMedium, .nsThick, .none, .none, .none, .nsThin], // ordinary + [.thin, .thin, .invalid, .nsThick, .none, .none, .none, .nsThin], // operator + [.nsMedium, .nsMedium, .invalid, .invalid, .nsMedium, .invalid, .invalid, .nsMedium], // binary + [.nsThick, .nsThick, .invalid, .none, .nsThick, .none, .none, .nsThick], // relation + [.none, .none, .invalid, .none, .none, .none, .none, .none], // open + [.none, .thin, .nsMedium, .nsThick, .none, .none, .none, .nsThin], // close + [.nsThin, .nsThin, .invalid, .nsThin, .nsThin, .nsThin, .nsThin, .nsThin], // punct + [.nsThin, .thin, .nsMedium, .nsThick, .nsThin, .none, .nsThin, .nsThin], // fraction + [.nsMedium, .nsThin, .nsMedium, .nsThick, .none, .none, .none, .nsThin]] // radical + } + return interElementSpaceArray +} + + +// Get's the index for the given type. If row is true, the index is for the row (i.e. left element) otherwise it is for the column (right element) +func getInterElementSpaceArrayIndexForType(_ type:MTMathAtomType, row:Bool) -> Int { + switch type { + case .color, .textcolor, .colorBox, .ordinary, .placeholder: // A placeholder is treated as ordinary + return 0 + case .largeOperator: + return 1 + case .binaryOperator: + return 2; + case .relation: + return 3; + case .open: + return 4; + case .close: + return 5; + case .punctuation: + return 6; + case .fraction, // Fraction and inner are treated the same. + .inner: + return 7; + case .radical: + if row { + // Radicals have inter element spaces only when on the left side. + // Note: This is a departure from latex but we don't want \sqrt{4}4 to look weird so we put a space in between. + // They have the same spacing as ordinary except with ordinary. + return 8; + } else { + // Treat radical as ordinary on the right side + return 0 + } + // Numbers, variables, and unary operators are treated as ordinary + case .number, .variable, .unaryOperator: + return 0 + // Decorative types (accent, underline, overline) are treated as ordinary + case .accent, .underline, .overline: + return 0 + // Special types that don't typically participate in spacing are treated as ordinary + case .boundary, .space, .style, .table: + return 0 + } +} + +// MARK: - Italics +// mathit +func getItalicized(_ ch:Character) -> UTF32Char { + var unicode = ch.utf32Char + + // Special cases for italics + if ch == "h" { return UnicodeSymbol.planksConstant } + // Dotless i (U+0131) → Mathematical Italic Small Dotless I (U+1D6A4) + if ch == "\u{0131}" { return 0x1D6A4 } + // Dotless j (U+0237) → Mathematical Italic Small Dotless J (U+1D6A5) + if ch == "\u{0237}" { return 0x1D6A5 } + + if ch.isUpperEnglish { + unicode = UnicodeSymbol.capitalItalicStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + unicode = UnicodeSymbol.lowerItalicStart + (ch.utf32Char - Character("a").utf32Char) + } else if ch.isCapitalGreek { + // Capital Greek characters + unicode = UnicodeSymbol.greekCapitalItalicStart + (ch.utf32Char - UnicodeSymbol.capitalGreekStart) + } else if ch.isLowerGreek { + // Greek characters + unicode = UnicodeSymbol.greekLowerItalicStart + (ch.utf32Char - UnicodeSymbol.lowerGreekStart) + } else if ch.isGreekSymbol { + return UnicodeSymbol.greekSymbolItalicStart + ch.greekSymbolOrder! + } + // Note there are no italicized numbers in unicode so we don't support italicizing numbers. + return unicode +} + +// mathbf +func getBold(_ ch:Character) -> UTF32Char { + var unicode = ch.utf32Char + if ch.isUpperEnglish { + unicode = UnicodeSymbol.mathCapitalBoldStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + unicode = UnicodeSymbol.mathLowerBoldStart + (ch.utf32Char - Character("a").utf32Char) + } else if ch.isCapitalGreek { + // Capital Greek characters + unicode = UnicodeSymbol.greekCapitalBoldStart + (ch.utf32Char - UnicodeSymbol.capitalGreekStart); + } else if ch.isLowerGreek { + // Greek characters + unicode = UnicodeSymbol.greekLowerBoldStart + (ch.utf32Char - UnicodeSymbol.lowerGreekStart); + } else if ch.isGreekSymbol { + return UnicodeSymbol.greekSymbolBoldStart + ch.greekSymbolOrder! + } else if ch.isNumber { + unicode = UnicodeSymbol.numberBoldStart + (ch.utf32Char - Character("0").utf32Char) + } + return unicode +} + +// mathbfit +func getBoldItalic(_ ch:Character) -> UTF32Char { + var unicode = ch.utf32Char + if ch.isUpperEnglish { + unicode = UnicodeSymbol.mathCapitalBoldItalicStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + unicode = UnicodeSymbol.mathLowerBoldItalicStart + (ch.utf32Char - Character("a").utf32Char) + } else if ch.isCapitalGreek { + // Capital Greek characters + unicode = UnicodeSymbol.greekCapitalBoldItalicStart + (ch.utf32Char - UnicodeSymbol.capitalGreekStart); + } else if ch.isLowerGreek { + // Greek characters + unicode = UnicodeSymbol.greekLowerBoldItalicStart + (ch.utf32Char - UnicodeSymbol.lowerGreekStart); + } else if ch.isGreekSymbol { + return UnicodeSymbol.greekSymbolBoldItalicStart + ch.greekSymbolOrder! + } else if ch.isNumber { + // No bold italic for numbers so we just bold them. + unicode = getBold(ch); + } + return unicode; +} + +// LaTeX default +func getDefaultStyle(_ ch:Character) -> UTF32Char { + if ch.isLowerEnglish || ch.isUpperEnglish || ch.isLowerGreek || ch.isGreekSymbol { + return getItalicized(ch); + } else if ch == "\u{0131}" || ch == "\u{0237}" { + // Dotless i (U+0131) and dotless j (U+0237) should be italicized in default style + return getItalicized(ch); + } else if ch.isNumber || ch.isCapitalGreek { + // In the default style numbers and capital greek is roman + return ch.utf32Char + } else if ch == "." { + // . is treated as a number in our code, but it doesn't change fonts. + return ch.utf32Char + } else { + NSException(name: NSExceptionName("IllegalCharacter"), reason: "Unknown character \(ch) for default style.").raise() + } + return ch.utf32Char +} + +// mathcal/mathscr (caligraphic or script) +func getCaligraphic(_ ch:Character) -> UTF32Char { + // Caligraphic has lots of exceptions: + switch ch { + case "B": + return 0x212C; // Script B (bernoulli) + case "E": + return 0x2130; // Script E (emf) + case "F": + return 0x2131; // Script F (fourier) + case "H": + return 0x210B; // Script H (hamiltonian) + case "I": + return 0x2110; // Script I + case "L": + return 0x2112; // Script L (laplace) + case "M": + return 0x2133; // Script M (M-matrix) + case "R": + return 0x211B; // Script R (Riemann integral) + case "e": + return 0x212F; // Script e (Natural exponent) + case "g": + return 0x210A; // Script g (real number) + case "o": + return 0x2134; // Script o (order) + default: + break; + } + var unicode:UTF32Char + if ch.isUpperEnglish { + unicode = UnicodeSymbol.mathCapitalScriptStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + // Latin Modern Math does not have lower case caligraphic characters, so we use + // the default style instead of showing a ? + unicode = getDefaultStyle(ch) + } else { + // Caligraphic characters don't exist for greek or numbers, we give them the + // default treatment. + unicode = getDefaultStyle(ch) + } + return unicode; +} + +// mathtt (monospace) +func getTypewriter(_ ch:Character) -> UTF32Char { + if ch.isUpperEnglish { + return UnicodeSymbol.mathCapitalTTStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + return UnicodeSymbol.mathLowerTTStart + (ch.utf32Char - Character("a").utf32Char) + } else if ch.isNumber { + return UnicodeSymbol.numberTTStart + (ch.utf32Char - Character("0").utf32Char) + } + // Monospace characters don't exist for greek, we give them the + // default treatment. + return getDefaultStyle(ch); +} + +// mathsf +func getSansSerif(_ ch:Character) -> UTF32Char { + if ch.isUpperEnglish { + return UnicodeSymbol.mathCapitalSansSerifStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + return UnicodeSymbol.mathLowerSansSerifStart + (ch.utf32Char - Character("a").utf32Char) + } else if ch.isNumber { + return UnicodeSymbol.numberSansSerifStart + (ch.utf32Char - Character("0").utf32Char) + } + // Sans-serif characters don't exist for greek, we give them the + // default treatment. + return getDefaultStyle(ch); +} + +// mathfrak +func getFraktur(_ ch:Character) -> UTF32Char { + // Fraktur has exceptions: + switch ch { + case "C": + return 0x212D; // C Fraktur + case "H": + return 0x210C; // Hilbert space + case "I": + return 0x2111; // Imaginary + case "R": + return 0x211C; // Real + case "Z": + return 0x2128; // Z Fraktur + default: + break; + } + if ch.isUpperEnglish { + return UnicodeSymbol.mathCapitalFrakturStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + return UnicodeSymbol.mathLowerFrakturStart + (ch.utf32Char - Character("a").utf32Char) + } + // Fraktur characters don't exist for greek & numbers, we give them the + // default treatment. + return getDefaultStyle(ch); +} + +// mathbb (double struck) +func getBlackboard(_ ch:Character) -> UTF32Char { + // Blackboard has lots of exceptions: + switch(ch) { + case "C": + return 0x2102; // Complex numbers + case "H": + return 0x210D; // Quarternions + case "N": + return 0x2115; // Natural numbers + case "P": + return 0x2119; // Primes + case "Q": + return 0x211A; // Rationals + case "R": + return 0x211D; // Reals + case "Z": + return 0x2124; // Integers + default: + break; + } + if ch.isUpperEnglish { + return UnicodeSymbol.mathCapitalBlackboardStart + (ch.utf32Char - Character("A").utf32Char) + } else if ch.isLowerEnglish { + return UnicodeSymbol.mathLowerBlackboardStart + (ch.utf32Char - Character("a").utf32Char) + } else if ch.isNumber { + return UnicodeSymbol.numberBlackboardStart + (ch.utf32Char - Character("0").utf32Char) + } + // Blackboard characters don't exist for greek, we give them the + // default treatment. + return getDefaultStyle(ch); +} + +func styleCharacter(_ ch:Character, fontStyle:MTFontStyle) -> UTF32Char { + switch fontStyle { + case .defaultStyle: + return getDefaultStyle(ch); + case .roman: + return ch.utf32Char + case .bold: + return getBold(ch); + case .italic: + return getItalicized(ch); + case .boldItalic: + return getBoldItalic(ch); + case .caligraphic: + return getCaligraphic(ch); + case .typewriter: + return getTypewriter(ch); + case .sansSerif: + return getSansSerif(ch); + case .fraktur: + return getFraktur(ch); + case .blackboard: + return getBlackboard(ch); + } +} + +func changeFont(_ str:String, fontStyle:MTFontStyle) -> String { + var retval = "" + let codes = Array(str) + for i in 0.. MTMathListDisplay? { + let finalizedList = mathList?.finalized + // default is not cramped, no width constraint + return self.createLineForMathList(finalizedList, font:font, style:style, cramped:false, maxWidth: 0) + } + + static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle, maxWidth:CGFloat) -> MTMathListDisplay? { + let finalizedList = mathList?.finalized + // default is not cramped + return self.createLineForMathList(finalizedList, font:font, style:style, cramped:false, maxWidth: maxWidth) + } + + // Internal + static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle, cramped:Bool) -> MTMathListDisplay? { + return self.createLineForMathList(mathList, font:font, style:style, cramped:cramped, spaced:false, maxWidth: 0) + } + + // Internal + static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle, cramped:Bool, maxWidth:CGFloat) -> MTMathListDisplay? { + return self.createLineForMathList(mathList, font:font, style:style, cramped:cramped, spaced:false, maxWidth: maxWidth) + } + + // Internal + static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle, cramped:Bool, spaced:Bool) -> MTMathListDisplay? { + return self.createLineForMathList(mathList, font:font, style:style, cramped:cramped, spaced:spaced, maxWidth: 0) + } + + // Internal + static func createLineForMathList(_ mathList:MTMathList?, font:MTFont?, style:MTLineStyle, cramped:Bool, spaced:Bool, maxWidth:CGFloat) -> MTMathListDisplay? { + assert(font != nil) + + // Always use tokenization approach + // The tokenization path handles both constrained (maxWidth > 0) and unconstrained (maxWidth = 0) rendering + return createLineForMathListWithTokenization( + mathList, + font: font, + style: style, + cramped: cramped, + spaced: spaced, + maxWidth: maxWidth + ) + } + + static var placeholderColor: MTColor { MTColor.blue } + + init(withFont font:MTFont?, style:MTLineStyle, cramped:Bool, spaced:Bool, maxWidth:CGFloat = 0) { + self.font = font + self.displayAtoms = [MTDisplay]() + self.currentPosition = CGPoint.zero + self.cramped = cramped + self.spaced = spaced + self.maxWidth = maxWidth + self.style = style + } + + static func preprocessMathList(_ ml:MTMathList?) -> [MTMathAtom] { + // Note: Some of the preprocessing described by the TeX algorithm is done in the finalize method of MTMathList. + // Specifically rules 5 & 6 in Appendix G are handled by finalize. + // This function does not do a complete preprocessing as specified by TeX either. It removes any special atom types + // that are not included in TeX and applies Rule 14 to merge ordinary characters. + var preprocessed = [MTMathAtom]() // arrayWithCapacity:ml.atoms.count) + var prevNode:MTMathAtom! = nil + preprocessed.reserveCapacity(ml!.atoms.count) + for atom in ml!.atoms { + if atom.type == .variable || atom.type == .number { + // This is not a TeX type node. TeX does this during parsing the input. + // switch to using the italic math font + // We convert it to ordinary + let newFont = changeFont(atom.nucleus, fontStyle: atom.fontStyle) // mathItalicize(atom.nucleus) + atom.type = .ordinary + atom.nucleus = newFont + } else if atom.type == .unaryOperator { + // Neither of these are TeX nodes. TeX treats these as Ordinary. So will we. + atom.type = .ordinary + } else if atom.type == .binaryOperator { + // CRITICAL FIX: Convert binary operators to ordinary (unary) in appropriate contexts + // According to TeX rules (TeXbook Appendix G, Rule 5), a binary operator is converted + // to ordinary when it appears after: Bin, Op, Rel, Open, Punct, or at the beginning + // This handles cases like "=-2" where minus should be unary, not binary + let shouldConvertToOrdinary: Bool + if prevNode == nil { + // At the beginning of the list + shouldConvertToOrdinary = true + } else { + switch prevNode.type { + case .binaryOperator, .relation, .open, .punctuation, .largeOperator: + shouldConvertToOrdinary = true + default: + shouldConvertToOrdinary = false + } + } + + if shouldConvertToOrdinary { + atom.type = .ordinary + } + } + + if atom.type == .ordinary { + // This is Rule 14 to merge ordinary characters. + // combine ordinary atoms together + // CRITICAL FIX: Only fuse atoms with the same fontStyle + // This prevents fusing roman text (\text{...}) with italic math variables (A, B, etc.) + // which would cause incorrect line breaking when the combined string is tokenized + if prevNode != nil && prevNode.type == .ordinary && prevNode.subScript == nil && prevNode.superScript == nil && prevNode.fontStyle == atom.fontStyle { + prevNode.fuse(with: atom) + // skip the current node, we are done here. + continue + } + } + + // TODO: add italic correction here or in second pass? + prevNode = atom + preprocessed.append(atom) + } + return preprocessed + } + + // returns the size of the font in this style + static func getStyleSize(_ style:MTLineStyle, font:MTFont?) -> CGFloat { + let original = font!.fontSize + let scaled: CGFloat + switch style { + case .display, .text: + scaled = original + case .script: + scaled = original * font!.mathTable!.scriptScaleDown + case .scriptOfScript: + scaled = original * font!.mathTable!.scriptScriptScaleDown + } + // Apply minimum font size threshold to prevent deeply nested exponents + // from becoming unreadable (common for expressions like 2^{2^{2^2}}) + // Minimum of 6pt ensures readability while maintaining proper hierarchy + return max(scaled, 6.0) + } + + // MARK: - Spacing + + // Returned in units of mu = 1/18 em. + func getSpacingInMu(_ type: InterElementSpaceType) -> Int { + // let valid = [MTLineStyle.display, .text] + switch type { + case .invalid: return -1 + case .none: return 0 + case .thin: return 3 + case .nsThin: return style.isNotScript ? 3 : 0; + case .nsMedium: return style.isNotScript ? 4 : 0; + case .nsThick: return style.isNotScript ? 5 : 0; + } + } + + func getInterElementSpace(_ left: MTMathAtomType, right:MTMathAtomType) -> CGFloat { + let leftIndex = getInterElementSpaceArrayIndexForType(left, row: true) + let rightIndex = getInterElementSpaceArrayIndexForType(right, row: false) + let spaceArray = getInterElementSpaces()[Int(leftIndex)] + let spaceTypeObj = spaceArray[Int(rightIndex)] + let spaceType = spaceTypeObj + assert(spaceType != .invalid, "Invalid space between \(left) and \(right)") + + let spaceMultipler = self.getSpacingInMu(spaceType) + if spaceMultipler > 0 { + // 1 em = size of font in pt. space multipler is in multiples mu or 1/18 em + return CGFloat(spaceMultipler) * styleFont.mathTable!.muUnit + } + return 0 + } + + + // MARK: - Subscript/Superscript + + func scriptStyle() -> MTLineStyle { + switch style { + case .display, .text: return .script + case .script, .scriptOfScript: return .scriptOfScript + } + } + + // subscript is always cramped + func subscriptCramped() -> Bool { true } + + // superscript is cramped only if the current style is cramped + func superScriptCramped() -> Bool { cramped } + + func superScriptShiftUp() -> CGFloat { + if cramped { + return styleFont.mathTable!.superscriptShiftUpCramped; + } else { + return styleFont.mathTable!.superscriptShiftUp; + } + } + + // make scripts for the last atom + // index is the index of the element which is getting the sub/super scripts. + func makeScripts(_ atom: MTMathAtom?, display:MTDisplay?, index:UInt, delta:CGFloat) { + guard let atom = atom else { return } + guard atom.subScript != nil || atom.superScript != nil else { return } + guard let mathTable = styleFont.mathTable else { return } + + var superScriptShiftUp = 0.0 + var subscriptShiftDown = 0.0 + + display?.hasScript = true + if !(display is MTCTLineDisplay), let display = display { + // get the font in script style + let scriptFontSize = Self.getStyleSize(self.scriptStyle(), font:font) + let scriptFont = font.copy(withSize: scriptFontSize) + let scriptFontMetrics = scriptFont.mathTable + + // if it is not a simple line then + if let scriptFontMetrics = scriptFontMetrics { + superScriptShiftUp = display.ascent - scriptFontMetrics.superscriptBaselineDropMax + subscriptShiftDown = display.descent + scriptFontMetrics.subscriptBaselineDropMin + } + } + + if atom.superScript == nil { + guard let _subscript = MTTypesetter.createLineForMathList(atom.subScript, font:font, style:self.scriptStyle(), cramped:self.subscriptCramped()) else { return } + _subscript.type = .ssubscript + _subscript.index = Int(index) + + subscriptShiftDown = fmax(subscriptShiftDown, mathTable.subscriptShiftDown); + subscriptShiftDown = fmax(subscriptShiftDown, _subscript.ascent - mathTable.subscriptTopMax); + // add the subscript + _subscript.position = CGPointMake(currentPosition.x, currentPosition.y - subscriptShiftDown); + displayAtoms.append(_subscript) + // update the position + currentPosition.x += _subscript.width + mathTable.spaceAfterScript; + return; + } + + guard let superScript = MTTypesetter.createLineForMathList(atom.superScript, font:font, style:self.scriptStyle(), cramped:self.superScriptCramped()) else { return } + superScript.type = .superscript + superScript.index = Int(index); + superScriptShiftUp = fmax(superScriptShiftUp, self.superScriptShiftUp()); + superScriptShiftUp = fmax(superScriptShiftUp, superScript.descent + mathTable.superscriptBottomMin); + + if atom.subScript == nil { + superScript.position = CGPointMake(currentPosition.x, currentPosition.y + superScriptShiftUp); + displayAtoms.append(superScript) + // update the position + currentPosition.x += superScript.width + mathTable.spaceAfterScript; + return; + } + guard let ssubscript = MTTypesetter.createLineForMathList(atom.subScript, font:font, style:self.scriptStyle(), cramped:self.subscriptCramped()) else { return } + ssubscript.type = .ssubscript + ssubscript.index = Int(index) + subscriptShiftDown = fmax(subscriptShiftDown, mathTable.subscriptShiftDown); + + // joint positioning of subscript & superscript + let subSuperScriptGap = (superScriptShiftUp - superScript.descent) + (subscriptShiftDown - ssubscript.ascent); + if (subSuperScriptGap < mathTable.subSuperscriptGapMin) { + // Set the gap to atleast as much + subscriptShiftDown += mathTable.subSuperscriptGapMin - subSuperScriptGap; + let superscriptBottomDelta = mathTable.superscriptBottomMaxWithSubscript - (superScriptShiftUp - superScript.descent); + if (superscriptBottomDelta > 0) { + // superscript is lower than the max allowed by the font with a subscript. + superScriptShiftUp += superscriptBottomDelta; + subscriptShiftDown -= superscriptBottomDelta; + } + } + // The delta is the italic correction above that shift superscript position + superScript.position = CGPointMake(currentPosition.x + delta, currentPosition.y + superScriptShiftUp); + displayAtoms.append(superScript) + ssubscript.position = CGPointMake(currentPosition.x, currentPosition.y - subscriptShiftDown); + displayAtoms.append(ssubscript) + currentPosition.x += max(superScript.width + delta, ssubscript.width) + mathTable.spaceAfterScript; + } + + // MARK: - Helper Functions + + /// Safely converts an NSRange location to UInt, returning 0 if the location is invalid (NSNotFound) + /// This prevents "Negative value is not representable" crashes when converting NSNotFound to UInt + private func safeUIntFromLocation(_ location: Int) -> UInt { + if location == NSNotFound || location < 0 { + return 0 + } + return UInt(location) + } + + // MARK: - Fractions + + func numeratorShiftUp(_ hasRule:Bool) -> CGFloat { + if hasRule { + if style == .display { + return styleFont.mathTable!.fractionNumeratorDisplayStyleShiftUp + } else { + return styleFont.mathTable!.fractionNumeratorShiftUp + } + } else { + if style == .display { + return styleFont.mathTable!.stackTopDisplayStyleShiftUp + } else { + return styleFont.mathTable!.stackTopShiftUp + } + } + } + + func numeratorGapMin() -> CGFloat { + if style == .display { + return styleFont.mathTable!.fractionNumeratorDisplayStyleGapMin; + } else { + return styleFont.mathTable!.fractionNumeratorGapMin; + } + } + + func denominatorShiftDown(_ hasRule:Bool) -> CGFloat { + if hasRule { + if style == .display { + return styleFont.mathTable!.fractionDenominatorDisplayStyleShiftDown; + } else { + return styleFont.mathTable!.fractionDenominatorShiftDown; + } + } else { + if style == .display { + return styleFont.mathTable!.stackBottomDisplayStyleShiftDown; + } else { + return styleFont.mathTable!.stackBottomShiftDown; + } + } + } + + func denominatorGapMin() -> CGFloat { + if style == .display { + return styleFont.mathTable!.fractionDenominatorDisplayStyleGapMin; + } else { + return styleFont.mathTable!.fractionDenominatorGapMin; + } + } + + func stackGapMin() -> CGFloat { + if style == .display { + return styleFont.mathTable!.stackDisplayStyleGapMin; + } else { + return styleFont.mathTable!.stackGapMin; + } + } + + func fractionDelimiterHeight()-> CGFloat { + if style == .display { + return styleFont.mathTable!.fractionDelimiterDisplayStyleSize; + } else { + return styleFont.mathTable!.fractionDelimiterSize; + } + } + + func fractionStyle() -> MTLineStyle { + // Keep fractions at the same style level instead of incrementing. + // This ensures that fraction numerators/denominators have the same + // font size as regular text, preventing them from appearing too small + // in inline mode or when nested. + return style + } + + func makeFraction(_ frac:MTFraction?) -> MTDisplay? { + guard let frac = frac else { return nil } + + // lay out the parts of the fraction + let numeratorStyle: MTLineStyle + let denominatorStyle: MTLineStyle + + if frac.isContinuedFraction { + // Continued fractions always use display style + numeratorStyle = .display + denominatorStyle = .display + } else { + // Regular fractions use adaptive style + let fractionStyle = self.fractionStyle; + numeratorStyle = fractionStyle() + denominatorStyle = fractionStyle() + } + + let numeratorDisplay = MTTypesetter.createLineForMathList(frac.numerator, font:font, style:numeratorStyle, cramped:false) + let denominatorDisplay = MTTypesetter.createLineForMathList(frac.denominator, font:font, style:denominatorStyle, cramped:true) + + // Handle empty numerator or denominator by creating empty displays + let numDisplay = numeratorDisplay ?? MTMathListDisplay(withDisplays: [], range: NSMakeRange(0, 0)) + let denomDisplay = denominatorDisplay ?? MTMathListDisplay(withDisplays: [], range: NSMakeRange(0, 0)) + + // determine the location of the numerator + var numeratorShiftUp = self.numeratorShiftUp(frac.hasRule) + var denominatorShiftDown = self.denominatorShiftDown(frac.hasRule) + let barLocation = styleFont.mathTable!.axisHeight + let barThickness = frac.hasRule ? styleFont.mathTable!.fractionRuleThickness : 0 + + if frac.hasRule { + // This is the difference between the lowest edge of the numerator and the top edge of the fraction bar + let distanceFromNumeratorToBar = (numeratorShiftUp - numDisplay.descent) - (barLocation + barThickness/2); + // The distance should at least be displayGap + let minNumeratorGap = self.numeratorGapMin; + if distanceFromNumeratorToBar < minNumeratorGap() { + // This makes the distance between the bottom of the numerator and the top edge of the fraction bar + // at least minNumeratorGap. + numeratorShiftUp += (minNumeratorGap() - distanceFromNumeratorToBar); + } + + // Do the same for the denominator + // This is the difference between the top edge of the denominator and the bottom edge of the fraction bar + let distanceFromDenominatorToBar = (barLocation - barThickness/2) - (denomDisplay.ascent - denominatorShiftDown); + // The distance should at least be denominator gap + let minDenominatorGap = self.denominatorGapMin; + if distanceFromDenominatorToBar < minDenominatorGap() { + // This makes the distance between the top of the denominator and the bottom of the fraction bar to be exactly + // minDenominatorGap + denominatorShiftDown += (minDenominatorGap() - distanceFromDenominatorToBar); + } + } else { + // This is the distance between the numerator and the denominator + let clearance = (numeratorShiftUp - numDisplay.descent) - (denomDisplay.ascent - denominatorShiftDown); + // This is the minimum clearance between the numerator and denominator. + // For ruleless fractions (like binom, choose, atop), use 1.5x the standard gap + // for better visual separation, following TeX's approach for binomial coefficients + let minGap = self.stackGapMin() * 1.5 + if clearance < minGap { + numeratorShiftUp += (minGap - clearance)/2; + denominatorShiftDown += (minGap - clearance)/2; + } + } + + let display = MTFractionDisplay(withNumerator: numDisplay, denominator: denomDisplay, position: currentPosition, range: frac.indexRange) + + display.numeratorUp = numeratorShiftUp; + display.denominatorDown = denominatorShiftDown; + display.lineThickness = barThickness; + display.linePosition = barLocation; + if frac.leftDelimiter.isEmpty && frac.rightDelimiter.isEmpty { + return display + } else { + return self.addDelimitersToFractionDisplay(display, forFraction: frac) + } + } + + func addDelimitersToFractionDisplay(_ display: MTFractionDisplay, forFraction frac: MTFraction) -> MTDisplay { + assert(!frac.leftDelimiter.isEmpty || !frac.rightDelimiter.isEmpty, "Fraction should have delimiters to call this function") + + var innerElements = [MTDisplay]() + let glyphHeight = self.fractionDelimiterHeight + var position = CGPoint.zero + if !frac.leftDelimiter.isEmpty { + if let leftGlyph = self.findGlyphForBoundary(frac.leftDelimiter, withHeight: glyphHeight()) { + leftGlyph.position = position + position.x += leftGlyph.width + innerElements.append(leftGlyph) + } + } + + display.position = position + position.x += display.width + innerElements.append(display) + + if !frac.rightDelimiter.isEmpty { + if let rightGlyph = self.findGlyphForBoundary(frac.rightDelimiter, withHeight: glyphHeight()) { + rightGlyph.position = position + position.x += rightGlyph.width + innerElements.append(rightGlyph) + } + } + let innerDisplay = MTMathListDisplay(withDisplays: innerElements, range: frac.indexRange) + innerDisplay.position = currentPosition + return innerDisplay + } + + // MARK: - Radicals + + func radicalVerticalGap() -> CGFloat { + if style == .display { + return styleFont.mathTable!.radicalDisplayStyleVerticalGap + } else { + return styleFont.mathTable!.radicalVerticalGap + } + } + + func getRadicalGlyphWithHeight(_ radicalHeight:CGFloat) -> MTDisplayDS? { + var glyphAscent=CGFloat(0), glyphDescent=CGFloat(0), glyphWidth=CGFloat(0) + + let radicalGlyph = self.findGlyphForCharacterAtIndex("\u{221A}".startIndex, inString:"\u{221A}") + let glyph = self.findGlyph(radicalGlyph, withHeight:radicalHeight, glyphAscent:&glyphAscent, glyphDescent:&glyphDescent, glyphWidth:&glyphWidth) + + var glyphDisplay:MTDisplayDS? + if glyphAscent + glyphDescent < radicalHeight { + // the glyphs is not as large as required. A glyph needs to be constructed using the extenders. + glyphDisplay = self.constructGlyph(radicalGlyph, withHeight:radicalHeight) + } + + if glyphDisplay == nil { + // No constructed display so use the glyph we got. + glyphDisplay = MTGlyphDisplay(withGlpyh: glyph, range: NSMakeRange(NSNotFound, 0), font:styleFont) + glyphDisplay!.ascent = glyphAscent; + glyphDisplay!.descent = glyphDescent; + glyphDisplay!.width = glyphWidth; + } + return glyphDisplay; + } + + func makeRadical(_ radicand:MTMathList?, range:NSRange) -> MTRadicalDisplay? { + let innerDisplay = MTTypesetter.createLineForMathList(radicand, font:font, style:style, cramped:true)! + var clearance = self.radicalVerticalGap() + let radicalRuleThickness = styleFont.mathTable!.radicalRuleThickness + let radicalHeight = innerDisplay.ascent + innerDisplay.descent + clearance + radicalRuleThickness + + let glyph = self.getRadicalGlyphWithHeight(radicalHeight)! + + // Note this is a departure from Latex. Latex assumes that glyphAscent == thickness. + // Open type math makes no such assumption, and ascent and descent are independent of the thickness. + // Latex computes delta as descent - (h(inner) + d(inner) + clearance) + // but since we may not have ascent == thickness, we modify the delta calculation slightly. + // If the font designer followes Latex conventions, it will be identical. + let delta = (glyph.descent + glyph.ascent) - (innerDisplay.ascent + innerDisplay.descent + clearance + radicalRuleThickness) + if delta > 0 { + clearance += delta/2 // increase the clearance to center the radicand inside the sign. + } + + // we need to shift the radical glyph up, to coincide with the baseline of inner. + // The new ascent of the radical glyph should be thickness + adjusted clearance + h(inner) + let radicalAscent = radicalRuleThickness + clearance + innerDisplay.ascent + let shiftUp = radicalAscent - glyph.ascent // Note: if the font designer followed latex conventions, this is the same as glyphAscent == thickness. + glyph.shiftDown = -shiftUp + + let radical = MTRadicalDisplay(withRadicand: innerDisplay, glyph: glyph, position: currentPosition, range: range) + radical.ascent = radicalAscent + styleFont.mathTable!.radicalExtraAscender + radical.topKern = styleFont.mathTable!.radicalExtraAscender + radical.lineThickness = radicalRuleThickness + // Note: Until we have radical construction from parts, it is possible that glyphAscent+glyphDescent is less + // than the requested height of the glyph (i.e. radicalHeight), so in the case the innerDisplay has a larger + // descent we use the innerDisplay's descent. + radical.descent = max(glyph.ascent + glyph.descent - radicalAscent, innerDisplay.descent) + radical.width = glyph.width + innerDisplay.width + return radical + } + + // MARK: - Glyphs + + func findGlyph(_ glyph:CGGlyph, withHeight height:CGFloat, glyphAscent:inout CGFloat, glyphDescent:inout CGFloat, glyphWidth:inout CGFloat) -> CGGlyph { + let variants = styleFont.mathTable!.getVerticalVariantsForGlyph(glyph) + let numVariants = variants.count; + var glyphs = [CGGlyph]()// numVariants) + glyphs.reserveCapacity(numVariants) + for i in 0 ..< numVariants { + let glyph = variants[i]!.uint16Value + glyphs.append(glyph) + } + + var bboxes = [CGRect](repeating: CGRect.zero, count: numVariants) + var advances = [CGSize](repeating: CGSize.zero, count: numVariants) + + // Get the bounds for these glyphs + CTFontGetBoundingRectsForGlyphs(styleFont.ctFont, .horizontal, glyphs, &bboxes, numVariants) + CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, glyphs, &advances, numVariants); + var ascent=CGFloat(0), descent=CGFloat(0), width=CGFloat(0) + for i in 0..= height) { + glyphAscent = ascent; + glyphDescent = descent; + glyphWidth = width; + return glyphs[i] + } + } + glyphAscent = ascent; + glyphDescent = descent; + glyphWidth = width; + return glyphs[numVariants - 1] + } + + func constructGlyph(_ glyph:CGGlyph, withHeight glyphHeight:CGFloat) -> MTGlyphConstructionDisplay? { + let parts = styleFont.mathTable!.getVerticalGlyphAssembly(forGlyph: glyph) + if parts.count == 0 { + return nil + } + var glyphs = [NSNumber](), offsets = [NSNumber]() + var height:CGFloat=0 + self.constructGlyphWithParts(parts, glyphHeight:glyphHeight, glyphs:&glyphs, offsets:&offsets, height:&height) + var first = glyphs[0].uint16Value + let width = CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, &first, nil, 1); + let display = MTGlyphConstructionDisplay(withGlyphs: glyphs, offsets: offsets, font: styleFont) + display.width = width; + display.ascent = height; + display.descent = 0; // it's upto the rendering to adjust the display up or down. + return display; + } + + func constructGlyphWithParts(_ parts:[GlyphPart], glyphHeight:CGFloat, glyphs:inout [NSNumber], offsets:inout [NSNumber], height:inout CGFloat) { + // Loop forever until the glyph height is valid + for numExtenders in 0..= glyphHeight) { + // we are done + glyphs = glyphsRv; + offsets = offsetsRv; + height = minHeight; + return; + } else if (glyphHeight <= maxHeight) { + // spread the delta equally between all the connectors + let delta = glyphHeight - minHeight; + let deltaIncrease = Float(delta) / Float(glyphsRv.count - 1) + var lastOffset = CGFloat(0) + for i in 0.. CGGlyph { + // Get the character at index taking into account UTF-32 characters + var chars = Array(str[index].utf16) + + // Get the glyph from the font + var glyph = [CGGlyph](repeating: CGGlyph.zero, count: chars.count) + let found = CTFontGetGlyphsForCharacters(styleFont.ctFont, &chars, &glyph, chars.count) + if !found { + // Try fallback font if available + if let fallbackFont = styleFont.fallbackFont { + let fallbackFound = CTFontGetGlyphsForCharacters(fallbackFont, &chars, &glyph, chars.count) + if fallbackFound { + return glyph[0] + } + } + // the font did not contain a glyph for our character, so we just return 0 (notdef) + return 0 + } + return glyph[0] + } + + // MARK: - Large Operators + + func makeLargeOp(_ op:MTLargeOperator!) -> MTDisplay? { + // Show limits above/below in display mode + // For inline mode, we still center limits below for operators like \lim, but with tighter spacing + let limits = op.limits && (style == .display || style == .text) + var delta = CGFloat(0) + if op.nucleus.count == 1 { + var glyph = self.findGlyphForCharacterAtIndex(op.nucleus.startIndex, inString:op.nucleus) + if glyph != 0 { + // Enlarge large operators to make them visually distinctive + if style == .display { + // Display style: use large variant for mathematical display mode (~2.2em) + glyph = styleFont.mathTable!.getLargerGlyph(glyph, forDisplayStyle: true) + } else if style == .text { + // Text/inline style: use moderately larger variant to ensure operator is taller than surrounding text + glyph = styleFont.mathTable!.getLargerGlyph(glyph, forDisplayStyle: false) + } + // Script and scriptOfScript styles keep base size (compact rendering) + } + // This is be the italic correction of the character. + delta = styleFont.mathTable!.getItalicCorrection(glyph) + + // vertically center + let bbox = CTFontGetBoundingRectsForGlyphs(styleFont.ctFont, .horizontal, &glyph, nil, 1); + let width = CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, &glyph, nil, 1); + var ascent=CGFloat(0), descent=CGFloat(0) + getBboxDetails(bbox, ascent: &ascent, descent: &descent) + let shiftDown = 0.5*(ascent - descent) - styleFont.mathTable!.axisHeight; + let glyphDisplay = MTGlyphDisplay(withGlpyh: glyph, range: op.indexRange, font: styleFont) + glyphDisplay.ascent = ascent; + glyphDisplay.descent = descent; + glyphDisplay.width = width; + if (op.subScript != nil) && !limits { + // Remove italic correction from the width of the glyph if + // there is a subscript and limits is not set. + glyphDisplay.width -= delta; + } + glyphDisplay.shiftDown = shiftDown; + glyphDisplay.position = currentPosition; + return self.addLimitsToDisplay(glyphDisplay, forOperator:op, delta:delta) + } else { + // Create a regular node + let line = NSMutableAttributedString(string: op.nucleus) + // add the font + line.addAttribute(kCTFontAttributeName as NSAttributedString.Key, value:styleFont.ctFont!, range:NSMakeRange(0, line.length)) + let displayAtom = MTCTLineDisplay(withString: line, position: currentPosition, range: op.indexRange, font: styleFont, atoms: [op]) + return self.addLimitsToDisplay(displayAtom, forOperator:op, delta:0) + } + } + + func addLimitsToDisplay(_ display:MTDisplay?, forOperator op:MTLargeOperator, delta:CGFloat) -> MTDisplay? { + // If there is no subscript or superscript, just return the current display + if op.subScript == nil && op.superScript == nil { + currentPosition.x += display!.width + return display; + } + // Show limits above/below in both display and text (inline) modes + if op.limits && (style == .display || style == .text) { + // make limits (above/below positioning) + var superScript:MTMathListDisplay? = nil, subScript:MTMathListDisplay? = nil + + // Scale font for script style before creating scripts + // This matches how MTDisplayPreRenderer.renderScript() handles script sizing + let scriptStyle = self.scriptStyle() + let scriptFontSize = MTTypesetter.getStyleSize(scriptStyle, font: font) + let scriptFont = font.copy(withSize: scriptFontSize) + + if op.superScript != nil { + superScript = MTTypesetter.createLineForMathList(op.superScript, font:scriptFont, style:scriptStyle, cramped:self.superScriptCramped()) + } + if op.subScript != nil { + subScript = MTTypesetter.createLineForMathList(op.subScript, font:scriptFont, style:scriptStyle, cramped:self.subscriptCramped()) + } + assert((superScript != nil) || (subScript != nil), "At least one of superscript or subscript should have been present."); + let opsDisplay = MTLargeOpLimitsDisplay(withNucleus:display, upperLimit:superScript, lowerLimit:subScript, limitShift:delta/2, extraPadding:0) + + // Use standard OpenType MATH metrics for limit spacing + if superScript != nil { + let upperLimitGap = max(styleFont.mathTable!.upperLimitGapMin, styleFont.mathTable!.upperLimitBaselineRiseMin - superScript!.descent); + opsDisplay.upperLimitGap = upperLimitGap; + } + if subScript != nil { + let lowerLimitGap = max(styleFont.mathTable!.lowerLimitGapMin, styleFont.mathTable!.lowerLimitBaselineDropMin - subScript!.ascent); + opsDisplay.lowerLimitGap = lowerLimitGap; + } + opsDisplay.position = currentPosition; + opsDisplay.range = op.indexRange; + currentPosition.x += opsDisplay.width; + return opsDisplay; + } else { + currentPosition.x += display!.width; + self.makeScripts(op, display:display, index:safeUIntFromLocation(op.indexRange.location), delta:delta) + return display; + } + } + + // MARK: - Large delimiters + + // Delimiter shortfall from plain.tex + static let kDelimiterFactor = CGFloat(901) + static let kDelimiterShortfallPoints = CGFloat(5) + + func makeLeftRight(_ inner: MTInner?, maxWidth: CGFloat = 0) -> MTDisplay? { + assert(inner!.leftBoundary != nil || inner!.rightBoundary != nil, "Inner should have a boundary to call this function"); + + let glyphHeight: CGFloat + + // Check if we have an explicit delimiter height (from \big, \Big, etc.) + if let delimiterMultiplier = inner!.delimiterHeight { + // delimiterHeight is a multiplier (e.g., 1.2, 1.8, 2.4, 3.0) + // Multiply by font size to get actual height + glyphHeight = styleFont.fontSize * delimiterMultiplier + } else { + // Calculate height based on inner content (for \left...\right) + let innerListDisplay = MTTypesetter.createLineForMathList(inner!.innerList, font:font, style:style, cramped:cramped, spaced:true, maxWidth:maxWidth) + let axisHeight = styleFont.mathTable!.axisHeight + // delta is the max distance from the axis + let delta = max(innerListDisplay!.ascent - axisHeight, innerListDisplay!.descent + axisHeight); + let d1 = (delta / 500) * MTTypesetter.kDelimiterFactor; // This represents atleast 90% of the formula + let d2 = 2 * delta - MTTypesetter.kDelimiterShortfallPoints; // This represents a shortfall of 5pt + // The size of the delimiter glyph should cover at least 90% of the formula or + // be at most 5pt short. + glyphHeight = max(d1, d2); + } + + var innerElements = [MTDisplay]() + var position = CGPoint.zero + + // Add horizontal padding between delimiters and content + // Use 2 mu (about 1/9 em) for breathing room, matching TeX standards + let delimiterPadding = styleFont.mathTable!.muUnit * 2 + + if inner!.leftBoundary != nil && !inner!.leftBoundary!.nucleus.isEmpty { + let leftGlyph = self.findGlyphForBoundary(inner!.leftBoundary!.nucleus, withHeight:glyphHeight) + leftGlyph!.position = position + position.x += leftGlyph!.width + innerElements.append(leftGlyph!) + // Add padding after left delimiter + position.x += delimiterPadding + } + + // Only include inner content if not using explicit delimiter height + // (explicit height commands like \big produce standalone delimiters) + if inner!.delimiterHeight == nil { + let innerListDisplay = MTTypesetter.createLineForMathList(inner!.innerList, font:font, style:style, cramped:cramped, spaced:true, maxWidth:maxWidth) + innerListDisplay!.position = position; + position.x += innerListDisplay!.width; + innerElements.append(innerListDisplay!) + } + + if inner!.rightBoundary != nil && !inner!.rightBoundary!.nucleus.isEmpty { + // Add padding before right delimiter + position.x += delimiterPadding + let rightGlyph = self.findGlyphForBoundary(inner!.rightBoundary!.nucleus, withHeight:glyphHeight) + rightGlyph!.position = position; + position.x += rightGlyph!.width; + innerElements.append(rightGlyph!) + } + let innerDisplay = MTMathListDisplay(withDisplays: innerElements, range: inner!.indexRange) + return innerDisplay + } + + func findGlyphForBoundary(_ delimiter:String, withHeight glyphHeight:CGFloat) -> MTDisplay? { + var glyphAscent=CGFloat(0), glyphDescent=CGFloat(0), glyphWidth=CGFloat(0) + let leftGlyph = self.findGlyphForCharacterAtIndex(delimiter.startIndex, inString:delimiter) + let glyph = self.findGlyph(leftGlyph, withHeight:glyphHeight, glyphAscent:&glyphAscent, glyphDescent:&glyphDescent, glyphWidth:&glyphWidth) + + var glyphDisplay:MTDisplayDS? + if (glyphAscent + glyphDescent < glyphHeight) { + // we didn't find a pre-built glyph that is large enough + glyphDisplay = self.constructGlyph(leftGlyph, withHeight:glyphHeight) + } + + if glyphDisplay == nil { + // Create a glyph display + glyphDisplay = MTGlyphDisplay(withGlpyh: glyph, range: NSMakeRange(NSNotFound, 0), font:styleFont) + glyphDisplay!.ascent = glyphAscent; + glyphDisplay!.descent = glyphDescent; + glyphDisplay!.width = glyphWidth; + } + // Center the glyph on the axis + let shiftDown = 0.5*(glyphDisplay!.ascent - glyphDisplay!.descent) - styleFont.mathTable!.axisHeight; + glyphDisplay!.shiftDown = shiftDown; + return glyphDisplay; + } + + // MARK: - Underline/Overline + + func makeUnderline(_ under:MTUnderLine?) -> MTDisplay? { + let innerListDisplay = MTTypesetter.createLineForMathList(under!.innerList, font:font, style:style, cramped:cramped) + let underDisplay = MTLineDisplay(withInner: innerListDisplay, position: currentPosition, range: under!.indexRange) + // Move the line down by the vertical gap. + underDisplay.lineShiftUp = -(innerListDisplay!.descent + styleFont.mathTable!.underbarVerticalGap); + underDisplay.lineThickness = styleFont.mathTable!.underbarRuleThickness; + underDisplay.ascent = innerListDisplay!.ascent + underDisplay.descent = innerListDisplay!.descent + styleFont.mathTable!.underbarVerticalGap + styleFont.mathTable!.underbarRuleThickness + styleFont.mathTable!.underbarExtraDescender; + underDisplay.width = innerListDisplay!.width; + return underDisplay; + } + + func makeOverline(_ over:MTOverLine?) -> MTDisplay? { + let innerListDisplay = MTTypesetter.createLineForMathList(over!.innerList, font:font, style:style, cramped:true) + let overDisplay = MTLineDisplay(withInner:innerListDisplay, position:currentPosition, range:over!.indexRange) + overDisplay.lineShiftUp = innerListDisplay!.ascent + styleFont.mathTable!.overbarVerticalGap; + overDisplay.lineThickness = styleFont.mathTable!.underbarRuleThickness; + overDisplay.ascent = innerListDisplay!.ascent + styleFont.mathTable!.overbarVerticalGap + styleFont.mathTable!.overbarRuleThickness + styleFont.mathTable!.overbarExtraAscender; + overDisplay.descent = innerListDisplay!.descent; + overDisplay.width = innerListDisplay!.width; + return overDisplay; + } + + // MARK: - Accents + + func isSingleCharAccentee(_ accent:MTAccent?) -> Bool { + guard let accent = accent else { return false } + if accent.innerList!.atoms.count != 1 { + // Not a single char list. + return false + } + let innerAtom = accent.innerList!.atoms[0] + if innerAtom.nucleus.count != 1 { + // A complex atom, not a simple char. + return false + } + if innerAtom.subScript != nil || innerAtom.superScript != nil { + return false + } + return true + } + + // The distance the accent must be moved from the beginning. + func getSkew(_ accent: MTAccent?, accenteeWidth width:CGFloat, accentGlyph:CGGlyph) -> CGFloat { + guard let accent = accent else { return 0 } + if accent.nucleus.isEmpty { + // No accent + return 0 + } + let accentAdjustment = styleFont.mathTable!.getTopAccentAdjustment(accentGlyph) + var accenteeAdjustment = CGFloat(0) + if !self.isSingleCharAccentee(accent) { + // use the center of the accentee + accenteeAdjustment = width/2 + } else { + let innerAtom = accent.innerList!.atoms[0] + let accenteeGlyph = self.findGlyphForCharacterAtIndex(innerAtom.nucleus.index(innerAtom.nucleus.endIndex, offsetBy:-1), inString:innerAtom.nucleus) + accenteeAdjustment = styleFont.mathTable!.getTopAccentAdjustment(accenteeGlyph) + } + // The adjustments need to aligned, so skew is just the difference. + return (accenteeAdjustment - accentAdjustment) + } + + // Find the largest horizontal variant if exists, with width less than max width. + func findVariantGlyph(_ glyph:CGGlyph, withMaxWidth maxWidth:CGFloat, maxWidth glyphAscent:inout CGFloat, glyphDescent:inout CGFloat, glyphWidth:inout CGFloat, glyphMinY:inout CGFloat) -> CGGlyph { + let variants = styleFont.mathTable!.getHorizontalVariantsForGlyph(glyph) + let numVariants = variants.count + assert(numVariants > 0, "A glyph is always it's own variant, so number of variants should be > 0"); + var glyphs = [CGGlyph]() // [numVariants) + glyphs.reserveCapacity(numVariants) + for i in 0 ..< numVariants { + let glyph = variants[i]!.uint16Value + glyphs.append(glyph) + } + + var curGlyph = glyphs[0] // if no other glyph is found, we'll return the first one. + var bboxes = [CGRect](repeating: CGRect.zero, count: numVariants) // [numVariants) + var advances = [CGSize](repeating: CGSize.zero, count:numVariants) + // Get the bounds for these glyphs + CTFontGetBoundingRectsForGlyphs(styleFont.ctFont, .horizontal, &glyphs, &bboxes, numVariants); + CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, &glyphs, &advances, numVariants); + for i in 0.. maxWidth) { + if (i == 0) { + // glyph dimensions are not yet set + glyphWidth = advances[i].width; + glyphAscent = ascent; + glyphDescent = descent; + glyphMinY = bounds.minY; + } + return curGlyph; + } else { + curGlyph = glyphs[i] + glyphWidth = advances[i].width; + glyphAscent = ascent; + glyphDescent = descent; + glyphMinY = bounds.minY; + } + } + // We exhausted all the variants and none was larger than the width, so we return the largest + return curGlyph; + } + + /// Gets the proper glyph name for arrow accents that have stretchy variants in the font. + /// Returns different glyphs based on the LaTeX command used: + /// - \vec: use combining character glyph (uni20D7) for small fixed-size arrow + /// - \overrightarrow: use non-combining arrow (arrowright) which can be stretched + func getArrowAccentGlyphName(_ accent: MTAccent) -> String? { + // Check if this is a stretchy arrow accent (set by the factory based on LaTeX command) + let useStretchy = accent.isStretchy + + // Map Unicode combining characters to appropriate glyph names + switch accent.nucleus { + case "\u{20D6}": // Combining left arrow above + return useStretchy ? "arrowleft" : "uni20D6" + case "\u{20D7}": // Combining right arrow above (\vec or \overrightarrow) + return useStretchy ? "arrowright" : "uni20D7" + case "\u{20E1}": // Combining left right arrow above + return useStretchy ? "arrowboth" : "uni20E1" + default: + return nil + } + } + + /// Gets the proper glyph name for wide accents that should stretch to cover content. + /// Returns different glyphs based on the LaTeX command used: + /// - \hat: use combining character for fixed-size accent + /// - \widehat: use non-combining circumflex which can be stretched + func getWideAccentGlyphName(_ accent: MTAccent) -> String? { + // Only apply to wide accents (set by factory based on LaTeX command) + guard accent.isWide else { return nil } + + // Map Unicode combining characters to non-combining glyph names with stretchy variants + switch accent.nucleus { + case "\u{0302}": // COMBINING CIRCUMFLEX ACCENT (\hat or \widehat) + return "circumflex" + case "\u{0303}": // COMBINING TILDE (\tilde or \widetilde) + return "tilde" + case "\u{030C}": // COMBINING CARON (\check or \widecheck) + return "caron" + default: + return nil + } + } + + /// Counts the approximate character length of the content under a wide accent. + /// This is used to select the appropriate glyph variant. + func getWideAccentContentLength(_ accent: MTAccent) -> Int { + guard let innerList = accent.innerList else { return 0 } + + var charCount = 0 + for atom in innerList.atoms { + switch atom.type { + case .variable, .number: + // Count actual characters + charCount += atom.nucleus.count + case .ordinary, .binaryOperator, .relation: + // Count as single character + charCount += 1 + case .fraction: + // Fractions count as 2 units + charCount += 2 + case .radical: + // Radicals count as 2 units + charCount += 2 + case .largeOperator: + // Large operators count as 2 units + charCount += 2 + default: + // Other types count as 1 unit + charCount += 1 + } + } + return charCount + } + + /// Determines which glyph variant to use for a wide accent based on content length. + /// Returns a multiplier for the requested width (1.0, 1.5, 2.0, or 2.5) + /// Selects variants based on character count to ensure proper coverage. + func getWideAccentVariantMultiplier(_ accent: MTAccent) -> CGFloat { + let charCount = getWideAccentContentLength(accent) + + // Map character count to variant width request multiplier + // This helps select larger glyph variants from the font's MATH table + // 1-2 chars: request 1.0x (smallest variant) + // 3-4 chars: request 1.5x (medium variant) + // 5-6 chars: request 2.0x (large variant) + // 7+ chars: request 2.5x (largest variant) + if charCount <= 2 { + return 1.0 + } else if charCount <= 4 { + return 1.5 + } else if charCount <= 6 { + return 2.0 + } else { + return 2.5 + } + } + + func makeAccent(_ accent:MTAccent?) -> MTDisplay? { + guard let accent = accent else { return nil } + + let accentee = MTTypesetter.createLineForMathList(accent.innerList, font:font, style:style, cramped:true) + if accent.nucleus.isEmpty { + // no accent! + return accentee + } + + // If accentee is nil (empty content), create an empty display + guard let accentee = accentee else { + // Return an empty display for empty content + let emptyDisplay = MTMathListDisplay(withDisplays: [], range: accent.indexRange) + emptyDisplay.position = currentPosition + return emptyDisplay + } + + var accentGlyph: CGGlyph + let isArrowAccent = getArrowAccentGlyphName(accent) != nil + let isWideAccent = getWideAccentGlyphName(accent) != nil + + // Check for special accent types that need non-combining glyphs + if let wideGlyphName = getWideAccentGlyphName(accent) { + // For wide accents, use non-combining glyphs (e.g., "circumflex", "tilde") + // These have horizontal variants that can stretch + accentGlyph = styleFont.get(glyphWithName: wideGlyphName) + } else if let arrowGlyphName = getArrowAccentGlyphName(accent) { + // For arrow accents, use non-combining arrow glyphs (e.g., "arrowright") + // These have larger horizontal variants than the combining versions + accentGlyph = styleFont.get(glyphWithName: arrowGlyphName) + } else { + // For regular accents, use Unicode character lookup + let end = accent.nucleus.index(before: accent.nucleus.endIndex) + accentGlyph = self.findGlyphForCharacterAtIndex(end, inString:accent.nucleus) + } + + let accenteeWidth = accentee.width; + var glyphAscent=CGFloat(0), glyphDescent=CGFloat(0), glyphWidth=CGFloat(0), glyphMinY=CGFloat(0) + + // Adjust requested width based on accent type: + // - Wide accents (\widehat): request width based on content length (variant selection) + // - Arrow accents (\overrightarrow): request extra width for stretching + // - Regular accents: request exact content width + let requestedWidth: CGFloat + if isWideAccent { + // For wide accents, request width based on content length to select appropriate variant + let multiplier = getWideAccentVariantMultiplier(accent) + requestedWidth = accenteeWidth * multiplier + } else if isArrowAccent { + if accent.isStretchy { + requestedWidth = accenteeWidth * 1.1 // Request extra width for stretching + } else { + requestedWidth = 1.0 // Get smallest non-zero variant (typically .h1) + } + } else { + requestedWidth = accenteeWidth + } + + accentGlyph = self.findVariantGlyph(accentGlyph, withMaxWidth:requestedWidth, maxWidth:&glyphAscent, glyphDescent:&glyphDescent, glyphWidth:&glyphWidth, glyphMinY:&glyphMinY) + + // For non-stretchy arrow accents (\vec): if we got a zero-width glyph (base combining char), + // manually select the first variant which is the proper accent size + if isArrowAccent && !accent.isStretchy && glyphWidth == 0 { + guard let mathTable = styleFont.mathTable else { return nil } + let variants = mathTable.getHorizontalVariantsForGlyph(accentGlyph) + if variants.count > 1, let variantNum = variants[1] { + // Use the first variant (.h1) which has proper width + accentGlyph = CGGlyph(variantNum.uint16Value) + var glyph = accentGlyph + var advances = CGSize.zero + CTFontGetAdvancesForGlyphs(styleFont.ctFont, .horizontal, &glyph, &advances, 1) + glyphWidth = advances.width + // Recalculate ascent and descent for the variant glyph + var boundingRects = CGRect.zero + CTFontGetBoundingRectsForGlyphs(styleFont.ctFont, .horizontal, &glyph, &boundingRects, 1) + glyphMinY = boundingRects.minY + glyphAscent = boundingRects.maxY + glyphDescent = -boundingRects.minY + } + } + + // Special accents (arrows and wide accents) need more vertical space and different positioning + let delta: CGFloat + let height: CGFloat + let skew: CGFloat + + if isWideAccent { + // Wide accents (\widehat, \widetilde): use same vertical spacing as stretchy arrows + delta = 0 // No compression for wide accents + guard let mathTable = styleFont.mathTable else { return nil } + let wideAccentSpacing = mathTable.upperLimitGapMin // Same as stretchy arrows + // Compensate for internal glyph whitespace (minY > 0) + let minYCompensation = max(0, glyphMinY) + height = accentee.ascent + wideAccentSpacing - minYCompensation + + // For wide accents: if the largest glyph variant is still smaller than content width, + // scale it horizontally to fully cover the content + if glyphWidth < accenteeWidth { + // Add padding to make accent extend slightly beyond content + // Use ~0.1em padding (less than arrows which use ~0.167em) + let widePadding = styleFont.fontSize / 10 // Approximately 0.1em + let targetWidth = accenteeWidth + widePadding + + let scaleX = targetWidth / glyphWidth + let accentGlyphDisplay = MTGlyphDisplay(withGlpyh: accentGlyph, range: accent.indexRange, font: styleFont) + accentGlyphDisplay.scaleX = scaleX // Apply horizontal scaling + accentGlyphDisplay.ascent = glyphAscent + accentGlyphDisplay.descent = glyphDescent + accentGlyphDisplay.width = targetWidth // Set width to include padding + accentGlyphDisplay.position = CGPointMake(0, height) // Align to left edge + + var finalAccentee = accentee + if self.isSingleCharAccentee(accent) && (accent.subScript != nil || accent.superScript != nil) { + // Attach the super/subscripts to the accentee instead of the accent. + guard let innerList = accent.innerList, + !innerList.atoms.isEmpty else { return nil } + let innerAtom = innerList.atoms[0] + innerAtom.superScript = accent.superScript + innerAtom.subScript = accent.subScript + accent.superScript = nil + accent.subScript = nil + if let remadeAccentee = MTTypesetter.createLineForMathList(accent.innerList, font:font, style:style, cramped:cramped) { + finalAccentee = remadeAccentee + } + } + + let display = MTAccentDisplay(withAccent:accentGlyphDisplay, accentee:finalAccentee, range:accent.indexRange) + display.width = finalAccentee.width + display.descent = finalAccentee.descent + let ascent = height + glyphAscent + display.ascent = max(finalAccentee.ascent, ascent) + display.position = currentPosition + return display + } else { + // Wide accent glyph is wide enough: center it over the content + skew = (accenteeWidth - glyphWidth) / 2 + } + } else if isArrowAccent { + // Arrow accents spacing depends on whether they're stretchy or not + guard let mathTable = styleFont.mathTable else { return nil } + if accent.isStretchy { + // Stretchy arrows (\overrightarrow): use full ascent + additional spacing + delta = 0 // No compression for stretchy arrows + let arrowSpacing = mathTable.upperLimitGapMin // Use standard gap + // Compensate for internal glyph whitespace (minY > 0) + let minYCompensation = max(0, glyphMinY) + height = accentee.ascent + arrowSpacing - minYCompensation + } else { + // Non-stretchy arrows (\vec): use tight spacing like regular accents + // This gives a more compact appearance suitable for single-character vectors + delta = min(accentee.ascent, mathTable.accentBaseHeight) + // Use same formula as regular accents (no minYCompensation adjustment) + // This places the arrow properly above the character + height = accentee.ascent - delta + } + + // For stretchy arrow accents (\overrightarrow): if the largest glyph variant is still smaller than content width, + // scale it horizontally to fully cover the content + // Add small padding to make arrow tip extend slightly beyond content + // For non-stretchy accents (\vec): always center without scaling + if accent.isStretchy && glyphWidth < accenteeWidth { + // Add padding to make arrow extend beyond content on the tip side + // Use approximately 0.15-0.2em extra width + let arrowPadding = styleFont.fontSize / 6 // Approximately 0.167em at typical font sizes + let targetWidth = accenteeWidth + arrowPadding + + let scaleX = targetWidth / glyphWidth + let accentGlyphDisplay = MTGlyphDisplay(withGlpyh: accentGlyph, range: accent.indexRange, font: styleFont) + accentGlyphDisplay.scaleX = scaleX // Apply horizontal scaling + accentGlyphDisplay.ascent = glyphAscent + accentGlyphDisplay.descent = glyphDescent + accentGlyphDisplay.width = targetWidth // Set width to include padding + accentGlyphDisplay.position = CGPointMake(0, height) // Align to left edge + + var finalAccentee = accentee + if self.isSingleCharAccentee(accent) && (accent.subScript != nil || accent.superScript != nil) { + // Attach the super/subscripts to the accentee instead of the accent. + guard let innerList = accent.innerList, + !innerList.atoms.isEmpty else { return nil } + let innerAtom = innerList.atoms[0] + innerAtom.superScript = accent.superScript + innerAtom.subScript = accent.subScript + accent.superScript = nil + accent.subScript = nil + if let remadeAccentee = MTTypesetter.createLineForMathList(accent.innerList, font:font, style:style, cramped:cramped) { + finalAccentee = remadeAccentee + } + } + + let display = MTAccentDisplay(withAccent:accentGlyphDisplay, accentee:finalAccentee, range:accent.indexRange) + display.width = finalAccentee.width + display.descent = finalAccentee.descent + let ascent = height + glyphAscent + display.ascent = max(finalAccentee.ascent, ascent) + display.position = currentPosition + return display + } else { + // Arrow glyph is wide enough or is non-stretchy (\vec): center it over the content + skew = (accenteeWidth - glyphWidth) / 2 + } + } else { + // For regular accents: use traditional tight positioning + guard let mathTable = styleFont.mathTable else { return nil } + delta = min(accentee.ascent, mathTable.accentBaseHeight) + skew = self.getSkew(accent, accenteeWidth:accenteeWidth, accentGlyph:accentGlyph) + height = accentee.ascent - delta // This is always positive since delta <= height. + } + + let accentPosition = CGPointMake(skew, height); + let accentGlyphDisplay = MTGlyphDisplay(withGlpyh: accentGlyph, range: accent.indexRange, font: styleFont) + accentGlyphDisplay.ascent = glyphAscent; + accentGlyphDisplay.descent = glyphDescent; + accentGlyphDisplay.width = glyphWidth; + accentGlyphDisplay.position = accentPosition; + + var finalAccentee = accentee + if self.isSingleCharAccentee(accent) && (accent.subScript != nil || accent.superScript != nil) { + // Attach the super/subscripts to the accentee instead of the accent. + guard let innerList = accent.innerList, + !innerList.atoms.isEmpty else { return nil } + let innerAtom = innerList.atoms[0] + innerAtom.superScript = accent.superScript; + innerAtom.subScript = accent.subScript; + accent.superScript = nil; + accent.subScript = nil; + // Remake the accentee (now with sub/superscripts) + // Note: Latex adjusts the heights in case the height of the char is different in non-cramped mode. However this shouldn't be the case since cramping + // only affects fractions and superscripts. We skip adjusting the heights. + if let remadeAccentee = MTTypesetter.createLineForMathList(accent.innerList, font:font, style:style, cramped:cramped) { + finalAccentee = remadeAccentee + } + } + + let display = MTAccentDisplay(withAccent:accentGlyphDisplay, accentee:finalAccentee, range:accent.indexRange) + display.width = finalAccentee.width; + display.descent = finalAccentee.descent; + + // Calculate total ascent based on positioning + // For arrows: height already includes spacing, so ascent = height + glyphAscent + // For regular accents: ascent = accentee.ascent - delta + glyphAscent (existing formula) + let ascent = height + glyphAscent; + display.ascent = max(finalAccentee.ascent, ascent); + display.position = currentPosition; + + return display; + } + + /// Determines if an accent can use Unicode composition for inline rendering. + /// Unicode combining characters only work correctly for single base characters. + /// Multi-character expressions and arrow accents need font-based rendering. + func canUseUnicodeComposition(_ accent: MTAccent) -> Bool { + // Check if innerList has exactly one simple character + guard let innerList = accent.innerList, + innerList.atoms.count == 1, + let firstAtom = innerList.atoms.first else { + return false + } + + // Only allow simple variable/number atoms + guard firstAtom.type == .variable || firstAtom.type == .number else { + return false + } + + // Check that the atom doesn't have subscripts/superscripts + guard firstAtom.subScript == nil && firstAtom.superScript == nil else { + return false + } + + // Exclude arrow accents - they need stretching from font glyphs + // These Unicode combining characters only apply to single preceding characters + let arrowAccents: Set = [ + "\u{20D6}", // overleftarrow + "\u{20D7}", // overrightarrow / vec + "\u{20E1}" // overleftrightarrow + ] + + if arrowAccents.contains(accent.nucleus) { + return false + } + + return true + } + + // MARK: - Table + + let kBaseLineSkipMultiplier = CGFloat(1.2) // default base line stretch is 12 pt for 10pt font. + let kLineSkipMultiplier = CGFloat(0.1) // default is 1pt for 10pt font. + let kLineSkipLimitMultiplier = CGFloat(0) + let kJotMultiplier = CGFloat(0.3) // A jot is 3pt for a 10pt font. + + func makeTable(_ table:MTMathTable?) -> MTDisplay? { + let numColumns = table!.numColumns; + if numColumns == 0 || table!.numRows == 0 { + // Empty table + return MTMathListDisplay(withDisplays: [MTDisplay](), range: table!.indexRange) + } + + var columnWidths = [CGFloat](repeating: 0, count: numColumns) + let displays = self.typesetCells(table, columnWidths:&columnWidths) + + // Position all the columns in each row + var rowDisplays = [MTDisplay]() + for row in displays { + if let rowDisplay = self.makeRowWithColumns(row, forTable:table, columnWidths:columnWidths) { + rowDisplays.append(rowDisplay) + } + } + + // Position all the rows + self.positionRows(rowDisplays, forTable:table) + let tableDisplay = MTMathListDisplay(withDisplays: rowDisplays, range: table!.indexRange) + tableDisplay.position = currentPosition; + return tableDisplay; + } + + // Typeset every cell in the table. As a side-effect calculate the max column width of each column. + func typesetCells(_ table:MTMathTable?, columnWidths: inout [CGFloat]) -> [[MTDisplay]] { + var displays = [[MTDisplay]]() + for row in table!.cells { + var colDisplays = [MTDisplay]() + for i in 0.. MTMathListDisplay? { + var columnStart = CGFloat(0) + var rowRange = NSMakeRange(NSNotFound, 0); + for i in 0.. 0 { + if rowRange.location != NSNotFound { + rowRange = NSUnionRange(rowRange, col.range); + } else { + rowRange = col.range; + } + } + // If col.range is invalid or has zero length, skip it - don't update rowRange + + col.position = CGPointMake(cellPos, 0); + columnStart += colWidth + table!.interColumnSpacing * styleFont.mathTable!.muUnit; + } + + // If no valid ranges were found (all cells had zero-length ranges), use a synthetic range + // This represents the row conceptually spanning all its columns + if rowRange.location == NSNotFound { + rowRange = NSMakeRange(0, cols.count); + } + + // Create a display for the row + let rowDisplay = MTMathListDisplay(withDisplays: cols, range:rowRange) + return rowDisplay + } + + func positionRows(_ rows:[MTDisplay], forTable table:MTMathTable?) { + // Position the rows + // We will first position the rows starting from 0 and then in the second pass center the whole table vertically. + var currPos = CGFloat(0) + let openup = table!.interRowAdditionalSpacing * kJotMultiplier * styleFont.fontSize; + let baselineSkip = openup + kBaseLineSkipMultiplier * styleFont.fontSize; + let lineSkip = openup + kLineSkipMultiplier * styleFont.fontSize; + let lineSkipLimit = openup + kLineSkipLimitMultiplier * styleFont.fontSize; + var prevRowDescent = CGFloat(0) + var ascent = CGFloat(0) + var first = true + for row in rows { + if first { + row.position = CGPointZero; + ascent += row.ascent; + first = false; + } else { + var skip = baselineSkip; + if (skip - (prevRowDescent + row.ascent) < lineSkipLimit) { + // rows are too close to each other. Space them apart further + skip = prevRowDescent + row.ascent + lineSkip; + } + // We are going down so we decrease the y value. + currPos -= skip; + row.position = CGPointMake(0, currPos); + } + prevRowDescent = row.descent; + } + + // Vertically center the whole structure around the axis + // The descent of the structure is the position of the last row + // plus the descent of the last row. + let descent = -currPos + prevRowDescent; + let shiftDown = 0.5*(ascent - descent) - styleFont.mathTable!.axisHeight; + + for row in rows { + row.position = CGPointMake(row.position.x, row.position.y - shiftDown); + } + } +} diff --git a/third-party/SwiftMath/Sources/MathRender/MTUnicode.swift b/third-party/SwiftMath/Sources/MathRender/MTUnicode.swift new file mode 100755 index 0000000000..a1fc48d41c --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/MTUnicode.swift @@ -0,0 +1,89 @@ +// +// Created by Mike Griebling on 2022-12-31. +// Translated from an Objective-C implementation by Kostub Deshmukh. +// +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation + +public struct UnicodeSymbol { + static let multiplication = "\u{00D7}" + static let division = "\u{00F7}" + static let fractionSlash = "\u{2044}" + static let whiteSquare = "\u{25A1}" + static let blackSquare = "\u{25A0}" + static let lessEqual = "\u{2264}" + static let greaterEqual = "\u{2265}" + static let notEqual = "\u{2260}" + static let squareRoot = "\u{221A}" // \sqrt + static let cubeRoot = "\u{221B}" + static let infinity = "\u{221E}" // \infty + static let angle = "\u{2220}" // \angle + static let degree = "\u{00B0}" // \circ + + static let capitalGreekStart = UInt32(0x0391) + static let capitalGreekEnd = UInt32(0x03A9) + static let lowerGreekStart = UInt32(0x03B1) + static let lowerGreekEnd = UInt32(0x03C9) + static let planksConstant = UInt32(0x210e) + static let lowerItalicStart = UInt32(0x1D44E) + static let capitalItalicStart = UInt32(0x1D434) + static let greekLowerItalicStart = UInt32(0x1D6FC) + static let greekCapitalItalicStart = UInt32(0x1D6E2) + static let greekSymbolItalicStart = UInt32(0x1D716) + + static let mathCapitalBoldStart = UInt32(0x1D400) + static let mathLowerBoldStart = UInt32(0x1D41A) + static let greekCapitalBoldStart = UInt32(0x1D6A8) + static let greekLowerBoldStart = UInt32(0x1D6C2) + static let greekSymbolBoldStart = UInt32(0x1D6DC) + static let numberBoldStart = UInt32(0x1D7CE) + + static let mathCapitalBoldItalicStart = UInt32(0x1D468) + static let mathLowerBoldItalicStart = UInt32(0x1D482) + static let greekCapitalBoldItalicStart = UInt32(0x1D71C) + static let greekLowerBoldItalicStart = UInt32(0x1D736) + static let greekSymbolBoldItalicStart = UInt32(0x1D750) + + static let mathCapitalScriptStart = UInt32(0x1D49C) + static let mathCapitalTTStart = UInt32(0x1D670) + static let mathLowerTTStart = UInt32(0x1D68A) + static let numberTTStart = UInt32(0x1D7F6) + static let mathCapitalSansSerifStart = UInt32(0x1D5A0) + static let mathLowerSansSerifStart = UInt32(0x1D5BA) + static let numberSansSerifStart = UInt32(0x1D7E2) + static let mathCapitalFrakturStart = UInt32(0x1D504) + static let mathLowerFrakturStart = UInt32(0x1D51E) + static let mathCapitalBlackboardStart = UInt32(0x1D538) + static let mathLowerBlackboardStart = UInt32(0x1D552) + static let numberBlackboardStart = UInt32(0x1D7D8) +} + +extension Character { + + var utf32Char: UTF32Char { self.unicodeScalars.map { $0.value }.reduce(0, +) } + var isLowerEnglish : Bool { self >= "a" && self <= "z" } + var isUpperEnglish : Bool { self >= "A" && self <= "Z" } + var isNumber : Bool { self >= "0" && self <= "9" } + + var isLowerGreek : Bool { + let uch = self.utf32Char + return uch >= UnicodeSymbol.lowerGreekStart && uch <= UnicodeSymbol.lowerGreekEnd + } + + var isCapitalGreek : Bool { + let uch = self.utf32Char + return uch >= UnicodeSymbol.capitalGreekStart && uch <= UnicodeSymbol.capitalGreekEnd + } + + var greekSymbolOrder : UInt32? { + let greekSymbols : [UTF32Char] = [0x03F5, 0x03D1, 0x03F0, 0x03D5, 0x03F1, 0x03D6] + let index = greekSymbols.firstIndex(of: self.utf32Char) + if let pos = index { return UInt32(pos) } + return nil + } + + var isGreekSymbol : Bool { self.greekSymbolOrder != nil } +} diff --git a/third-party/SwiftMath/Sources/MathRender/RWLock.swift b/third-party/SwiftMath/Sources/MathRender/RWLock.swift new file mode 100644 index 0000000000..ccca3976e0 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/RWLock.swift @@ -0,0 +1,58 @@ +import Foundation + +final class RWLock { + init() { + pthread_rwlock_init(&lock, nil) + } + + deinit { + pthread_rwlock_destroy(&lock) + } + + func read(_ block: () -> T) -> T { + pthread_rwlock_rdlock(&lock) + defer { pthread_rwlock_unlock(&lock) } + return block() + } + + func readWrite(_ block: () -> T) -> T { + pthread_rwlock_wrlock(&lock) + defer { pthread_rwlock_unlock(&lock) } + return block() + } + + private var lock = pthread_rwlock_t() +} + +@propertyWrapper +struct RWLocked { + init(wrappedValue: T) { + value = wrappedValue + } + + var wrappedValue: T { + get { + lock.read { + value + } + } + set { + lock.readWrite { + value = newValue + } + } + } + + @discardableResult + mutating func readWrite(_ block: (inout T) -> Void) -> (oldValue: T, newValue: T) { + lock.readWrite { + let old = value + block(&value) + return (old, value) + } + } + + private var value: T + private let lock = RWLock() +} + diff --git a/third-party/SwiftMath/Sources/MathRender/Tokenization/MTAtomTokenizer.swift b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTAtomTokenizer.swift new file mode 100644 index 0000000000..2a76a661a4 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTAtomTokenizer.swift @@ -0,0 +1,1145 @@ +// +// MTAtomTokenizer.swift +// SwiftMath +// +// Created by Claude Code on 2025-12-16. +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation +import CoreGraphics + +/// Tokenizes MTMathAtom lists into breakable elements +class MTAtomTokenizer { + + // MARK: - Properties + + let font: MTFont + let style: MTLineStyle + let cramped: Bool + let maxWidth: CGFloat + let widthCalculator: MTElementWidthCalculator + let displayRenderer: MTDisplayPreRenderer + + // MARK: - Initialization + + init(font: MTFont, style: MTLineStyle, cramped: Bool = false, maxWidth: CGFloat = 0) { + self.font = font + self.style = style + self.cramped = cramped + self.maxWidth = maxWidth + self.widthCalculator = MTElementWidthCalculator(font: font, style: style) + self.displayRenderer = MTDisplayPreRenderer(font: font, style: style, cramped: cramped) + } + + // MARK: - Main Tokenization + + /// Tokenize a list of atoms into breakable elements + func tokenize(_ atoms: [MTMathAtom]) -> [MTBreakableElement] { + var elements: [MTBreakableElement] = [] + var index = 0 + var currentStyle = self.style + + while index < atoms.count { + let atom = atoms[index] + let prevAtom = index > 0 ? atoms[index - 1] : nil + + // Check for style change atoms + if atom.type == .style, let styleAtom = atom as? MTMathStyle { + // Update style for subsequent atoms + currentStyle = styleAtom.style + index += 1 + continue + } + + // Create a tokenizer with the current style for this atom + let atomTokenizer: MTAtomTokenizer + if currentStyle != self.style { + atomTokenizer = MTAtomTokenizer(font: font, style: currentStyle, cramped: cramped, maxWidth: maxWidth) + } else { + atomTokenizer = self + } + + // Handle scripts (subscript/superscript) - these must be grouped with their base + if atom.superScript != nil || atom.subScript != nil { + let baseElements = atomTokenizer.tokenizeAtomWithScripts(atom, prevAtom: prevAtom, atomIndex: index, allAtoms: atoms) + elements.append(contentsOf: baseElements) + } else { + // Check if this is a multi-character text atom that needs character-level tokenization + let isTextAtom = atom.fontStyle == .roman + let isMultiChar = atom.nucleus.count > 1 + + if isTextAtom && isMultiChar { + // Break down multi-character text into individual characters for punctuation rules + let charElements = atomTokenizer.tokenizeMultiCharText(atom, prevElements: elements, atomIndex: index, allAtoms: atoms) + elements.append(contentsOf: charElements) + } else { + // Regular atom without scripts + if let element = atomTokenizer.tokenizeAtom(atom, prevAtom: prevAtom, atomIndex: index, allAtoms: atoms) { + elements.append(element) + } + } + } + + index += 1 + } + + return elements + } + + // MARK: - Atom Tokenization + + /// Tokenize a single atom (without scripts) + private func tokenizeAtom(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int, allAtoms: [MTMathAtom]) -> MTBreakableElement? { + switch atom.type { + // Simple text and variables + case .ordinary, .variable, .number: + return tokenizeTextAtom(atom, prevAtom: prevAtom, atomIndex: atomIndex, allAtoms: allAtoms) + + // Operators + case .binaryOperator, .relation, .unaryOperator: + return tokenizeOperator(atom, prevAtom: prevAtom, atomIndex: atomIndex) + + // Delimiters + case .open: + return tokenizeOpenDelimiter(atom, prevAtom: prevAtom, atomIndex: atomIndex) + + case .close: + return tokenizeCloseDelimiter(atom, prevAtom: prevAtom, atomIndex: atomIndex) + + // Punctuation + case .punctuation: + return tokenizePunctuation(atom, prevAtom: prevAtom, atomIndex: atomIndex) + + // Complex structures (atomic) + case .fraction: + return tokenizeFraction(atom as! MTFraction, prevAtom: prevAtom, atomIndex: atomIndex) + + case .radical: + return tokenizeRadical(atom as! MTRadical, prevAtom: prevAtom, atomIndex: atomIndex) + + case .largeOperator: + return tokenizeLargeOperator(atom as! MTLargeOperator, prevAtom: prevAtom, atomIndex: atomIndex) + + case .accent: + return tokenizeAccent(atom as! MTAccent, prevAtom: prevAtom, atomIndex: atomIndex, allAtoms: allAtoms) + + case .underline: + return tokenizeUnderline(atom as! MTUnderLine, prevAtom: prevAtom, atomIndex: atomIndex) + + case .overline: + return tokenizeOverline(atom as! MTOverLine, prevAtom: prevAtom, atomIndex: atomIndex) + + case .table: + return tokenizeTable(atom as! MTMathTable, prevAtom: prevAtom, atomIndex: atomIndex) + + case .inner: + return tokenizeInner(atom as! MTInner, prevAtom: prevAtom, atomIndex: atomIndex) + + // Spacing + case .space: + return tokenizeSpace(atom, prevAtom: prevAtom, atomIndex: atomIndex) + + // Style changes - these don't create elements + case .style: + return nil + + // Color - extract inner content with color attribute + case .color, .colorBox, .textcolor: + // For now, treat as ordinary (color will be handled in display generation) + return tokenizeTextAtom(atom, prevAtom: prevAtom, atomIndex: atomIndex, allAtoms: allAtoms) + + default: + // Treat unknown types as ordinary + return tokenizeTextAtom(atom, prevAtom: prevAtom, atomIndex: atomIndex, allAtoms: allAtoms) + } + } + + // MARK: - Text Atom Tokenization + + private func tokenizeTextAtom(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int, allAtoms: [MTMathAtom]) -> MTBreakableElement? { + let text = atom.nucleus + guard !text.isEmpty else { return nil } + + // Calculate width + let width = widthCalculator.measureText(text) + + // Calculate ascent/descent (approximate using font metrics) + let ascent = font.mathTable?.axisHeight ?? font.fontSize * 0.5 + let descent = font.fontSize * 0.2 + let height = ascent + descent + + // Determine break rules using Unicode word boundary detection + var isBreakBefore = true + var isBreakAfter = true + var penaltyBefore = MTBreakPenalty.good + var penaltyAfter = MTBreakPenalty.good + + let isTextAtom = atom.fontStyle == .roman + + // Only apply word boundary logic to text atoms (not math variables) + if isTextAtom { + // First apply punctuation rules for single-character text + // This handles cases where punctuation appears in roman text rather than as separate punctuation atoms + if text.count == 1, let char = text.first { + let (punctBreakBefore, punctBreakAfter, punctPenaltyBefore, punctPenaltyAfter) = punctuationBreakRules(char) + + // Apply punctuation rules + isBreakBefore = punctBreakBefore + penaltyBefore = punctPenaltyBefore + isBreakAfter = punctBreakAfter + penaltyAfter = punctPenaltyAfter + } + + // Then apply word boundary logic - this ANDs with punctuation rules + // Both rules must allow breaking for a break to be permitted + + // Check if we should break BEFORE this atom + if let prevAtom = prevAtom { + // Handle previous accent atoms (e.g., "é" before "r" in "bactéries") + if prevAtom.type == .accent && isTextLetterAtom(prevAtom) { + // Previous is a text accent - don't break if current is a letter + if text.first?.isLetter == true { + isBreakBefore = false + penaltyBefore = MTBreakPenalty.never + } + } else if prevAtom.fontStyle == .roman { + let prevText = prevAtom.nucleus + if !prevText.isEmpty && !text.isEmpty { + // Use Unicode word boundary detection + if !hasWordBoundaryBetween(prevText, and: text) { + // No word boundary = we're in the middle of a word + isBreakBefore = false + penaltyBefore = MTBreakPenalty.never + } + } + } + } + + // Check if we should break AFTER this atom + if atomIndex + 1 < allAtoms.count { + let nextAtom = allAtoms[atomIndex + 1] + // Handle next accent atoms (e.g., "t" before "é" in "bactéries") + if nextAtom.type == .accent && isTextLetterAtom(nextAtom) { + // Next is a text accent - don't break if current is a letter + if text.last?.isLetter == true { + isBreakAfter = false + penaltyAfter = MTBreakPenalty.never + } + } else if nextAtom.fontStyle == .roman { + let nextText = nextAtom.nucleus + if !text.isEmpty && !nextText.isEmpty { + // Use Unicode word boundary detection + if !hasWordBoundaryBetween(text, and: nextText) { + // No word boundary = next atom is part of same word + isBreakAfter = false + penaltyAfter = MTBreakPenalty.never + } + } + } + } + } + + return MTBreakableElement( + content: .text(text), + width: width, + height: height, + ascent: ascent, + descent: descent, + isBreakBefore: isBreakBefore, + isBreakAfter: isBreakAfter, + penaltyBefore: penaltyBefore, + penaltyAfter: penaltyAfter, + groupId: nil, + parentId: nil, + originalAtom: atom, + indexRange: atom.indexRange, + color: nil, + backgroundColor: nil, + indivisible: false + ) + } + + /// Tokenize a multi-character text atom into individual character elements + /// This enables character-level line breaking with proper punctuation rules + private func tokenizeMultiCharText(_ atom: MTMathAtom, prevElements: [MTBreakableElement], atomIndex: Int, allAtoms: [MTMathAtom]) -> [MTBreakableElement] { + let text = atom.nucleus + guard text.count > 1 else { return [] } + + let debugTokenization = !"".isEmpty // Enable to debug text tokenization + if debugTokenization { + print("\n=== Tokenizing multi-char text: '\(text)' ===") + } + + var charElements: [MTBreakableElement] = [] + let characters = Array(text) + + for (charIndex, char) in characters.enumerated() { + let charString = String(char) + + // Calculate width for this character + let width = widthCalculator.measureText(charString) + + // Calculate ascent/descent (approximate using font metrics) + let ascent = font.mathTable?.axisHeight ?? font.fontSize * 0.5 + let descent = font.fontSize * 0.2 + let height = ascent + descent + + // Determine break rules for this character + let (isBreakBefore, isBreakAfter, penaltyBefore, penaltyAfter) = characterBreakRules( + char: char, + prevChar: charIndex > 0 ? characters[charIndex - 1] : nil, + nextChar: charIndex < characters.count - 1 ? characters[charIndex + 1] : nil, + isFirstInAtom: charIndex == 0, + isLastInAtom: charIndex == characters.count - 1, + prevElements: prevElements, + nextAtom: atomIndex + 1 < allAtoms.count ? allAtoms[atomIndex + 1] : nil + ) + + let element = MTBreakableElement( + content: .text(charString), + width: width, + height: height, + ascent: ascent, + descent: descent, + isBreakBefore: isBreakBefore, + isBreakAfter: isBreakAfter, + penaltyBefore: penaltyBefore, + penaltyAfter: penaltyAfter, + groupId: nil, + parentId: nil, + originalAtom: atom, + indexRange: atom.indexRange, + color: nil, + backgroundColor: nil, + indivisible: false + ) + + if debugTokenization { + print(" [\(charIndex)] '\(charString)' breakBefore=\(isBreakBefore) breakAfter=\(isBreakAfter) penaltyBefore=\(penaltyBefore) penaltyAfter=\(penaltyAfter) width=\(width)") + } + + charElements.append(element) + } + + return charElements + } + + /// Determine break rules for a character in a multi-character text string + private func characterBreakRules( + char: Character, + prevChar: Character?, + nextChar: Character?, + isFirstInAtom: Bool, + isLastInAtom: Bool, + prevElements: [MTBreakableElement], + nextAtom: MTMathAtom? + ) -> (isBreakBefore: Bool, isBreakAfter: Bool, penaltyBefore: Int, penaltyAfter: Int) { + + // Apply punctuation rules + let (punctBreakBefore, punctBreakAfter, punctPenaltyBefore, punctPenaltyAfter) = punctuationBreakRules(char) + + var isBreakBefore = punctBreakBefore + var isBreakAfter = punctBreakAfter + var penaltyBefore = punctPenaltyBefore + var penaltyAfter = punctPenaltyAfter + + // Apply word boundary logic + // Don't break in the middle of a word (but CJK characters CAN break between each other) + if let prevChar = prevChar { + if char.isLetter && prevChar.isLetter { + // Check if either character is CJK - CJK allows breaks between characters + let isCJKBreak = isCJKCharacter(char) || isCJKCharacter(prevChar) + + if !isCJKBreak { + // Both letters in same non-CJK script - middle of word, don't break + isBreakBefore = false + penaltyBefore = MTBreakPenalty.never + } + // else: At least one is CJK - allow break (keep punctBreakBefore value) + } else if prevChar == "'" || prevChar == "-" { + // Apostrophe or hyphen - part of word + isBreakBefore = false + penaltyBefore = MTBreakPenalty.never + } + } else if isFirstInAtom { + // First character - check against previous element + if let lastElement = prevElements.last { + switch lastElement.content { + case .text(let prevText): + if let prevLastChar = prevText.last { + if char.isLetter && prevLastChar.isLetter { + // Check if either character is CJK + let isCJKBreak = isCJKCharacter(char) || isCJKCharacter(prevLastChar) + + if !isCJKBreak { + // Both non-CJK letters - don't break + isBreakBefore = false + penaltyBefore = MTBreakPenalty.never + } + } else if prevLastChar == "'" || prevLastChar == "-" { + isBreakBefore = false + penaltyBefore = MTBreakPenalty.never + } + } + case .display: + // Check if previous element is a text-mode accent (e.g., "é") + // Accents in text mode should not allow breaks after them if current is a letter + if lastElement.originalAtom.type == .accent, + isTextLetterAtom(lastElement.originalAtom), + char.isLetter { + isBreakBefore = false + penaltyBefore = MTBreakPenalty.never + } + default: + break + } + } + } + + if let nextChar = nextChar { + if char.isLetter && nextChar.isLetter { + // Check if either character is CJK + let isCJKBreak = isCJKCharacter(char) || isCJKCharacter(nextChar) + + if !isCJKBreak { + // Both non-CJK letters - middle of word, don't break + isBreakAfter = false + penaltyAfter = MTBreakPenalty.never + } + } else if nextChar == "'" || nextChar == "-" { + // Before apostrophe or hyphen - part of word + isBreakAfter = false + penaltyAfter = MTBreakPenalty.never + } + } else if isLastInAtom { + // Last character - check against next atom + if let nextAtom = nextAtom { + // Handle next accent atoms (e.g., "t" before "é" in "bactéries") + if nextAtom.type == .accent && isTextLetterAtom(nextAtom) { + if char.isLetter { + isBreakAfter = false + penaltyAfter = MTBreakPenalty.never + } + } else if nextAtom.fontStyle == .roman, + let nextFirstChar = nextAtom.nucleus.first { + if char.isLetter && nextFirstChar.isLetter { + // Check if either character is CJK + let isCJKBreak = isCJKCharacter(char) || isCJKCharacter(nextFirstChar) + + if !isCJKBreak { + // Both non-CJK letters - don't break + isBreakAfter = false + penaltyAfter = MTBreakPenalty.never + } + } + } + } + } + + return (isBreakBefore, isBreakAfter, penaltyBefore, penaltyAfter) + } + + // MARK: - Word Boundary Detection + + /// Determines if a character is a CJK (Chinese, Japanese, Korean) character + /// CJK characters can break between each other even though they are technically "letters" + private func isCJKCharacter(_ char: Character) -> Bool { + guard let scalar = char.unicodeScalars.first else { return false } + let value = scalar.value + + // CJK Unified Ideographs and extensions + return (value >= 0x4E00 && value <= 0x9FFF) || // CJK Unified Ideographs (most common Chinese/Japanese kanji) + (value >= 0x3400 && value <= 0x4DBF) || // CJK Unified Ideographs Extension A + (value >= 0x20000 && value <= 0x2A6DF) || // CJK Unified Ideographs Extension B + (value >= 0x3040 && value <= 0x309F) || // Hiragana (Japanese) + (value >= 0x30A0 && value <= 0x30FF) || // Katakana (Japanese) + (value >= 0xAC00 && value <= 0xD7AF) // Hangul Syllables (Korean) + } + + /// Determines if there's a word boundary between two text fragments + /// Combines Unicode word segmentation with special handling for contractions and hyphenated words + private func hasWordBoundaryBetween(_ text1: String, and text2: String) -> Bool { + // RULE 1: Check for apostrophes and hyphens between letters (contractions and hyphenated words) + // These should NOT be treated as word boundaries even though Unicode does + if let lastChar1 = text1.last, let firstChar2 = text2.first { + // Pattern: letter + apostrophe|hyphen + letter → NOT a word boundary + if lastChar1.isLetter && (firstChar2 == "'" || firstChar2 == "-") { + return false // Don't break before apostrophe/hyphen + } + if (lastChar1 == "'" || lastChar1 == "-") && firstChar2.isLetter { + return false // Don't break after apostrophe/hyphen + } + } + + // RULE 2: Use Unicode word boundary detection for everything else + // This properly handles: + // - International text (café, naïve, etc.) + // - Various Unicode whitespace characters + // - Em-dashes, ellipses, and other Unicode punctuation + // - Complex scripts (Thai, Japanese, etc.) + let combined = text1 + text2 + let junctionIndex = text1.endIndex + + var wordBoundaries: Set = [] + combined.enumerateSubstrings(in: combined.startIndex.. PunctuationClass { + let scalar = String(char).unicodeScalars.first?.value ?? 0 + + // Latin opening punctuation - never break after + if "([{".contains(char) { return .openingPunctuation } + + // Latin closing punctuation and sentence-ending - never break before + if ")]}".contains(char) { return .closingPunctuation } + if ".,;:!?".contains(char) { return .sentenceEnding } + + // Latin quotation marks - opening quotes + // U+0022 " QUOTATION MARK, U+0027 ' APOSTROPHE + // U+2018 ' LEFT SINGLE QUOTATION MARK, U+201C " LEFT DOUBLE QUOTATION MARK + // U+00AB « LEFT-POINTING DOUBLE ANGLE QUOTATION MARK, U+2039 ‹ SINGLE LEFT-POINTING ANGLE QUOTATION MARK + if scalar == 0x0022 || scalar == 0x0027 || // Basic quotes + scalar == 0x2018 || scalar == 0x201C || // Curly left quotes + scalar == 0x00AB || scalar == 0x2039 { // Guillemets + return .openingPunctuation + } + + // Latin quotation marks - closing quotes + // U+2019 ' RIGHT SINGLE QUOTATION MARK, U+201D " RIGHT DOUBLE QUOTATION MARK + // U+00BB » RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK, U+203A › SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + if scalar == 0x2019 || scalar == 0x201D || // Curly right quotes + scalar == 0x00BB || scalar == 0x203A { // Guillemets + return .closingPunctuation + } + + // CJK opening brackets (禁則: line-start prohibited) + // Japanese/Chinese full-width brackets and corner brackets + if "「『(【〔〈《".contains(char) { + return .openingPunctuation + } + + // CJK closing brackets (禁則: line-end prohibited) + if "」』)】〕〉》".contains(char) { + return .closingPunctuation + } + + // CJK sentence-ending punctuation (禁則: line-end prohibited) + // Japanese/Chinese full-width periods, commas, and other punctuation + if "。、!?:;".contains(char) { + return .sentenceEnding + } + + // CJK small kana (禁則: line-end prohibited) + // These are smaller versions of hiragana/katakana that must not start a line + if "ぁぃぅぇぉっゃゅょゎァィゥェォッャュョヮ".contains(char) { + return .cjkSmallKana + } + + // CJK iteration marks (禁則: line-end prohibited) + if "ゝゞヽヾ々〻".contains(char) { + return .cjkSmallKana // Same rules as small kana + } + + // CJK prolonged sound mark (禁則: line-end prohibited) + if char == "ー" { + return .cjkSmallKana // Same rules as small kana + } + + return .neutral + } + + /// Determine break rules for punctuation based on its classification + private func punctuationBreakRules(_ char: Character) -> (isBreakBefore: Bool, isBreakAfter: Bool, penaltyBefore: Int, penaltyAfter: Int) { + let classification = classifyPunctuation(char) + + switch classification { + case .openingPunctuation: + // Opening punctuation: can break before, NEVER after + // Examples: ( [ { " ' « 「『 + return (true, false, MTBreakPenalty.good, MTBreakPenalty.never) + + case .closingPunctuation: + // Closing punctuation: NEVER before, can break after + // Examples: ) ] } " ' » 」』 + return (false, true, MTBreakPenalty.never, MTBreakPenalty.good) + + case .sentenceEnding: + // Sentence-ending punctuation: NEVER before, good break after + // Examples: . , ; : ! ? 。、 + return (false, true, MTBreakPenalty.never, MTBreakPenalty.best) + + case .cjkSmallKana: + // CJK small kana and iteration marks: NEVER before, can break after + // Examples: っゃゅょゎ ゝゞ ー + return (false, true, MTBreakPenalty.never, MTBreakPenalty.good) + + case .neutral: + // Other punctuation: use default rules + return (true, true, MTBreakPenalty.good, MTBreakPenalty.good) + } + } + + // MARK: - Operator Tokenization + + private func tokenizeOperator(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? { + let op = atom.nucleus + guard !op.isEmpty else { return nil } + + // Calculate width with operator spacing + let width = widthCalculator.measureOperator(op, type: atom.type) + + let ascent = font.fontSize * 0.5 + let descent = font.fontSize * 0.2 + let height = ascent + descent + + return MTBreakableElement( + content: .operator(op, type: atom.type), + width: width, + height: height, + ascent: ascent, + descent: descent, + isBreakBefore: true, + isBreakAfter: true, + penaltyBefore: MTBreakPenalty.best, // Operators are best break points + penaltyAfter: MTBreakPenalty.best, + groupId: nil, + parentId: nil, + originalAtom: atom, + indexRange: atom.indexRange, + color: nil, + backgroundColor: nil, + indivisible: false + ) + } + + // MARK: - Delimiter Tokenization + + private func tokenizeOpenDelimiter(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? { + let delimiter = atom.nucleus + let width = widthCalculator.measureText(delimiter) + let ascent = font.fontSize * 0.6 + let descent = font.fontSize * 0.2 + + return MTBreakableElement( + content: .text(delimiter), + width: width, + height: ascent + descent, + ascent: ascent, + descent: descent, + isBreakBefore: true, + isBreakAfter: false, // NEVER break after open delimiter + penaltyBefore: MTBreakPenalty.acceptable, + penaltyAfter: MTBreakPenalty.bad, + groupId: nil, + parentId: nil, + originalAtom: atom, + indexRange: atom.indexRange, + color: nil, + backgroundColor: nil, + indivisible: false + ) + } + + private func tokenizeCloseDelimiter(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? { + let delimiter = atom.nucleus + let width = widthCalculator.measureText(delimiter) + let ascent = font.fontSize * 0.6 + let descent = font.fontSize * 0.2 + + return MTBreakableElement( + content: .text(delimiter), + width: width, + height: ascent + descent, + ascent: ascent, + descent: descent, + isBreakBefore: false, // NEVER break before close delimiter + isBreakAfter: true, + penaltyBefore: MTBreakPenalty.bad, + penaltyAfter: MTBreakPenalty.acceptable, + groupId: nil, + parentId: nil, + originalAtom: atom, + indexRange: atom.indexRange, + color: nil, + backgroundColor: nil, + indivisible: false + ) + } + + // MARK: - Punctuation Tokenization + + private func tokenizePunctuation(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? { + let punct = atom.nucleus + let width = widthCalculator.measureText(punct) + let ascent = font.fontSize * 0.5 + let descent = font.fontSize * 0.2 + + // Apply proper punctuation breaking rules based on character classification + // Default rules for multi-character punctuation or empty + var isBreakBefore = false + var isBreakAfter = true + var penaltyBefore = MTBreakPenalty.bad + var penaltyAfter = MTBreakPenalty.good + + // For single-character punctuation, use classification rules + if punct.count == 1, let char = punct.first { + (isBreakBefore, isBreakAfter, penaltyBefore, penaltyAfter) = punctuationBreakRules(char) + } + + return MTBreakableElement( + content: .text(punct), + width: width, + height: ascent + descent, + ascent: ascent, + descent: descent, + isBreakBefore: isBreakBefore, + isBreakAfter: isBreakAfter, + penaltyBefore: penaltyBefore, + penaltyAfter: penaltyAfter, + groupId: nil, + parentId: nil, + originalAtom: atom, + indexRange: atom.indexRange, + color: nil, + backgroundColor: nil, + indivisible: false + ) + } + + // MARK: - Script Tokenization + + private func tokenizeAtomWithScripts(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int, allAtoms: [MTMathAtom]) -> [MTBreakableElement] { + var elements: [MTBreakableElement] = [] + let groupId = UUID() // All elements in this group must stay together + + // First, create the base element + if let baseElement = tokenizeAtom(atom, prevAtom: prevAtom, atomIndex: atomIndex, allAtoms: allAtoms) { + var modifiedBase = baseElement + // Modify to be part of group + modifiedBase = MTBreakableElement( + content: baseElement.content, + width: baseElement.width, + height: baseElement.height, + ascent: baseElement.ascent, + descent: baseElement.descent, + isBreakBefore: baseElement.isBreakBefore, + isBreakAfter: false, // Cannot break after base - must include scripts + penaltyBefore: baseElement.penaltyBefore, + penaltyAfter: MTBreakPenalty.never, + groupId: groupId, + parentId: nil, + originalAtom: baseElement.originalAtom, + indexRange: baseElement.indexRange, + color: baseElement.color, + backgroundColor: baseElement.backgroundColor, + indivisible: baseElement.indivisible + ) + elements.append(modifiedBase) + } + + // Add superscript first if present (matches legacy typesetter order) + if let superScript = atom.superScript { + if let scriptDisplay = displayRenderer.renderScript(superScript, isSuper: true) { + let scriptElement = MTBreakableElement( + content: .script(scriptDisplay, isSuper: true), + width: scriptDisplay.width, + height: scriptDisplay.ascent + scriptDisplay.descent, + ascent: scriptDisplay.ascent, + descent: scriptDisplay.descent, + isBreakBefore: false, // Must stay with base + isBreakAfter: atom.subScript == nil, // Can break after if last script + penaltyBefore: MTBreakPenalty.never, + penaltyAfter: atom.subScript == nil ? MTBreakPenalty.good : MTBreakPenalty.never, + groupId: groupId, + parentId: nil, + originalAtom: atom, + indexRange: atom.indexRange, + color: nil, + backgroundColor: nil, + indivisible: true + ) + elements.append(scriptElement) + } + } + + // Add subscript after superscript (matches legacy typesetter order) + if let subScript = atom.subScript { + if let scriptDisplay = displayRenderer.renderScript(subScript, isSuper: false) { + let scriptElement = MTBreakableElement( + content: .script(scriptDisplay, isSuper: false), + width: scriptDisplay.width, + height: scriptDisplay.ascent + scriptDisplay.descent, + ascent: scriptDisplay.ascent, + descent: scriptDisplay.descent, + isBreakBefore: false, // Must stay with base + isBreakAfter: true, // Can break after subscript (it's always last) + penaltyBefore: MTBreakPenalty.never, + penaltyAfter: MTBreakPenalty.good, + groupId: groupId, + parentId: nil, + originalAtom: atom, + indexRange: atom.indexRange, + color: nil, + backgroundColor: nil, + indivisible: true + ) + elements.append(scriptElement) + } + } + + return elements + } + + // MARK: - Complex Structure Tokenization + + private func tokenizeFraction(_ fraction: MTFraction, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? { + // Create a temporary typesetter to render the fraction + let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false) + guard let display = typesetter.makeFraction(fraction) else { return nil } + + return MTBreakableElement( + content: .display(display), + width: display.width, + height: display.ascent + display.descent, + ascent: display.ascent, + descent: display.descent, + isBreakBefore: true, + isBreakAfter: true, + penaltyBefore: MTBreakPenalty.moderate, + penaltyAfter: MTBreakPenalty.moderate, + groupId: nil, + parentId: nil, + originalAtom: fraction, + indexRange: fraction.indexRange, + color: nil, + backgroundColor: nil, + indivisible: true // Fractions are atomic + ) + } + + private func tokenizeRadical(_ radical: MTRadical, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? { + let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false) + guard let display = typesetter.makeRadical(radical.radicand, range: radical.indexRange) else { return nil } + + // Add degree if present + if radical.degree != nil { + // Use .script style (71% size) instead of .scriptOfScript (50% size) + // This matches TeX standard for radical degrees + let degree = MTTypesetter.createLineForMathList(radical.degree, font: font, style: .script) + display.setDegree(degree, fontMetrics: font.mathTable) + } + + return MTBreakableElement( + content: .display(display), + width: display.width, + height: display.ascent + display.descent, + ascent: display.ascent, + descent: display.descent, + isBreakBefore: true, + isBreakAfter: true, + penaltyBefore: MTBreakPenalty.good, + penaltyAfter: MTBreakPenalty.good, + groupId: nil, + parentId: nil, + originalAtom: radical, + indexRange: radical.indexRange, + color: nil, + backgroundColor: nil, + indivisible: true // Radicals are atomic + ) + } + + private func tokenizeLargeOperator(_ op: MTLargeOperator, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? { + // CRITICAL DISTINCTION: + // - If op.limits=true (e.g., \sum, \prod, \lim in text mode): Scripts go ABOVE/BELOW + // → makeLargeOp() creates MTLargeOpLimitsDisplay, which is self-contained + // → We should NOT clear scripts, let makeLargeOp() handle everything + // + // - If op.limits=false (e.g., \int in text mode): Scripts go TO THE SIDE + // → makeLargeOp() would create scripts via makeScripts(), causing duplication + // → We MUST clear scripts and let tokenizeAtomWithScripts() handle them separately + + let limits = op.limits && (style == .display || style == .text) + + let originalSuperScript = op.superScript + let originalSubScript = op.subScript + + // Only clear scripts for side-script operators (limits=false) + if !limits && (originalSuperScript != nil || originalSubScript != nil) { + op.superScript = nil + op.subScript = nil + } + + let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false) + guard let operatorDisplay = typesetter.makeLargeOp(op) else { + // Restore scripts before returning + op.superScript = originalSuperScript + op.subScript = originalSubScript + return nil + } + + // CRITICAL: Handle scripts based on positioning mode + if !limits { + // Side-script operators (limits=false): Restore scripts for tokenizeAtomWithScripts to handle + op.superScript = originalSuperScript + op.subScript = originalSubScript + } else { + // Limit operators (limits=true): Scripts are already rendered in MTLargeOpLimitsDisplay + // MUST clear them from atom to prevent tokenizeAtomWithScripts from rendering them again + op.superScript = nil + op.subScript = nil + } + + // CRITICAL: Handle italic correction (delta) for side-script operators + // When scripts are present and limits is false, the operator width is reduced by delta + // (see MTTypesetter.makeLargeOp line 1046-1050) + // Since we cleared scripts for side-script operators, makeLargeOp() didn't apply this reduction + var finalWidth = operatorDisplay.width + + if !limits && (originalSubScript != nil) { + // Get the italic correction for the operator glyph + if let glyphDisplay = operatorDisplay as? MTGlyphDisplay, + let mathTable = font.mathTable { + let delta = mathTable.getItalicCorrection(glyphDisplay.glyph) + finalWidth -= delta + } + } + + let finalDisplay = operatorDisplay + + return MTBreakableElement( + content: .display(finalDisplay), + width: finalWidth, + height: finalDisplay.ascent + finalDisplay.descent, + ascent: finalDisplay.ascent, + descent: finalDisplay.descent, + isBreakBefore: true, + isBreakAfter: true, + penaltyBefore: MTBreakPenalty.good, + penaltyAfter: MTBreakPenalty.good, + groupId: nil, + parentId: nil, + originalAtom: op, + indexRange: op.indexRange, + color: nil, + backgroundColor: nil, + indivisible: true + ) + } + + private func tokenizeAccent(_ accent: MTAccent, prevAtom: MTMathAtom?, atomIndex: Int, allAtoms: [MTMathAtom]) -> MTBreakableElement? { + let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false) + guard let display = typesetter.makeAccent(accent) else { return nil } + + // Determine break rules - accents in text mode should respect word boundaries + var isBreakBefore = true + var isBreakAfter = true + var penaltyBefore = MTBreakPenalty.good + var penaltyAfter = MTBreakPenalty.good + + // Check if this accent is in a text context (the accented character is roman text) + // An accent's innerList contains the base character(s) + let isTextAccent = accent.innerList?.atoms.first?.fontStyle == .roman + + if isTextAccent { + // Check previous atom - if it's a letter in text mode, don't break before + if let prevAtom = prevAtom { + let prevIsTextLetter = isTextLetterAtom(prevAtom) + if prevIsTextLetter { + isBreakBefore = false + penaltyBefore = MTBreakPenalty.never + } + } + + // Check next atom - if it's a letter in text mode, don't break after + if atomIndex + 1 < allAtoms.count { + let nextAtom = allAtoms[atomIndex + 1] + let nextIsTextLetter = isTextLetterAtom(nextAtom) + if nextIsTextLetter { + isBreakAfter = false + penaltyAfter = MTBreakPenalty.never + } + } + } + + return MTBreakableElement( + content: .display(display), + width: display.width, + height: display.ascent + display.descent, + ascent: display.ascent, + descent: display.descent, + isBreakBefore: isBreakBefore, + isBreakAfter: isBreakAfter, + penaltyBefore: penaltyBefore, + penaltyAfter: penaltyAfter, + groupId: nil, + parentId: nil, + originalAtom: accent, + indexRange: accent.indexRange, + color: nil, + backgroundColor: nil, + indivisible: true + ) + } + + /// Helper to check if an atom is a letter in text mode (roman style) + private func isTextLetterAtom(_ atom: MTMathAtom) -> Bool { + // For accent atoms, check their inner content FIRST + // (accent atom's nucleus contains the accent mark, not the letter) + if let accent = atom as? MTAccent { + if let firstInner = accent.innerList?.atoms.first { + return isTextLetterAtom(firstInner) + } + return false + } + // For regular text atoms with roman style + if atom.fontStyle == .roman { + // Check if the nucleus contains only letters + let nucleus = atom.nucleus + if !nucleus.isEmpty { + return nucleus.allSatisfy { $0.isLetter } + } + } + return false + } + + private func tokenizeUnderline(_ underline: MTUnderLine, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? { + let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false) + guard let display = typesetter.makeUnderline(underline) else { return nil } + + return MTBreakableElement( + content: .display(display), + width: display.width, + height: display.ascent + display.descent, + ascent: display.ascent, + descent: display.descent, + isBreakBefore: true, + isBreakAfter: true, + penaltyBefore: MTBreakPenalty.good, + penaltyAfter: MTBreakPenalty.good, + groupId: nil, + parentId: nil, + originalAtom: underline, + indexRange: underline.indexRange, + color: nil, + backgroundColor: nil, + indivisible: true + ) + } + + private func tokenizeOverline(_ overline: MTOverLine, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? { + let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false) + guard let display = typesetter.makeOverline(overline) else { return nil } + + return MTBreakableElement( + content: .display(display), + width: display.width, + height: display.ascent + display.descent, + ascent: display.ascent, + descent: display.descent, + isBreakBefore: true, + isBreakAfter: true, + penaltyBefore: MTBreakPenalty.good, + penaltyAfter: MTBreakPenalty.good, + groupId: nil, + parentId: nil, + originalAtom: overline, + indexRange: overline.indexRange, + color: nil, + backgroundColor: nil, + indivisible: true + ) + } + + private func tokenizeTable(_ table: MTMathTable, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? { + let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false, maxWidth: maxWidth) + guard let display = typesetter.makeTable(table) else { return nil } + + return MTBreakableElement( + content: .display(display), + width: display.width, + height: display.ascent + display.descent, + ascent: display.ascent, + descent: display.descent, + isBreakBefore: true, + isBreakAfter: true, + penaltyBefore: MTBreakPenalty.moderate, + penaltyAfter: MTBreakPenalty.moderate, + groupId: nil, + parentId: nil, + originalAtom: table, + indexRange: table.indexRange, + color: nil, + backgroundColor: nil, + indivisible: true + ) + } + + private func tokenizeInner(_ inner: MTInner, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? { + let typesetter = MTTypesetter(withFont: font, style: style, cramped: cramped, spaced: false) + guard let display = typesetter.makeLeftRight(inner) else { return nil } + + return MTBreakableElement( + content: .display(display), + width: display.width, + height: display.ascent + display.descent, + ascent: display.ascent, + descent: display.descent, + isBreakBefore: true, + isBreakAfter: true, + penaltyBefore: MTBreakPenalty.good, + penaltyAfter: MTBreakPenalty.good, + groupId: nil, + parentId: nil, + originalAtom: inner, + indexRange: inner.indexRange, + color: nil, + backgroundColor: nil, + indivisible: false + ) + } + + private func tokenizeSpace(_ atom: MTMathAtom, prevAtom: MTMathAtom?, atomIndex: Int) -> MTBreakableElement? { + // Space atoms typically don't participate in breaking + // They are rendered as-is + let width = widthCalculator.measureSpace(atom.type) + + return MTBreakableElement( + content: .space(width), + width: width, + height: 0, + ascent: 0, + descent: 0, + isBreakBefore: false, + isBreakAfter: false, + penaltyBefore: MTBreakPenalty.never, + penaltyAfter: MTBreakPenalty.never, + groupId: nil, + parentId: nil, + originalAtom: atom, + indexRange: atom.indexRange, + color: nil, + backgroundColor: nil, + indivisible: true + ) + } +} diff --git a/third-party/SwiftMath/Sources/MathRender/Tokenization/MTBreakableElement.swift b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTBreakableElement.swift new file mode 100644 index 0000000000..163a86871f --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTBreakableElement.swift @@ -0,0 +1,115 @@ +// +// MTBreakableElement.swift +// SwiftMath +// +// Created by Claude Code on 2025-12-16. +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation +import CoreGraphics + +// MARK: - MTElementContent + +/// Represents the content type of a breakable element +enum MTElementContent { + /// Simple text content + case text(String) + /// Pre-rendered display (fraction, radical, etc.) + case display(MTDisplay) + /// Math operator with spacing + case `operator`(String, type: MTMathAtomType) + /// Explicit spacing + case space(CGFloat) + /// Superscript or subscript display + case script(MTDisplay, isSuper: Bool) +} + +// MARK: - MTBreakableElement + +/// Represents a breakable element with pre-calculated width and break rules +struct MTBreakableElement { + // MARK: Display properties + + /// The content of this element + let content: MTElementContent + + /// Pre-calculated width (cached) + let width: CGFloat + + /// Height of the element + let height: CGFloat + + /// Distance from baseline to top + let ascent: CGFloat + + /// Distance from baseline to bottom + let descent: CGFloat + + // MARK: Breaking rules + + /// Can break BEFORE this element? + let isBreakBefore: Bool + + /// Can break AFTER this element? + let isBreakAfter: Bool + + /// Penalty for breaking before (0=good, 100=bad, 150=never) + let penaltyBefore: Int + + /// Penalty for breaking after (0=good, 100=bad, 150=never) + let penaltyAfter: Int + + // MARK: Relationship tracking + + /// Elements with same groupId must stay together + let groupId: UUID? + + /// Parent element ID (for scripts) + let parentId: UUID? + + // MARK: Source tracking + + /// Original atom this element was created from + let originalAtom: MTMathAtom + + /// Index range in the original math list + let indexRange: NSRange + + // MARK: Optional attributes + + /// Text color for this element + let color: MTColor? + + /// Background color for this element + let backgroundColor: MTColor? + + // MARK: Atomicity flag + + /// If true, NEVER break this element internally + let indivisible: Bool +} + +// MARK: - Penalty Constants + +/// Penalty values for line breaking decisions +enum MTBreakPenalty { + /// Best break points (operators, relations) + static let best = 0 + + /// Good break points (ordinary atoms, after scripts) + static let good = 10 + + /// Moderate penalty (before fractions, radicals) + static let moderate = 15 + + /// Acceptable break points + static let acceptable = 50 + + /// Bad break points (avoid if possible) + static let bad = 100 + + /// Never break here (grouped elements) + static let never = 150 +} diff --git a/third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayGenerator.swift b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayGenerator.swift new file mode 100644 index 0000000000..b9faf4f267 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayGenerator.swift @@ -0,0 +1,389 @@ +// +// MTDisplayGenerator.swift +// SwiftMath +// +// Created by Claude Code on 2025-12-16. +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation +import CoreGraphics +import CoreText + +/// Generates MTDisplay objects from fitted lines of breakable elements +class MTDisplayGenerator { + + // MARK: - Properties + + let font: MTFont + let style: MTLineStyle + let widthCalculator: MTElementWidthCalculator + + // MARK: - Initialization + + init(font: MTFont, style: MTLineStyle) { + self.font = font + self.style = style + self.widthCalculator = MTElementWidthCalculator(font: font, style: style) + } + + // MARK: - Display Generation + + /// Generate displays from fitted lines + func generateDisplays(from lines: [[MTBreakableElement]], startPosition: CGPoint) -> [MTDisplay] { + var allDisplays: [MTDisplay] = [] + var currentY = startPosition.y + + // Minimum spacing between lines (20% of font size for breathing room) + let minimumLineSpacing = font.fontSize * 0.2 + + for (index, line) in lines.enumerated() { + let (lineDisplays, currentLineMetrics) = generateLine(line, at: CGPoint(x: startPosition.x, y: currentY)) + allDisplays.append(contentsOf: lineDisplays) + + // Calculate spacing for next line based on actual content heights + if index < lines.count - 1 { + let nextLine = lines[index + 1] + let nextLineAscent = nextLine.map { $0.ascent }.max() ?? 0 + + // Space needed = current line's descent + minimum spacing + next line's ascent + let spaceNeeded = currentLineMetrics.descent + minimumLineSpacing + nextLineAscent + + // Ensure minimum spacing of 1.2x font size for readability + let minSpacing = font.fontSize * 1.2 + currentY -= max(spaceNeeded, minSpacing) + } + } + + return allDisplays + } + + /// Line metrics for spacing calculation + struct LineMetrics { + let ascent: CGFloat + let descent: CGFloat + var height: CGFloat { ascent + descent } + } + + /// Generate displays for a single line + private func generateLine(_ elements: [MTBreakableElement], at position: CGPoint) -> ([MTDisplay], LineMetrics) { + var displays: [MTDisplay] = [] + var xOffset: CGFloat = 0 + + // Calculate line metrics + let lineAscent = elements.map { $0.ascent }.max() ?? 0 + let lineDescent = elements.map { $0.descent }.max() ?? 0 + + // Baseline y position + let baseline = position.y + + var i = 0 + while i < elements.count { + let element = elements[i] + + // Check if this is part of a group (base + scripts) + if let groupId = element.groupId { + // Collect all elements in this group + var groupElements: [MTBreakableElement] = [] + var j = i + while j < elements.count && elements[j].groupId == groupId { + groupElements.append(elements[j]) + j += 1 + } + + // Render the group + let groupAdvance = renderGroup(groupElements, at: CGPoint(x: position.x + xOffset, y: baseline), displays: &displays) + xOffset += groupAdvance + i = j + } else { + // Regular element (not part of a group) + + // CRITICAL: For operators, spacing should be split evenly before and after + // The element.width includes both spacing, but we need to position the operator + // with half spacing before it + var spacingBefore: CGFloat = 0 + if case .operator(let op, _) = element.content { + // Get the actual text width vs element width to calculate spacing + let textWidth = widthCalculator.measureText(op) + let totalSpacing = element.width - textWidth + spacingBefore = totalSpacing / 2 + } + + let elementPosition = CGPoint(x: position.x + xOffset + spacingBefore, y: baseline) + + switch element.content { + case .text(let text): + let display = createTextDisplay(text, at: elementPosition, element: element) + displays.append(display) + + case .display(let preRenderedDisplay): + // Use pre-rendered display (fraction, radical, etc.) + let mutableDisplay = preRenderedDisplay + mutableDisplay.position = elementPosition + displays.append(mutableDisplay) + + case .operator(let op, _): + let display = createTextDisplay(op, at: elementPosition, element: element) + displays.append(display) + + case .script: + // Standalone script (shouldn't happen, but handle gracefully) + break + + case .space: + // No display for space, just advance position + break + } + + xOffset += element.width + i += 1 + } + } + + return (displays, LineMetrics(ascent: lineAscent, descent: lineDescent)) + } + + /// Render a group of elements (base + scripts) and return the horizontal advance + private func renderGroup(_ groupElements: [MTBreakableElement], at position: CGPoint, displays: inout [MTDisplay]) -> CGFloat { + var baseWidth: CGFloat = 0 + var superscriptWidth: CGFloat = 0 + var subscriptWidth: CGFloat = 0 + var baseXOffset: CGFloat = 0 + + // Check if this group has any scripts + let hasScripts = groupElements.contains { element in + if case .script = element.content { + return true + } + return false + } + + // Track the start index of base displays for dimension adjustment + let baseDisplayStartIndex = displays.count + + // First pass: render base elements and collect script widths + for element in groupElements { + switch element.content { + case .script: + // Skip scripts in first pass + break + default: + // Render base element + let basePosition = CGPoint(x: position.x + baseXOffset, y: position.y) + + switch element.content { + case .text(let text): + let display = createTextDisplay(text, at: basePosition, element: element, hasScript: hasScripts) + displays.append(display) + case .display(let preRenderedDisplay): + let mutableDisplay = preRenderedDisplay + mutableDisplay.position = basePosition + displays.append(mutableDisplay) + case .operator(let op, _): + let display = createTextDisplay(op, at: basePosition, element: element, hasScript: hasScripts) + displays.append(display) + default: + break + } + + baseWidth += element.width + baseXOffset += element.width + } + } + + // Second pass: collect script information for joint positioning + var superscriptDisplay: MTDisplay? = nil + var subscriptDisplay: MTDisplay? = nil + var hasBothScripts = false + + for element in groupElements { + if case .script(let scriptDisplay, let isSuper) = element.content { + if isSuper { + superscriptDisplay = scriptDisplay + } else { + subscriptDisplay = scriptDisplay + } + } + } + + hasBothScripts = superscriptDisplay != nil && subscriptDisplay != nil + + // Third pass: render scripts with proper positioning + var superScriptShiftUp: CGFloat = 0 + var subscriptShiftDown: CGFloat = 0 + + // Check if base is a glyph (not CTLineDisplay) for special positioning + // For glyphs (like large operators), position scripts relative to glyph edges + var isGlyphBase = false + for disp in displays[baseDisplayStartIndex.. 0 { + // Superscript is lower than the max allowed by the font with a subscript + superScriptShiftUp += superscriptBottomDelta + subscriptShiftDown -= superscriptBottomDelta + } + } + } + + // Calculate italic correction (delta) for superscript positioning + // Superscripts are positioned at baseWidth + delta, subscripts at baseWidth + var delta: CGFloat = 0 + if superscriptDisplay != nil { + // Get italic correction from the base display if it's a glyph + for disp in displays[baseDisplayStartIndex.. baseDisplayStartIndex { + // Calculate the full extent of the group including scripts + var maxAscent: CGFloat = 0 + var maxDescent: CGFloat = 0 + + for i in baseDisplayStartIndex.. MTDisplay { + let attrString = NSMutableAttributedString(string: text) + attrString.addAttribute( + NSAttributedString.Key(kCTFontAttributeName as String), + value: font.ctFont as Any, + range: NSMakeRange(0, attrString.length) + ) + + // If the atom was fused (multiple ordinary chars combined), use fusedAtoms + // Otherwise, use the original atom + let atoms: [MTMathAtom] + if !element.originalAtom.fusedAtoms.isEmpty { + atoms = element.originalAtom.fusedAtoms + } else { + atoms = [element.originalAtom] + } + + let display = MTCTLineDisplay( + withString: attrString, + position: position, + range: element.indexRange, + font: font, + atoms: atoms + ) + + // Mark if this base element has associated scripts + display.hasScript = hasScript + + return display + } +} diff --git a/third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayPreRenderer.swift b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayPreRenderer.swift new file mode 100644 index 0000000000..d6af072a60 --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayPreRenderer.swift @@ -0,0 +1,88 @@ +// +// MTDisplayPreRenderer.swift +// SwiftMath +// +// Created by Claude Code on 2025-12-16. +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation +import CoreGraphics + +/// Pre-renders complex atoms (fractions, radicals, etc.) as MTDisplay objects during tokenization +class MTDisplayPreRenderer { + + // MARK: - Properties + + let font: MTFont + let style: MTLineStyle + let cramped: Bool + + // MARK: - Initialization + + init(font: MTFont, style: MTLineStyle, cramped: Bool) { + self.font = font + self.style = style + self.cramped = cramped + } + + // MARK: - Script Rendering + + /// Render a script (superscript or subscript) as a display + func renderScript(_ mathList: MTMathList, isSuper: Bool) -> MTDisplay? { + let scriptStyle = getScriptStyle() + let scriptCramped = isSuper ? cramped : true // Subscripts are always cramped + + // Scale the font for the script style + let scriptFontSize = MTTypesetter.getStyleSize(scriptStyle, font: font) + let scriptFont = font.copy(withSize: scriptFontSize) + + guard let display = MTTypesetter.createLineForMathList( + mathList, + font: scriptFont, + style: scriptStyle, + cramped: scriptCramped, + spaced: false + ) else { + return nil + } + + // If the result is a MTMathListDisplay with a single subdisplay, unwrap it + // This matches the behavior of the legacy typesetter + if display.subDisplays.count == 1 { + return display.subDisplays[0] + } + + return display + } + + /// Get the appropriate style for scripts + private func getScriptStyle() -> MTLineStyle { + switch style { + case .display, .text: + return .script + case .script, .scriptOfScript: + return .scriptOfScript + } + } + + // MARK: - Helper Methods + + /// Pre-render a simple math list without width constraints + /// Used for rendering content inside fractions, radicals, etc. + func renderMathList(_ mathList: MTMathList?, style renderStyle: MTLineStyle? = nil, cramped renderCramped: Bool? = nil) -> MTDisplay? { + guard let mathList = mathList else { return nil } + + let actualStyle = renderStyle ?? style + let actualCramped = renderCramped ?? cramped + + return MTTypesetter.createLineForMathList( + mathList, + font: font, + style: actualStyle, + cramped: actualCramped, + spaced: false + ) + } +} diff --git a/third-party/SwiftMath/Sources/MathRender/Tokenization/MTElementWidthCalculator.swift b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTElementWidthCalculator.swift new file mode 100644 index 0000000000..258eb768be --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTElementWidthCalculator.swift @@ -0,0 +1,165 @@ +// +// MTElementWidthCalculator.swift +// SwiftMath +// +// Created by Claude Code on 2025-12-16. +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation +import CoreText +import CoreGraphics + +/// Calculates widths for breakable elements with appropriate spacing +class MTElementWidthCalculator { + + // MARK: - Properties + + let font: MTFont + let style: MTLineStyle + + // MARK: - Initialization + + init(font: MTFont, style: MTLineStyle) { + self.font = font + self.style = style + } + + // MARK: - Text Width Measurement + + /// Measure width of simple text + func measureText(_ text: String) -> CGFloat { + guard !text.isEmpty else { return 0 } + + let attrString = NSAttributedString(string: text, attributes: [ + kCTFontAttributeName as NSAttributedString.Key: font.ctFont as Any + ]) + let line = CTLineCreateWithAttributedString(attrString as CFAttributedString) + return CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil)) + } + + // MARK: - Operator Width Measurement + + /// Measure width of operator with appropriate spacing + func measureOperator(_ op: String, type: MTMathAtomType) -> CGFloat { + let baseWidth = measureText(op) + let spacing = getOperatorSpacing(type) + return baseWidth + spacing + } + + /// Get spacing for an operator (both sides) + private func getOperatorSpacing(_ type: MTMathAtomType) -> CGFloat { + guard let mathTable = font.mathTable else { return 0 } + let muUnit = mathTable.muUnit + + switch type { + case .binaryOperator: + // Binary operators: 4mu on each side = 8mu total + return 2 * muUnit * 4 + + case .relation: + // Relations: 5mu on each side = 10mu total + return 2 * muUnit * 5 + + case .largeOperator: + // Large operators in inline mode: 1mu on each side + if style == .display || style == .text { + return 0 // In display mode, handled by MTLargeOpLimitsDisplay + } + return 2 * muUnit * 1 + + default: + return 0 + } + } + + // MARK: - Display Width Measurement + + /// Measure width of a pre-rendered display + func measureDisplay(_ display: MTDisplay) -> CGFloat { + return display.width + } + + // MARK: - Space Width Measurement + + /// Get width of explicit spacing command + func measureSpace(_ spaceType: MTMathAtomType) -> CGFloat { + guard let mathTable = font.mathTable else { return 0 } + let muUnit = mathTable.muUnit + + // Note: These are the explicit spacing commands in LaTeX + // \, = thin space (3mu) + // \: = medium space (4mu) + // \; = thick space (5mu) + // \quad = 1em + // \qquad = 2em + + switch spaceType { + case .space: + // Default space - context dependent + // For now, use thin space + return muUnit * 3 + default: + return 0 + } + } + + /// Measure explicit space value + func measureExplicitSpace(_ width: CGFloat) -> CGFloat { + return width + } + + // MARK: - Inter-element Spacing + + /// Get inter-element spacing between two atom types + func getInterElementSpacing(left: MTMathAtomType, right: MTMathAtomType) -> CGFloat { + let leftIndex = getInterElementSpaceArrayIndexForType(left, row: true) + let rightIndex = getInterElementSpaceArrayIndexForType(right, row: false) + let spaceArray = getInterElementSpaces()[Int(leftIndex)] + let spaceType = spaceArray[Int(rightIndex)] + + guard spaceType != .invalid else { + // Should not happen in well-formed math + return 0 + } + + let spaceMultiplier = getSpacingInMu(spaceType) + if spaceMultiplier > 0, let mathTable = font.mathTable { + return CGFloat(spaceMultiplier) * mathTable.muUnit + } + return 0 + } + + /// Get spacing multiplier in mu units + private func getSpacingInMu(_ spaceType: InterElementSpaceType) -> Int { + switch style { + case .display, .text: + switch spaceType { + case .none, .invalid: + return 0 + case .thin: + return 3 + case .nsThin, .nsMedium, .nsThick: + // ns = non-script, same as regular in display/text mode + switch spaceType { + case .nsThin: return 3 + case .nsMedium: return 4 + case .nsThick: return 5 + default: return 0 + } + } + + case .script, .scriptOfScript: + switch spaceType { + case .none, .invalid: + return 0 + case .thin: + return 3 + case .nsThin, .nsMedium, .nsThick: + // In script mode, ns types don't add space + return 0 + } + } + } +} diff --git a/third-party/SwiftMath/Sources/MathRender/Tokenization/MTLineFitter.swift b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTLineFitter.swift new file mode 100644 index 0000000000..5afdc36a2a --- /dev/null +++ b/third-party/SwiftMath/Sources/MathRender/Tokenization/MTLineFitter.swift @@ -0,0 +1,266 @@ +// +// MTLineFitter.swift +// SwiftMath +// +// Created by Claude Code on 2025-12-16. +// This software may be modified and distributed under the terms of the +// MIT license. See the LICENSE file for details. +// + +import Foundation +import CoreGraphics + +/// Fits breakable elements into lines respecting width constraints and break rules +class MTLineFitter { + + // MARK: - Properties + + let maxWidth: CGFloat + let margin: CGFloat + + // MARK: - Initialization + + init(maxWidth: CGFloat, margin: CGFloat = 0) { + self.maxWidth = maxWidth + self.margin = margin + } + + // MARK: - Line Fitting + + /// Fit elements into lines using greedy algorithm with backtracking + func fitLines(_ elements: [MTBreakableElement]) -> [[MTBreakableElement]] { + guard !elements.isEmpty else { return [] } + guard maxWidth > 0 else { return [elements] } // No width constraint + + let debugPunctuation = !"".isEmpty // Enable to debug line breaking issues + + if debugPunctuation { + print("\n=== MTLineFitter: fitting \(elements.count) elements, maxWidth=\(maxWidth) ===") + for (idx, elem) in elements.enumerated() { + if case .text(let t) = elem.content { + print("[\(idx)] '\(t)' breakBefore=\(elem.isBreakBefore) breakAfter=\(elem.isBreakAfter) width=\(elem.width)") + } + } + } + + var lines: [[MTBreakableElement]] = [[]] + var currentWidth: CGFloat = 0 + var i = 0 + + while i < elements.count { + let element = elements[i] + + if debugPunctuation, case .text(let t) = element.content { + print("\n Processing element[\(i)]: '\(t)' breakBefore=\(element.isBreakBefore)") + } + + // Handle grouped elements (base + scripts) + if let groupId = element.groupId { + let (groupElements, nextIndex) = collectGroup(elements, startIndex: i, groupId: groupId) + + // Calculate group width correctly for scripts + // Scripts overlap vertically, so width = max(script widths), not sum + let groupWidth = calculateGroupWidth(groupElements) + + // Check if group fits on current line + if !lines.last!.isEmpty && currentWidth + groupWidth > maxWidth - margin { + // Group doesn't fit - check if first element of group can start a new line + if groupElements.first?.isBreakBefore ?? true { + // Can start new line + lines.append([]) + currentWidth = 0 + } else { + // Cannot start new line - keep with previous line (allow overflow) + // This handles cases like punctuation after base+script groups + } + } + + // Add entire group to current line + lines[lines.count - 1].append(contentsOf: groupElements) + currentWidth += groupWidth + i = nextIndex + continue + } + + // Check if element fits on current line + if !lines.last!.isEmpty && currentWidth + element.width > maxWidth - margin { + if debugPunctuation, case .text = element.content { + print(" Doesn't fit (width=\(currentWidth) + \(element.width) > \(maxWidth)), current line has \(lines.last!.count) elements") + } + // Element doesn't fit - find best break point in current line + if let breakIndex = findBestBreak(in: lines[lines.count - 1]) { + if debugPunctuation { + print(" Found break at index \(breakIndex) out of \(lines.last!.count) elements") + if breakIndex < lines.last!.count { + if case .text(let t) = lines.last![breakIndex].content { + print(" Break at element: '\(t)'") + } + } + } + // Found a break point - move elements from breakIndex onward to next line + let moveElements = Array(lines[lines.count - 1][breakIndex...]) + let oldLine = Array(lines[lines.count - 1][.. Adding '\(t)' to new line (part of unbreakable sequence)") + } + lines[lines.count - 1].append(element) + currentWidth += element.width + i += 1 + continue + } else { + if debugPunctuation, case .text(let t) = element.content { + print(" -> Adding '\(t)' to new line (can start line)") + } + } + // Current element can start a line, will be added to new line below + } else { + // Should not happen if findBestBreak is correct, but handle gracefully + // Keep elements on current line (allow overflow) + lines[lines.count - 1].append(contentsOf: moveElements) + currentWidth += moveElements.reduce(0) { $0 + $1.width } + } + } else { + // No good break point found + // Check if current element can start a new line + if element.isBreakBefore { + // Element can start new line + lines.append([]) + currentWidth = 0 + } else { + // Element cannot start a new line (e.g., closing punctuation) + // Keep it on current line even if it causes overflow + // This respects punctuation rules over width constraints + lines[lines.count - 1].append(element) + currentWidth += element.width + i += 1 + continue + } + } + } + + // Add element to current line (may overflow if indivisible and too wide) + lines[lines.count - 1].append(element) + currentWidth += element.width + i += 1 + } + + let finalLines = lines.filter { !$0.isEmpty } + + if debugPunctuation { + print("\n=== Final lines: ===") + for (lineIdx, line) in finalLines.enumerated() { + print("Line \(lineIdx):") + for elem in line { + if case .text(let t) = elem.content { + print(" '\(t)'", terminator: "") + } + } + print() + } + } + + return finalLines + } + + // MARK: - Helper Methods + + /// Calculate the correct width for a group of elements (e.g., base + scripts) + /// Scripts overlap vertically, so the group width is not the sum of all widths + private func calculateGroupWidth(_ groupElements: [MTBreakableElement]) -> CGFloat { + // For grouped elements (base + scripts), just sum all widths + // The display generator will handle the actual positioning and overlap + // This is just for line fitting purposes + return groupElements.reduce(0) { $0 + $1.width } + } + + /// Collect all elements that share the same groupId + private func collectGroup(_ elements: [MTBreakableElement], startIndex: Int, groupId: UUID) -> ([MTBreakableElement], Int) { + var groupElements: [MTBreakableElement] = [] + var index = startIndex + + while index < elements.count && elements[index].groupId == groupId { + groupElements.append(elements[index]) + index += 1 + } + + return (groupElements, index) + } + + /// Find the best break point in a line + /// Returns the index where the break should occur (elements from this index move to next line) + private func findBestBreak(in line: [MTBreakableElement]) -> Int? { + var bestIndex: Int? = nil + var lowestPenalty = Int.max + + let debugBreak = !"".isEmpty // Enable to debug break point selection + + // Scan from right to left to prefer breaking later in the line + // Note: Skip the last element (idx == line.count - 1) because breaking after it + // would move 0 elements to the next line, which is pointless + for (idx, element) in line.enumerated().reversed() { + // Skip the last element - we need to move at least 1 element to the next line + if idx >= line.count - 1 { + continue + } + + // Can we break after this element? + let canBreakAfter = element.isBreakAfter + let penaltyAfter = element.penaltyAfter + + // Check if next element (which would move to new line) allows breaking before it + let canBreakBeforeNext = line[idx + 1].isBreakBefore + let penaltyBeforeNext = line[idx + 1].penaltyBefore + + // We can break here only if BOTH: + // 1. Current element allows breaking after it + // 2. Next element allows breaking before it + if canBreakAfter && canBreakBeforeNext { + let totalPenalty = max(penaltyAfter, penaltyBeforeNext) + if totalPenalty < lowestPenalty { + if debugBreak && idx < line.count - 1 { + let currText = if case .text(let t) = element.content { t } else { "?" } + let nextText = if case .text(let t) = line[idx + 1].content { t } else { "?" } + print(" Considering break: '\(currText)' | '\(nextText)' at idx=\(idx), penalty=\(totalPenalty)") + } + bestIndex = idx + 1 + lowestPenalty = totalPenalty + } + } + } + + if debugBreak { + print(" Best break: index=\(bestIndex ?? -1), penalty=\(lowestPenalty)") + } + + // Only return if we found an acceptable break point + if let index = bestIndex, lowestPenalty <= MTBreakPenalty.bad { + return index + } + + return nil + } + + /// Check if a line width exceeds the maximum + private func exceedsMaxWidth(_ width: CGFloat) -> Bool { + return width > maxWidth - margin + } +} diff --git a/third-party/SwiftSVG/BUILD b/third-party/SwiftSVG/BUILD new file mode 100644 index 0000000000..565ad88ec4 --- /dev/null +++ b/third-party/SwiftSVG/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SwiftSVG", + module_name = "SwiftSVG", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//third-party/XMLCoder", + "//third-party/Swift2D", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/third-party/SwiftSVG/Sources/Circle.swift b/third-party/SwiftSVG/Sources/Circle.swift new file mode 100644 index 0000000000..d42c8b9c73 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Circle.swift @@ -0,0 +1,93 @@ +import Swift2D +import XMLCoder + +/// Basic shape, used to draw circles based on a center point and a radius. +/// +/// The arc of a ‘circle’ element begins at the "3 o'clock" point on the radius and progresses towards the +/// "9 o'clock" point. The starting point and direction of the arc are affected by the user space transform +/// in the same manner as the geometry of the element. +/// +/// ## Documentation +/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle) +/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#CircleElement) +public struct Circle: Element { + + /// The x-axis coordinate of the center of the circle. + public var x: Double = 0.0 + /// The y-axis coordinate of the center of the circle. + public var y: Double = 0.0 + /// The radius of the circle. + public var r: Double = 0.0 + + // MARK: CoreAttributes + + public var id: String? + + // MARK: PresentationAttributes + + public var fillColor: String? + public var fillOpacity: Double? + public var fillRule: Fill.Rule? + public var strokeColor: String? + public var strokeWidth: Double? + public var strokeOpacity: Double? + public var strokeLineCap: Stroke.LineCap? + public var strokeLineJoin: Stroke.LineJoin? + public var strokeMiterLimit: Double? + public var transform: String? + + // MARK: StylingAttributes + + public var style: String? + + enum CodingKeys: String, CodingKey { + case x = "cx" + case y = "cy" + case r + case id + case fillColor = "fill" + case fillOpacity = "fill-opacity" + case fillRule = "fill-rule" + case strokeColor = "stroke" + case strokeWidth = "stroke-width" + case strokeOpacity = "stroke-opacity" + case strokeLineCap = "stroke-linecap" + case strokeLineJoin = "stroke-linejoin" + case strokeMiterLimit = "stroke-miterlimit" + case transform + case style + } + + public init() {} + + public init(x: Double, y: Double, r: Double) { + self.x = x + self.y = y + self.r = r + } +} + +extension Circle: CustomStringConvertible { + public var description: String { + let desc = "" + } +} + +extension Circle: DirectionalCommandRepresentable { + public func commands(clockwise: Bool) throws -> [Path.Command] { + EllipseProcessor(circle: self).commands(clockwise: clockwise) + } +} + +extension Circle: DynamicNodeDecoding { + public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding { + .attribute + } +} + +extension Circle: DynamicNodeEncoding { + public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding { + .attribute + } +} diff --git a/third-party/SwiftSVG/Sources/CommandRepresentable.swift b/third-party/SwiftSVG/Sources/CommandRepresentable.swift new file mode 100644 index 0000000000..f6bb79f05d --- /dev/null +++ b/third-party/SwiftSVG/Sources/CommandRepresentable.swift @@ -0,0 +1,15 @@ +/// Elements conforming to `CommandRepresentable` can be expressed in the form of `Path.Command`s. +public protocol CommandRepresentable { + func commands() throws -> [Path.Command] +} + +public protocol DirectionalCommandRepresentable: CommandRepresentable { + func commands(clockwise: Bool) throws -> [Path.Command] +} + +public extension DirectionalCommandRepresentable { + /// Defaults to anti/counter-clockwise commands. + func commands() throws -> [Path.Command] { + try commands(clockwise: false) + } +} diff --git a/third-party/SwiftSVG/Sources/Container.swift b/third-party/SwiftSVG/Sources/Container.swift new file mode 100644 index 0000000000..c90f0ea33a --- /dev/null +++ b/third-party/SwiftSVG/Sources/Container.swift @@ -0,0 +1,58 @@ +public protocol Container { + var circles: [Circle]? { get set } + var ellipses: [Ellipse]? { get set } + var groups: [Group]? { get set } + var lines: [Line]? { get set } + var paths: [Path]? { get set } + var polygons: [Polygon]? { get set } + var polylines: [Polyline]? { get set } + var rectangles: [Rectangle]? { get set } + var texts: [Text]? { get set } +} + +enum ContainerKeys: String, CodingKey { + case circles = "circle" + case ellipses = "ellipse" + case groups = "g" + case lines = "line" + case paths = "path" + case polylines = "polyline" + case polygons = "polygon" + case rectangles = "rect" + case texts = "text" +} + +public extension Container { + var containerDescription: String { + var contents: String = "" + + let circles = circles?.compactMap(\.description) ?? [] + circles.forEach { contents.append("\n\($0)") } + + let ellipses = ellipses?.compactMap(\.description) ?? [] + ellipses.forEach { contents.append("\n\($0)") } + + let groups = groups?.compactMap(\.description) ?? [] + groups.forEach { contents.append("\n\($0)") } + + let lines = lines?.compactMap(\.description) ?? [] + lines.forEach { contents.append("\n\($0)") } + + let paths = paths?.compactMap(\.description) ?? [] + paths.forEach { contents.append("\n\($0)") } + + let polylines = polylines?.compactMap(\.description) ?? [] + polylines.forEach { contents.append("\n\($0)") } + + let polygons = polygons?.compactMap(\.description) ?? [] + polygons.forEach { contents.append("\n\($0)") } + + let rectangles = rectangles?.compactMap(\.description) ?? [] + rectangles.forEach { contents.append("\n\($0)") } + + let texts = texts?.compactMap(\.description) ?? [] + texts.forEach { contents.append("\n\($0)") } + + return contents + } +} diff --git a/third-party/SwiftSVG/Sources/CoreAttributes.swift b/third-party/SwiftSVG/Sources/CoreAttributes.swift new file mode 100644 index 0000000000..df93043baa --- /dev/null +++ b/third-party/SwiftSVG/Sources/CoreAttributes.swift @@ -0,0 +1,17 @@ +public protocol CoreAttributes { + var id: String? { get set } +} + +enum CoreAttributesKeys: String, CodingKey { + case id +} + +public extension CoreAttributes { + var coreDescription: String { + if let id { + "\(CoreAttributesKeys.id.rawValue)=\"\(id)\"" + } else { + "" + } + } +} diff --git a/third-party/SwiftSVG/Sources/Element.swift b/third-party/SwiftSVG/Sources/Element.swift new file mode 100644 index 0000000000..d9a612ef8e --- /dev/null +++ b/third-party/SwiftSVG/Sources/Element.swift @@ -0,0 +1,46 @@ +public protocol Element: CoreAttributes, PresentationAttributes, StylingAttributes {} + +public extension Element { + var attributeDescription: String { + var components: [String] = [] + + if !coreDescription.isEmpty { + components.append(coreDescription) + } + if !presentationDescription.isEmpty { + components.append(presentationDescription) + } + if !stylingDescription.isEmpty { + components.append(stylingDescription) + } + + return components.joined(separator: " ") + } +} + +public extension CommandRepresentable where Self: Element { + /// When a `Path` is accessed on an element, the path that is returned should have the supplied transformations + /// applied. + /// + /// For instance, if + /// * a `Path.data` contains relative elements, + /// * and `transformations` contains a `.translate` + /// + /// Than the path created will not only use 'absolute' instructions, but those instructions will be modified to + /// include the required transformation. + func path(applying transformations: [Transformation] = []) throws -> Path { + var _transformations = transformations + _transformations.append(contentsOf: self.transformations) + + let commands = try commands().map { $0.applying(transformations: _transformations) } + + var path = Path(commands: commands) + path.fillColor = fillColor + path.fillOpacity = fillOpacity + path.strokeColor = strokeColor + path.strokeOpacity = strokeOpacity + path.strokeWidth = strokeWidth + + return path + } +} diff --git a/third-party/SwiftSVG/Sources/Ellipse.swift b/third-party/SwiftSVG/Sources/Ellipse.swift new file mode 100644 index 0000000000..bbabf36e61 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Ellipse.swift @@ -0,0 +1,93 @@ +import Swift2D +import XMLCoder + +/// SVG basic shape, used to create ellipses based on a center coordinate, and both their x and y radius. +/// +/// The arc of an ‘ellipse’ element begins at the "3 o'clock" point on the radius and progresses towards the +/// "9 o'clock" point. The starting point and direction of the arc are affected by the user space transform in the same +/// manner as the geometry of the element. +public struct Ellipse: Element { + + /// The x position of the ellipse. + public var x: Double = 0.0 + /// The y position of the ellipse. + public var y: Double = 0.0 + /// The radius of the ellipse on the x axis. + public var rx: Double = 0.0 + /// The radius of the ellipse on the y axis. + public var ry: Double = 0.0 + + // MARK: CoreAttributes + + public var id: String? + + // MARK: PresentationAttributes + + public var fillColor: String? + public var fillOpacity: Double? + public var fillRule: Fill.Rule? + public var strokeColor: String? + public var strokeWidth: Double? + public var strokeOpacity: Double? + public var strokeLineCap: Stroke.LineCap? + public var strokeLineJoin: Stroke.LineJoin? + public var strokeMiterLimit: Double? + public var transform: String? + + // MARK: StylingAttributes + + public var style: String? + + enum CodingKeys: String, CodingKey { + case x = "cx" + case y = "cy" + case rx + case ry + case id + case fillColor = "fill" + case fillOpacity = "fill-opacity" + case fillRule = "fill-rule" + case strokeColor = "stroke" + case strokeWidth = "stroke-width" + case strokeOpacity = "stroke-opacity" + case strokeLineCap = "stroke-linecap" + case strokeLineJoin = "stroke-linejoin" + case strokeMiterLimit = "stroke-miterlimit" + case transform + case style + } + + public init() {} + + public init(x: Double, y: Double, rx: Double, ry: Double) { + self.x = x + self.y = y + self.rx = rx + self.ry = ry + } +} + +extension Ellipse: CustomStringConvertible { + public var description: String { + let desc = "" + } +} + +extension Ellipse: DirectionalCommandRepresentable { + public func commands(clockwise: Bool) throws -> [Path.Command] { + EllipseProcessor(ellipse: self).commands(clockwise: clockwise) + } +} + +extension Ellipse: DynamicNodeDecoding { + public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding { + .attribute + } +} + +extension Ellipse: DynamicNodeEncoding { + public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding { + .attribute + } +} diff --git a/third-party/SwiftSVG/Sources/Extensions/Point+SwiftSVG.swift b/third-party/SwiftSVG/Sources/Extensions/Point+SwiftSVG.swift new file mode 100644 index 0000000000..0c789bcae3 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Extensions/Point+SwiftSVG.swift @@ -0,0 +1,37 @@ +import Swift2D + +extension Point { + static var nan: Point { + Point(x: Double.nan, y: Double.nan) + } + + var hasNaN: Bool { + x.isNaN || y.isNaN + } + + /// Returns a copy of the instance with the **x** value replaced with the provided value. + func with(x value: Double) -> Point { + Point(x: value, y: y) + } + + /// Returns a copy of the instance with the **y** value replaced with the provided value. + func with(y value: Double) -> Point { + Point(x: x, y: value) + } + + /// Adjusts the **x** value by the provided amount. + /// + /// This will explicitly check for `.isNaN`, and if encountered, will simply + /// use the provided value. + func adjusting(x value: Double) -> Point { + (x.isNaN) ? with(x: value) : with(x: x + value) + } + + /// Adjusts the **y** value by the provided amount. + /// + /// This will explicitly check for `.isNaN`, and if encountered, will simply + /// use the provided value. + func adjusting(y value: Double) -> Point { + (y.isNaN) ? with(y: value) : with(y: y + value) + } +} diff --git a/third-party/SwiftSVG/Sources/Fill.swift b/third-party/SwiftSVG/Sources/Fill.swift new file mode 100644 index 0000000000..608192aa29 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Fill.swift @@ -0,0 +1,44 @@ +import Swift2D + +public struct Fill { + + public var color: String? + public var opacity: Double? + public var rule: Rule = .nonZero + + public init() {} + + /// Presentation attribute defining the algorithm to use to determine the inside part of a shape. + /// + /// The default `Rule` is `.nonzero`. + public enum Rule: String, Sendable, Codable, CaseIterable { + /// The value evenodd determines the "insideness" of a point in the shape by drawing a ray from that point to + /// infinity in any direction and counting the number of path segments from the given shape that the ray + /// crosses. If this number is odd, the point is inside; if even, the point is outside. + case evenOdd = "evenodd" + /// The value nonzero determines the "insideness" of a point in the shape by drawing a ray from that point to + /// infinity in any direction, and then examining the places where a segment of the shape crosses the ray. + /// Starting with a count of zero, add one each time a path segment crosses the ray from left to right and + /// subtract one each time a path segment crosses the ray from right to left. After counting the crossings, if + /// the result is zero then the point is outside the path. Otherwise, it is inside. + case nonZero = "nonzero" + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(String.self) + guard let rule = Rule(rawValue: rawValue) else { + print("Attempts to decode Fill.Rule with rawValue: '\(rawValue)'") + self = .nonZero + return + } + + self = rule + } + } +} + +extension Fill.Rule: CustomStringConvertible { + public var description: String { + rawValue + } +} diff --git a/third-party/SwiftSVG/Sources/Group.swift b/third-party/SwiftSVG/Sources/Group.swift new file mode 100644 index 0000000000..c95d2d0ad5 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Group.swift @@ -0,0 +1,152 @@ +import XMLCoder + +/// A container used to group other SVG elements. +/// +/// Grouping constructs, when used in conjunction with the ‘desc’ and ‘title’ elements, provide information +/// about document structure and semantics. +/// +/// ## Documentation +/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g) +/// | [W3](https://www.w3.org/TR/SVG11/struct.html#Groups) +public struct Group: Container, Element { + + // Container + public var circles: [Circle]? + public var ellipses: [Ellipse]? + public var groups: [Group]? + public var lines: [Line]? + public var paths: [Path]? + public var polygons: [Polygon]? + public var polylines: [Polyline]? + public var rectangles: [Rectangle]? + public var texts: [Text]? + + // MARK: CoreAttributes + + public var id: String? + public var title: String? + public var desc: String? + + // MARK: PresentationAttributes + + public var fillColor: String? + public var fillOpacity: Double? + public var fillRule: Fill.Rule? + public var strokeColor: String? + public var strokeWidth: Double? + public var strokeOpacity: Double? + public var strokeLineCap: Stroke.LineCap? + public var strokeLineJoin: Stroke.LineJoin? + public var strokeMiterLimit: Double? + public var transform: String? + + // MARK: StylingAttributes + + public var style: String? + + enum CodingKeys: String, CodingKey { + case circles = "circle" + case ellipses = "ellipse" + case groups = "g" + case lines = "line" + case paths = "path" + case polylines = "polyline" + case polygons = "polygon" + case rectangles = "rect" + case texts = "text" + case id + case title + case desc + case fillColor = "fill" + case fillOpacity = "fill-opacity" + case fillRule = "fill-rule" + case strokeColor = "stroke" + case strokeWidth = "stroke-width" + case strokeOpacity = "stroke-opacity" + case strokeLineCap = "stroke-linecap" + case strokeLineJoin = "stroke-linejoin" + case strokeMiterLimit = "stroke-miterlimit" + case transform + case style + } + + public init() {} + + /// A representation of all the sub-`Path`s in the `Group`. + public func subpaths(applying transformations: [Transformation] = []) throws -> [Path] { + var _transformations = transformations + _transformations.append(contentsOf: self.transformations) + + var output: [Path] = [] + + if let circles { + try output.append(contentsOf: circles.compactMap { try $0.path(applying: _transformations) }) + } + + if let ellipses { + try output.append(contentsOf: ellipses.compactMap { try $0.path(applying: _transformations) }) + } + + if let rectangles { + try output.append(contentsOf: rectangles.compactMap { try $0.path(applying: _transformations) }) + } + + if let polygons { + try output.append(contentsOf: polygons.compactMap { try $0.path(applying: _transformations) }) + } + + if let polylines { + try output.append(contentsOf: polylines.compactMap { try $0.path(applying: _transformations) }) + } + + if let paths { + try output.append(contentsOf: paths.map { try $0.path(applying: _transformations) }) + } + + if let groups { + try groups.forEach { + try output.append(contentsOf: $0.subpaths(applying: _transformations)) + } + } + + return output + } +} + +extension Group: CustomStringConvertible { + public var description: String { + var contents: String = "" + + if let title { + contents.append("\n\(title)") + } + + if let desc { + contents.append("\n\(desc)") + } + + contents.append(containerDescription) + + return "\(contents)\n" + } +} + +extension Group: DynamicNodeDecoding { + public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding { + if let _ = ContainerKeys(stringValue: key.stringValue) { + return .element + } + + return .attribute + } +} + +extension Group: DynamicNodeEncoding { + public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding { + if let _ = ContainerKeys(stringValue: key.stringValue) { + return .element + } + + return .attribute + } +} diff --git a/third-party/SwiftSVG/Sources/Internal/EllipseProcessor.swift b/third-party/SwiftSVG/Sources/Internal/EllipseProcessor.swift new file mode 100644 index 0000000000..409c2a7163 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Internal/EllipseProcessor.swift @@ -0,0 +1,88 @@ +import Foundation +import Swift2D + +struct EllipseProcessor { + + let x: Double + let y: Double + let rx: Double + let ry: Double + + /// The _optimal_ offset for control points when representing a + /// circle/ellipse as 4 bezier curves. + /// + /// [Stack Overflow](https://stackoverflow.com/questions/1734745/how-to-create-circle-with-bézier-curves) + static func controlPointOffset(_ radius: Double) -> Double { + (Double(4.0 / 3.0) * tan(Double.pi / 8.0)) * radius + } + + init(ellipse: Ellipse) { + x = ellipse.x + y = ellipse.y + rx = ellipse.rx + ry = ellipse.ry + } + + init(circle: Circle) { + x = circle.x + y = circle.y + rx = circle.r + ry = circle.r + } + + func commands(clockwise: Bool) -> [Path.Command] { + var commands: [Path.Command] = [] + + let xOffset = Self.controlPointOffset(rx) + let yOffset = Self.controlPointOffset(ry) + + let zero = Point(x: x + rx, y: y) + let ninety = Point(x: x, y: y - ry) + let oneEighty = Point(x: x - rx, y: y) + let twoSeventy = Point(x: x, y: y + ry) + + var cp1: Point = .zero + var cp2: Point = .zero + + // Starting at degree 0 (the right most point) + commands.append(.moveTo(point: zero)) + + if clockwise { + cp1 = Point(x: zero.x, y: zero.y + yOffset) + cp2 = Point(x: twoSeventy.x + xOffset, y: twoSeventy.y) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: twoSeventy)) + + cp1 = Point(x: twoSeventy.x - xOffset, y: twoSeventy.y) + cp2 = Point(x: oneEighty.x, y: oneEighty.y + yOffset) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: oneEighty)) + + cp1 = Point(x: oneEighty.x, y: oneEighty.y - yOffset) + cp2 = Point(x: ninety.x - xOffset, y: ninety.y) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: ninety)) + + cp1 = Point(x: ninety.x + xOffset, y: ninety.y) + cp2 = Point(x: zero.x, y: zero.y - yOffset) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: zero)) + } else { + cp1 = Point(x: zero.x, y: zero.y - yOffset) + cp2 = Point(x: ninety.x + xOffset, y: ninety.y) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: ninety)) + + cp1 = Point(x: ninety.x - xOffset, y: ninety.y) + cp2 = Point(x: oneEighty.x, y: oneEighty.y - yOffset) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: oneEighty)) + + cp1 = Point(x: oneEighty.x, y: oneEighty.y + yOffset) + cp2 = Point(x: twoSeventy.x - xOffset, y: twoSeventy.y) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: twoSeventy)) + + cp1 = Point(x: twoSeventy.x + xOffset, y: twoSeventy.y) + cp2 = Point(x: zero.x, y: zero.y + yOffset) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: zero)) + } + + commands.append(.closePath) + + return commands + } +} diff --git a/third-party/SwiftSVG/Sources/Internal/PathProcessor.swift b/third-party/SwiftSVG/Sources/Internal/PathProcessor.swift new file mode 100644 index 0000000000..25c808c4d7 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Internal/PathProcessor.swift @@ -0,0 +1,12 @@ +import Foundation + +struct PathProcessor { + + let data: String + + func commands() throws -> [Path.Command] { + let parser = Path.ComponentParser() + let components = try Path.Component.components(from: data) + return try parser.parse(components) + } +} diff --git a/third-party/SwiftSVG/Sources/Internal/PolygonProcressor.swift b/third-party/SwiftSVG/Sources/Internal/PolygonProcressor.swift new file mode 100644 index 0000000000..d05ab1a408 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Internal/PolygonProcressor.swift @@ -0,0 +1,52 @@ +import Foundation +import Swift2D + +struct PolygonProcessor { + + let points: String + + func commands() throws -> [Path.Command] { + let pairs = points.components(separatedBy: " ") + let components = pairs.flatMap { $0.components(separatedBy: ",") } + guard components.count > 0 else { + return [] + } + + guard components.count % 2 == 0 else { + // An odd number of components means that parsing probably failed + return [] + } + + var commands: [Path.Command] = [] + + var firstValue: Bool = true + for (idx, component) in components.enumerated() { + guard let _value = Double(component) else { + return commands + } + + let value = Double(_value) + + if firstValue { + if idx == 0 { + commands.append(.moveTo(point: Point(x: value, y: .nan))) + } else { + commands.append(.lineTo(point: Point(x: value, y: .nan))) + } + firstValue = false + } else { + let count = commands.count + guard let modified = try? commands.last?.adjustingArgument(at: 1, by: value) else { + return commands + } + + commands[count - 1] = modified + firstValue = true + } + } + + commands.append(.closePath) + + return commands + } +} diff --git a/third-party/SwiftSVG/Sources/Internal/PolylineProcessor.swift b/third-party/SwiftSVG/Sources/Internal/PolylineProcessor.swift new file mode 100644 index 0000000000..a909c4ed3f --- /dev/null +++ b/third-party/SwiftSVG/Sources/Internal/PolylineProcessor.swift @@ -0,0 +1,54 @@ +import Foundation +import Swift2D + +struct PolylineProcessor { + + let points: String + + func commands() throws -> [Path.Command] { + let pairs = points.components(separatedBy: " ") + let components = pairs.flatMap { $0.components(separatedBy: ",") } + let values = components.compactMap { Double($0) }.map { Double($0) } + + guard values.count > 2 else { + // More than just a starting point is required. + return [] + } + + guard values.count % 2 == 0 else { + // An odd number of components means that parsing probably failed + return [] + } + + var commands: [Path.Command] = [] + + let move = values.prefix(upTo: 2) + let segments = values.suffix(from: 2) + + commands.append(.moveTo(point: Point(x: move[0], y: move[1]))) + + var _value: Double = .nan + for value in segments { + if _value.isNaN { + _value = value + } else { + commands.append(.lineTo(point: Point(x: _value, y: value))) + _value = .nan + } + } + + let reversedSegments = segments.dropLast(2).reversed() + for value in reversedSegments { + if _value.isNaN { + _value = value + } else { + commands.append(.lineTo(point: Point(x: _value, y: value))) + _value = .nan + } + } + + commands.append(.closePath) + + return commands + } +} diff --git a/third-party/SwiftSVG/Sources/Internal/RectangleProcessor.swift b/third-party/SwiftSVG/Sources/Internal/RectangleProcessor.swift new file mode 100644 index 0000000000..2b93440834 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Internal/RectangleProcessor.swift @@ -0,0 +1,175 @@ +import Swift2D + +struct RectangleProcessor { + + let rectangle: Rectangle + + func commands(clockwise: Bool) -> [Path.Command] { + var rx = rectangle.rx + var ry = rectangle.ry + + if let _rx = rx, _rx > (rectangle.width / 2.0) { + rx = rectangle.width / 2.0 + } + + if let _ry = ry, _ry > (rectangle.height / 2.0) { + ry = rectangle.height / 2.0 + } + + var commands: [Path.Command] = [] + + switch (rx, ry) { + case (.some(let radiusX), .some(let radiusY)) where radiusX != radiusY: + // Use Cubic Bezier Curve to form rounded corners + // TODO: Verify that the control points are right + + var cp1: Point = .zero + var cp2: Point = .zero + var point: Point = Point(x: rectangle.x + radiusX, y: rectangle.y) + + commands.append(.moveTo(point: point)) + + if clockwise { + point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y) + commands.append(.lineTo(point: point)) + + cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y) + cp2 = cp1 + point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radiusY) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) + + point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radiusY) + commands.append(.lineTo(point: point)) + + cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height) + cp2 = cp1 + point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y + rectangle.height) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) + + point = .init(x: rectangle.x + radiusX, y: rectangle.y + rectangle.height) + commands.append(.lineTo(point: point)) + + cp1 = .init(x: rectangle.x, y: rectangle.y + rectangle.height) + cp2 = cp1 + point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radiusY) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) + + point = .init(x: rectangle.x, y: rectangle.y + radiusY) + commands.append(.lineTo(point: point)) + + cp1 = .init(x: rectangle.x, y: rectangle.y) + cp2 = cp1 + point = .init(x: rectangle.x + radiusX, y: rectangle.y) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) + } else { + cp1 = .init(x: rectangle.x, y: rectangle.y) + cp2 = cp1 + point = .init(x: rectangle.x, y: rectangle.y + radiusY) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) + + point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radiusY) + commands.append(.lineTo(point: point)) + + cp1 = .init(x: rectangle.x, y: rectangle.y + rectangle.height) + cp2 = cp1 + point = .init(x: rectangle.x + radiusX, y: rectangle.y + rectangle.height) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) + + point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y + rectangle.height) + commands.append(.lineTo(point: point)) + + cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height) + cp2 = cp1 + point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radiusY) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) + + point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radiusY) + commands.append(.lineTo(point: point)) + + cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y) + cp2 = cp1 + point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y) + commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) + } + case (.some(let radius), .none), (.none, .some(let radius)), (.some(let radius), _): + // use Quadratic Bezier Curve to form rounded corners + + var cp: Point = .zero + var point: Point = Point(x: rectangle.x + radius, y: rectangle.y) + + commands.append(.moveTo(point: point)) + + if clockwise { + point = .init(x: (rectangle.x + rectangle.width) - radius, y: rectangle.y) + commands.append(.lineTo(point: point)) + + cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y) + point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radius) + commands.append(.quadraticBezierCurve(cp: cp, point: point)) + + point = .init(x: rectangle.x + rectangle.width, y: (rectangle.y + rectangle.height) - radius) + commands.append(.lineTo(point: point)) + + cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height) + point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y + rectangle.height) + commands.append(.quadraticBezierCurve(cp: cp, point: point)) + + point = .init(x: rectangle.x + radius, y: rectangle.y + rectangle.height) + commands.append(.lineTo(point: point)) + + cp = .init(x: rectangle.x, y: rectangle.y + rectangle.height) + point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radius) + commands.append(.quadraticBezierCurve(cp: cp, point: point)) + + point = .init(x: rectangle.x, y: rectangle.y + radius) + commands.append(.lineTo(point: point)) + + cp = .init(x: rectangle.x, y: rectangle.y) + point = .init(x: rectangle.x + radius, y: rectangle.y) + commands.append(.quadraticBezierCurve(cp: cp, point: point)) + } else { + cp = .init(x: rectangle.x, y: rectangle.y) + point = .init(x: rectangle.x, y: rectangle.y + radius) + commands.append(.quadraticBezierCurve(cp: cp, point: point)) + + point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radius) + commands.append(.lineTo(point: point)) + + cp = .init(x: rectangle.x, y: rectangle.y + rectangle.height) + point = .init(x: rectangle.x + radius, y: rectangle.y + rectangle.height) + commands.append(.quadraticBezierCurve(cp: cp, point: point)) + + point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y + rectangle.height) + commands.append(.lineTo(point: point)) + + cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height) + point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radius) + commands.append(.quadraticBezierCurve(cp: cp, point: point)) + + point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radius) + commands.append(.lineTo(point: point)) + + cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y) + point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y) + commands.append(.quadraticBezierCurve(cp: cp, point: point)) + } + case (.none, .none): + // draw three line segments. + commands.append(.moveTo(point: Point(x: rectangle.x, y: rectangle.y))) + + if clockwise { + commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y))) + commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height))) + commands.append(.lineTo(point: Point(x: rectangle.x, y: rectangle.y + rectangle.height))) + } else { + commands.append(.lineTo(point: Point(x: rectangle.x, y: rectangle.y + rectangle.height))) + commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height))) + commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y))) + } + } + + commands.append(.closePath) + + return commands + } +} diff --git a/third-party/SwiftSVG/Sources/Line.swift b/third-party/SwiftSVG/Sources/Line.swift new file mode 100644 index 0000000000..08ee77488f --- /dev/null +++ b/third-party/SwiftSVG/Sources/Line.swift @@ -0,0 +1,98 @@ +import Swift2D +import XMLCoder + +/// SVG basic shape used to create a line connecting two points. +/// +/// ## Documentation +/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line) +/// | [W3](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line) +public struct Line: Element { + + /// Defines the x-axis coordinate of the line starting point. + public var x1: Double = 0.0 + /// Defines the x-axis coordinate of the line ending point. + public var y1: Double = 0.0 + /// Defines the y-axis coordinate of the line starting point. + public var x2: Double = 0.0 + /// Defines the y-axis coordinate of the line ending point. + public var y2: Double = 0.0 + + // MARK: CoreAttributes + + public var id: String? + + // MARK: PresentationAttributes + + public var fillColor: String? + public var fillOpacity: Double? + public var fillRule: Fill.Rule? + public var strokeColor: String? + public var strokeWidth: Double? + public var strokeOpacity: Double? + public var strokeLineCap: Stroke.LineCap? + public var strokeLineJoin: Stroke.LineJoin? + public var strokeMiterLimit: Double? + public var transform: String? + + // MARK: StylingAttributes + + public var style: String? + + enum CodingKeys: String, CodingKey { + case x1 + case y1 + case x2 + case y2 + case id + case fillColor = "fill" + case fillOpacity = "fill-opacity" + case fillRule = "fill-rule" + case strokeColor = "stroke" + case strokeWidth = "stroke-width" + case strokeOpacity = "stroke-opacity" + case strokeLineCap = "stroke-linecap" + case strokeLineJoin = "stroke-linejoin" + case strokeMiterLimit = "stroke-miterlimit" + case transform + case style + } + + public init() {} + + public init(x1: Double, y1: Double, x2: Double, y2: Double) { + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + } +} + +extension Line: CommandRepresentable { + public func commands() throws -> [Path.Command] { + [ + .moveTo(point: Point(x: x1, y: y1)), + .lineTo(point: Point(x: x2, y: y2)), + .lineTo(point: Point(x: x1, y: y1)), + .closePath, + ] + } +} + +extension Line: CustomStringConvertible { + public var description: String { + let desc = "" + } +} + +extension Line: DynamicNodeDecoding { + public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding { + .attribute + } +} + +extension Line: DynamicNodeEncoding { + public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding { + .attribute + } +} diff --git a/third-party/SwiftSVG/Sources/Path.Command.swift b/third-party/SwiftSVG/Sources/Path.Command.swift new file mode 100644 index 0000000000..e0fb49a729 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Path.Command.swift @@ -0,0 +1,276 @@ +import Foundation +import Swift2D + +public extension Path { + /// Path commands are instructions that define a path to be drawn. + /// + /// Each command is composed of a command letter and numbers that represent the command parameters. + enum Command: Equatable, Sendable, CustomStringConvertible { + /// Moves the current drawing point + case moveTo(point: Point) + /// Draw a straight line from the current point to the point provided + case lineTo(point: Point) + /// Draw a smooth curve using three points (+ origin) + case cubicBezierCurve(cp1: Point, cp2: Point, point: Point) + /// Draw a smooth curve using two points (+ origin) + case quadraticBezierCurve(cp: Point, point: Point) + /// Draw a curve defined as a portion of an ellipse + case ellipticalArcCurve(rx: Double, ry: Double, angle: Double, largeArc: Bool, clockwise: Bool, point: Point) + /// ClosePath instructions draw a straight line from the current position to the first point in the path. + case closePath + + public enum Prefix: Character, CaseIterable { + case move = "M" + case relativeMove = "m" + case line = "L" + case relativeLine = "l" + case horizontalLine = "H" + case relativeHorizontalLine = "h" + case verticalLine = "V" + case relativeVerticalLine = "v" + case cubicBezierCurve = "C" + case relativeCubicBezierCurve = "c" + case smoothCubicBezierCurve = "S" + case relativeSmoothCubicBezierCurve = "s" + case quadraticBezierCurve = "Q" + case relativeQuadraticBezierCurve = "q" + case smoothQuadraticBezierCurve = "T" + case relativeSmoothQuadraticBezierCurve = "t" + case ellipticalArcCurve = "A" + case relativeEllipticalArcCurve = "a" + case close = "Z" + case relativeClose = "z" + + public static var characterSet: CharacterSet { + CharacterSet(charactersIn: allCases.map { String($0.rawValue) }.joined()) + } + } + + public enum Coordinates { + case absolute + case relative + } + + public enum Error: Swift.Error { + case message(String) + case invalidAdjustment(Path.Command) + case invalidArgumentPosition(Int, Path.Command) + case invalidRelativeCommand + } + + public var description: String { + switch self { + case .moveTo(let point): + return "\(Prefix.move.rawValue)\(point.x),\(point.y)" + case .lineTo(let point): + return "\(Prefix.line.rawValue)\(point.x),\(point.y)" + case .cubicBezierCurve(let cp1, let cp2, let point): + return "\(Prefix.cubicBezierCurve.rawValue)\(cp1.x),\(cp1.y) \(cp2.x),\(cp2.y) \(point.x),\(point.y)" + case .quadraticBezierCurve(let cp, let point): + return "\(Prefix.quadraticBezierCurve.rawValue)\(cp.x),\(cp.y) \(point.x),\(point.y)" + case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point): + let la = largeArc ? 1 : 0 + let cw = clockwise ? 1 : 0 + return "\(Prefix.ellipticalArcCurve.rawValue)\(rx) \(ry) \(angle) \(la) \(cw) \(point.x) \(point.y)" + case .closePath: + return "\(Prefix.close.rawValue)" + } + } + + /// The primary point that dictates the commands action. + public var point: Point { + switch self { + case .moveTo(let point): point + case .lineTo(let point): point + case .cubicBezierCurve(_, _, let point): point + case .quadraticBezierCurve(_, let point): point + case .ellipticalArcCurve(_, _, _, _, _, let point): point + case .closePath: .zero + } + } + } +} + +public extension Path.Command { + /// Applies the provided `Transformation` to the instances values. + func applying(transformation: Transformation) -> Path.Command { + switch transformation { + case .translate(let x, let y): + switch self { + case .moveTo(let point): + let _point = point.adjusting(x: x).adjusting(y: y) + return .moveTo(point: _point) + case .lineTo(let point): + let _point = point.adjusting(x: x).adjusting(y: y) + return .lineTo(point: _point) + case .cubicBezierCurve(let cp1, let cp2, let point): + let _cp1 = cp1.adjusting(x: x).adjusting(y: y) + let _cp2 = cp2.adjusting(x: x).adjusting(y: y) + let _point = point.adjusting(x: x).adjusting(y: y) + return .cubicBezierCurve(cp1: _cp1, cp2: _cp2, point: _point) + case .quadraticBezierCurve(let cp, let point): + let _cp = cp.adjusting(x: x).adjusting(y: y) + let _point = point.adjusting(x: x).adjusting(y: y) + return .quadraticBezierCurve(cp: _cp, point: _point) + case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point): + let _point = point.adjusting(x: x).adjusting(y: y) + return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: _point) + case .closePath: + return self + } + case .matrix: + // TODO: What should occur here? + return self + } + } + + /// Applies multiple transformations in the order they are specified. + func applying(transformations: [Transformation]) -> Path.Command { + var command = self + + for transformation in transformations { + command = command.applying(transformation: transformation) + } + + return command + } +} + +extension Path.Command { + /// Determines if all values are provided (i.e. !.isNaN) + var isComplete: Bool { + switch self { + case .moveTo(let point), .lineTo(let point): + !point.hasNaN + case .cubicBezierCurve(let cp1, let cp2, let point): + !cp1.hasNaN && !cp2.hasNaN && !point.hasNaN + case .quadraticBezierCurve(let cp, let point): + !cp.hasNaN && !point.hasNaN + case .ellipticalArcCurve(let rx, let ry, let angle, _, _, let point): + !rx.isNaN && !ry.isNaN && !angle.isNaN && !point.hasNaN + case .closePath: + true + } + } + + /// The last control point used in drawing the path. + /// + /// Only valid for curves. + var lastControlPoint: Point? { + switch self { + case .cubicBezierCurve(_, let cp2, _): + cp2 + case .quadraticBezierCurve(let cp, _): + cp + default: + nil + } + } + + /// A mirror representation of `lastControlPoint`. + var lastControlPointMirror: Point? { + guard let cp = lastControlPoint else { + return nil + } + + return Point(x: point.x + (point.x - cp.x), y: point.y + (point.y - cp.y)) + } + + /// The total number of argument values the command requires. + var arguments: Int { + switch self { + case .moveTo: 2 + case .lineTo: 2 + case .cubicBezierCurve: 6 + case .quadraticBezierCurve: 4 + case .ellipticalArcCurve: 7 + case .closePath: 0 + } + } + + /// Adjusts a Command argument by a specified amount. + /// + /// A `Point` consumes two positions. So, in the example `.quadraticBezierCurve(cp: .zero, point: .zero)`: + /// * position 0 = Control Point X + /// * position 1 = Control Point Y + /// * position 2 = Point X + /// * position 3 = Point Y + /// + /// - parameter position: The index of the argument parameter to adjust. + /// - parameter value: The value to add to the existing value. If the current value equal `.isNaN`, than the + /// supplied value is used as-is. + /// - throws: `Path.Command.Error` + func adjustingArgument(at position: Int, by value: Double) throws -> Path.Command { + switch self { + case .moveTo(let point): + switch position { + case 0: + return .moveTo(point: point.adjusting(x: value)) + case 1: + return .moveTo(point: point.adjusting(y: value)) + default: + throw Path.Command.Error.invalidArgumentPosition(position, self) + } + case .lineTo(let point): + switch position { + case 0: + return .lineTo(point: point.adjusting(x: value)) + case 1: + return .lineTo(point: point.adjusting(y: value)) + default: + throw Path.Command.Error.invalidArgumentPosition(position, self) + } + case .cubicBezierCurve(let cp1, let cp2, let point): + switch position { + case 0: + return .cubicBezierCurve(cp1: cp1.adjusting(x: value), cp2: cp2, point: point) + case 1: + return .cubicBezierCurve(cp1: cp1.adjusting(y: value), cp2: cp2, point: point) + case 2: + return .cubicBezierCurve(cp1: cp1, cp2: cp2.adjusting(x: value), point: point) + case 3: + return .cubicBezierCurve(cp1: cp1, cp2: cp2.adjusting(y: value), point: point) + case 4: + return .cubicBezierCurve(cp1: cp1, cp2: cp2, point: point.adjusting(x: value)) + case 5: + return .cubicBezierCurve(cp1: cp1, cp2: cp2, point: point.adjusting(y: value)) + default: + throw Path.Command.Error.invalidArgumentPosition(position, self) + } + case .quadraticBezierCurve(let cp, let point): + switch position { + case 0: + return .quadraticBezierCurve(cp: cp.adjusting(x: value), point: point) + case 1: + return .quadraticBezierCurve(cp: cp.adjusting(y: value), point: point) + case 2: + return .quadraticBezierCurve(cp: cp, point: point.adjusting(x: value)) + case 3: + return .quadraticBezierCurve(cp: cp, point: point.adjusting(y: value)) + default: + throw Path.Command.Error.invalidArgumentPosition(position, self) + } + case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point): + switch position { + case 0: + return .ellipticalArcCurve(rx: value, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point) + case 1: + return .ellipticalArcCurve(rx: rx, ry: value, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point) + case 2: + return .ellipticalArcCurve(rx: rx, ry: ry, angle: value, largeArc: largeArc, clockwise: clockwise, point: point) + case 3: + return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: !value.isZero, clockwise: clockwise, point: point) + case 4: + return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: !value.isZero, point: point) + case 5: + return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point.adjusting(x: value)) + case 6: + return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point.adjusting(y: value)) + default: + throw Path.Command.Error.invalidArgumentPosition(position, self) + } + case .closePath: + throw Path.Command.Error.invalidAdjustment(self) + } + } +} diff --git a/third-party/SwiftSVG/Sources/Path.Component.swift b/third-party/SwiftSVG/Sources/Path.Component.swift new file mode 100644 index 0000000000..6c02251142 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Path.Component.swift @@ -0,0 +1,98 @@ +import Foundation + +public extension Path { + /// A unit of a SVG path data string. + enum Component { + case prefix(Command.Prefix) + case value(Double) + + /// Interprets a `Path` `data` attribute into individual `Component`s for command processing. + public static func components(from data: String) throws -> [Component] { + var blocks: [String] = [] + var block: String = "" + + for scalar in data.unicodeScalars { + if scalar == "e" { + // Account for exponential value notation. + block.append(String(scalar)) + continue + } + + if CharacterSet.letters.contains(scalar) { + if !block.isEmpty { + blocks.append(block) + block = "" + } + + blocks.append(String(scalar)) + continue + } + + if CharacterSet.whitespaces.contains(scalar) { + if !block.isEmpty { + blocks.append(block) + block = "" + } + + continue + } + + if CharacterSet(charactersIn: ",").contains(scalar) { + if !block.isEmpty { + blocks.append(block) + block = "" + } + + continue + } + + if CharacterSet(charactersIn: "-").contains(scalar) { + if !block.isEmpty, block.last != "e" { + // Again, account for exponential values. + blocks.append(block) + block = "" + } + + block.append(String(scalar)) + continue + } + + if CharacterSet(charactersIn: ".").contains(scalar) { + if block.contains(".") { + // Already decimal value, this is a new value + blocks.append(block) + block = "" + } + + block.append(String(scalar)) + continue + } + + if CharacterSet.decimalDigits.contains(scalar) { + block.append(String(scalar)) + continue + } + + print("Unhandled Character: \(scalar)") + } + + if !block.isEmpty { + blocks.append(block) + block = "" + } + + return blocks + .filter { !$0.isEmpty } + .compactMap { + if let prefix = Path.Command.Prefix(rawValue: $0.first!) { + .prefix(prefix) + } else if let value = Double($0) { + .value(value) + } else { + // throw in the future? + nil + } + } + } + } +} diff --git a/third-party/SwiftSVG/Sources/Path.ComponentParser.swift b/third-party/SwiftSVG/Sources/Path.ComponentParser.swift new file mode 100644 index 0000000000..c64c682a6b --- /dev/null +++ b/third-party/SwiftSVG/Sources/Path.ComponentParser.swift @@ -0,0 +1,275 @@ +import Swift2D + +public extension Path { + /// Utility used to construct a collection of `Path.Command` from a collection of `Path.Component`. + class ComponentParser { + /// The command currently being built + private var command: Path.Command? + /// Coordinate system being used + private var coordinates: Path.Command.Coordinates = .absolute + /// The argument position of the _command_ to be processed. + private var position: Int = 0 + /// Indicates that only a single value will be processed on the next component pass. + private var singleValue: Bool = false + /// The originating coordinates of the path. + private var pathOrigin: Point = .nan + /// The last point as processed by the parser. + private var currentPoint: Point = .zero + + public init() {} + + public func parse(_ components: [Path.Component]) throws -> [Path.Command] { + var commands: [Path.Command] = [] + + try components.forEach { component in + if let command = try parse(component, lastCommand: commands.last) { + commands.append(command) + } + } + + return commands + } + + private func parse(_ component: Path.Component, lastCommand: Path.Command?) throws -> Path.Command? { + switch component { + case .prefix(let prefix): + setup(prefix: prefix, lastCommand: lastCommand) + case .value(let value): + try process(value: value, lastCommand: lastCommand) + } + } + + private func setup(prefix: Path.Command.Prefix, lastCommand: Path.Command?) -> Path.Command? { + position = 0 + singleValue = false + + switch prefix { + case .move: + command = .moveTo(point: .nan) + coordinates = .absolute + case .relativeMove: + command = .moveTo(point: currentPoint) + coordinates = .relative + case .line: + command = .lineTo(point: .nan) + coordinates = .absolute + case .relativeLine: + command = .lineTo(point: currentPoint) + coordinates = .relative + case .horizontalLine: + command = .lineTo(point: currentPoint.with(x: .nan)) + coordinates = .absolute + case .relativeHorizontalLine: + command = .lineTo(point: currentPoint) + coordinates = .relative + singleValue = true + case .verticalLine: + command = .lineTo(point: currentPoint.with(y: .nan)) + coordinates = .absolute + position = 1 + case .relativeVerticalLine: + command = .lineTo(point: currentPoint) + coordinates = .relative + position = 1 + singleValue = true + case .cubicBezierCurve: + command = .cubicBezierCurve(cp1: .nan, cp2: .nan, point: .nan) + coordinates = .absolute + case .relativeCubicBezierCurve: + command = .cubicBezierCurve(cp1: currentPoint, cp2: currentPoint, point: currentPoint) + coordinates = .relative + case .smoothCubicBezierCurve: + if case .cubicBezierCurve(_, let cp, _) = lastCommand { + command = .cubicBezierCurve(cp1: cp.reflecting(around: currentPoint), cp2: .nan, point: .nan) + } else { + command = .cubicBezierCurve(cp1: currentPoint, cp2: .nan, point: .nan) + } + coordinates = .absolute + position = 2 + case .relativeSmoothCubicBezierCurve: + if case .cubicBezierCurve(_, let cp, _) = lastCommand { + command = .cubicBezierCurve(cp1: cp.reflecting(around: cp.reflecting(around: currentPoint)), cp2: currentPoint, point: currentPoint) + } else { + command = .cubicBezierCurve(cp1: currentPoint, cp2: currentPoint, point: currentPoint) + } + coordinates = .relative + position = 2 + case .quadraticBezierCurve: + command = .quadraticBezierCurve(cp: .nan, point: .nan) + coordinates = .absolute + case .relativeQuadraticBezierCurve: + command = .quadraticBezierCurve(cp: currentPoint, point: currentPoint) + coordinates = .relative + case .smoothQuadraticBezierCurve: + if case .quadraticBezierCurve(let cp, _) = lastCommand { + command = .quadraticBezierCurve(cp: cp.reflecting(around: currentPoint), point: .nan) + } else { + command = .quadraticBezierCurve(cp: currentPoint, point: .nan) + } + coordinates = .absolute + position = 2 + case .relativeSmoothQuadraticBezierCurve: + if case .quadraticBezierCurve(let cp, _) = lastCommand { + command = .quadraticBezierCurve(cp: cp.reflecting(around: currentPoint), point: currentPoint) + } else { + command = .quadraticBezierCurve(cp: currentPoint, point: currentPoint) + } + coordinates = .relative + position = 2 + case .ellipticalArcCurve: + command = .ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: .nan) + coordinates = .absolute + case .relativeEllipticalArcCurve: + command = .ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: currentPoint) + coordinates = .relative + case .close, .relativeClose: + currentPoint = pathOrigin + reset() + return .closePath + } + + return nil + } + + private func process(value: Double, lastCommand: Path.Command?) throws -> Path.Command? { + if let command { + try continueCommand(command, with: value) + } else { + try nextCommand(with: value, lastCommand: lastCommand) + } + + if let command, command.isComplete { + switch coordinates { + case .relative: + guard position == -1 else { + return nil + } + + fallthrough + case .absolute: + currentPoint = command.point + if case .moveTo = command { + pathOrigin = command.point + } + reset() + return command + } + } else { + return nil + } + } + + private func continueCommand(_ command: Path.Command, with value: Double) throws { + switch command { + case .moveTo, .cubicBezierCurve, .quadraticBezierCurve, .ellipticalArcCurve: + self.command = try command.adjustingArgument(at: position, by: value) + switch coordinates { + case .absolute: + position += 1 + case .relative: + switch position { + case 0 ... (command.arguments - 2): + position += 1 + case command.arguments - 1: + position = -1 + default: + break // throw? + } + } + case .lineTo: + self.command = try command.adjustingArgument(at: position, by: value) + switch coordinates { + case .absolute: + position += 1 + case .relative: + switch position { + case 0: + if singleValue { + singleValue = false + position = -1 + } else { + position += 1 + } + case 1: + if singleValue { + singleValue = false + } + position = -1 + default: + break // throw? + } + } + case .closePath: + break + } + } + + private func nextCommand(with value: Double, lastCommand: Path.Command?) throws { + guard let command = lastCommand else { + throw Path.Command.Error.invalidRelativeCommand + } + + switch command { + case .moveTo: + switch coordinates { + case .absolute: + self.command = .lineTo(point: Point(x: value, y: .nan)) + position = 1 + case .relative: + let c = Path.Command.lineTo(point: command.point) + self.command = try c.adjustingArgument(at: 0, by: value) + position = 1 + } + case .lineTo: + switch coordinates { + case .absolute: + self.command = .lineTo(point: Point(x: value, y: .nan)) + position = 1 + case .relative: + let c = Path.Command.lineTo(point: command.point) + self.command = try c.adjustingArgument(at: 0, by: value) + position = 1 + } + case .cubicBezierCurve: + switch coordinates { + case .absolute: + self.command = .cubicBezierCurve(cp1: Point(x: value, y: .nan), cp2: .nan, point: .nan) + position = 1 + case .relative: + let c = Path.Command.cubicBezierCurve(cp1: command.point, cp2: command.point, point: command.point) + self.command = try c.adjustingArgument(at: 0, by: value) + position = 1 + } + case .quadraticBezierCurve: + switch coordinates { + case .absolute: + self.command = .quadraticBezierCurve(cp: Point(x: value, y: .nan), point: .nan) + position = 1 + case .relative: + let c = Path.Command.quadraticBezierCurve(cp: command.point, point: command.point) + self.command = try c.adjustingArgument(at: 0, by: value) + position = 1 + } + case .ellipticalArcCurve: + switch coordinates { + case .absolute: + self.command = .ellipticalArcCurve(rx: value, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: .nan) + position = 1 + case .relative: + let c = Path.Command.ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: command.point) + self.command = try c.adjustingArgument(at: 0, by: value) + position = 1 + } + case .closePath: + break + } + } + + private func reset() { + command = nil + coordinates = .absolute + position = 0 + singleValue = false + } + } +} diff --git a/third-party/SwiftSVG/Sources/Path.swift b/third-party/SwiftSVG/Sources/Path.swift new file mode 100644 index 0000000000..9c02c65704 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Path.swift @@ -0,0 +1,101 @@ +import XMLCoder + +/// Generic element to define a shape. +/// +/// A path is defined by including a ‘path’ element in a SVG document which contains a **d="(path data)"** +/// attribute, where the **‘d’** attribute contains the moveto, line, curve (both Cubic and Quadratic Bézier), +/// arc and closepath instructions. +/// +/// ## Documentation +/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path) +/// | [W3](https://www.w3.org/TR/SVG11/paths.html) +public struct Path: Element { + + /// The definition of the outline of a shape. + public var data: String = "" + + // MARK: CoreAttributes + + public var id: String? + + // MARK: PresentationAttributes + + public var fillColor: String? + public var fillOpacity: Double? + public var fillRule: Fill.Rule? + public var strokeColor: String? + public var strokeWidth: Double? + public var strokeOpacity: Double? + public var strokeLineCap: Stroke.LineCap? + public var strokeLineJoin: Stroke.LineJoin? + public var strokeMiterLimit: Double? + public var transform: String? + + // MARK: StylingAttributes + + public var style: String? + + enum CodingKeys: String, CodingKey { + case data = "d" + case id + case fillColor = "fill" + case fillOpacity = "fill-opacity" + case fillRule = "fill-rule" + case strokeColor = "stroke" + case strokeWidth = "stroke-width" + case strokeOpacity = "stroke-opacity" + case strokeLineCap = "stroke-linecap" + case strokeLineJoin = "stroke-linejoin" + case strokeMiterLimit = "stroke-miterlimit" + case transform + case style + } + + public init() {} + + public init(data: String) { + self.init() + self.data = data + } + + public init(commands: [Path.Command]) { + self.init() + data = commands.map(\.description).joined() + } +} + +extension Path: CommandRepresentable { + public func commands() throws -> [Command] { + try PathProcessor(data: data).commands() + } +} + +extension Path: CustomStringConvertible { + public var description: String { + "" + } +} + +extension Path: DynamicNodeDecoding { + public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding { + .attribute + } +} + +extension Path: DynamicNodeEncoding { + public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding { + .attribute + } +} + +extension Path: Equatable { + public static func == (lhs: Path, rhs: Path) -> Bool { + do { + let lhsCommands = try lhs.commands() + let rhsCommands = try rhs.commands() + return lhsCommands == rhsCommands + } catch { + return false + } + } +} diff --git a/third-party/SwiftSVG/Sources/Polygon.swift b/third-party/SwiftSVG/Sources/Polygon.swift new file mode 100644 index 0000000000..84320b929f --- /dev/null +++ b/third-party/SwiftSVG/Sources/Polygon.swift @@ -0,0 +1,82 @@ +import XMLCoder + +/// Defines a closed shape consisting of a set of connected straight line segments. +/// +/// The last point is connected to the first point. For open shapes, see the `Polyline` element. If an odd number of +/// coordinates is provided, then the element is in error. +/// +/// ## Documentation +/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon) +/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#PolygonElement) +public struct Polygon: Element { + + /// The points that make up the polygon. + public var points: String = "" + + // MARK: CoreAttributes + + public var id: String? + + // MARK: PresentationAttributes + + public var fillColor: String? + public var fillOpacity: Double? + public var fillRule: Fill.Rule? + public var strokeColor: String? + public var strokeWidth: Double? + public var strokeOpacity: Double? + public var strokeLineCap: Stroke.LineCap? + public var strokeLineJoin: Stroke.LineJoin? + public var strokeMiterLimit: Double? + public var transform: String? + + // MARK: StylingAttributes + + public var style: String? + + enum CodingKeys: String, CodingKey { + case points + case id + case fillColor = "fill" + case fillOpacity = "fill-opacity" + case fillRule = "fill-rule" + case strokeColor = "stroke" + case strokeWidth = "stroke-width" + case strokeOpacity = "stroke-opacity" + case strokeLineCap = "stroke-linecap" + case strokeLineJoin = "stroke-linejoin" + case strokeMiterLimit = "stroke-miterlimit" + case transform + case style + } + + public init() {} + + public init(points: String) { + self.points = points + } +} + +extension Polygon: CommandRepresentable { + public func commands() throws -> [Path.Command] { + try PolygonProcessor(points: points).commands() + } +} + +extension Polygon: CustomStringConvertible { + public var description: String { + "" + } +} + +extension Polygon: DynamicNodeDecoding { + public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding { + .attribute + } +} + +extension Polygon: DynamicNodeEncoding { + public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding { + .attribute + } +} diff --git a/third-party/SwiftSVG/Sources/Polyline.swift b/third-party/SwiftSVG/Sources/Polyline.swift new file mode 100644 index 0000000000..5fb8c930c4 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Polyline.swift @@ -0,0 +1,81 @@ +import XMLCoder + +/// SVG basic shape that creates straight lines connecting several points. +/// +/// Typically a polyline is used to create open shapes as the last point doesn't have to be connected to the first +/// point. For closed shapes see the `Polygon` element. +/// +/// ## Documentation +/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polyline) +/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#PolylineElement) +public struct Polyline: Element { + + public var points: String = "" + + // MARK: CoreAttributes + + public var id: String? + + // MARK: PresentationAttributes + + public var fillColor: String? + public var fillOpacity: Double? + public var fillRule: Fill.Rule? + public var strokeColor: String? + public var strokeWidth: Double? + public var strokeOpacity: Double? + public var strokeLineCap: Stroke.LineCap? + public var strokeLineJoin: Stroke.LineJoin? + public var strokeMiterLimit: Double? + public var transform: String? + + // MARK: StylingAttributes + + public var style: String? + + enum CodingKeys: String, CodingKey { + case points + case id + case fillColor = "fill" + case fillOpacity = "fill-opacity" + case fillRule = "fill-rule" + case strokeColor = "stroke" + case strokeWidth = "stroke-width" + case strokeOpacity = "stroke-opacity" + case strokeLineCap = "stroke-linecap" + case strokeLineJoin = "stroke-linejoin" + case strokeMiterLimit = "stroke-miterlimit" + case transform + case style + } + + public init() {} + + public init(points: String) { + self.points = points + } +} + +extension Polyline: CommandRepresentable { + public func commands() throws -> [Path.Command] { + try PolylineProcessor(points: points).commands() + } +} + +extension Polyline: CustomStringConvertible { + public var description: String { + "" + } +} + +extension Polyline: DynamicNodeDecoding { + public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding { + .attribute + } +} + +extension Polyline: DynamicNodeEncoding { + public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding { + .attribute + } +} diff --git a/third-party/SwiftSVG/Sources/PresentationAttributes.swift b/third-party/SwiftSVG/Sources/PresentationAttributes.swift new file mode 100644 index 0000000000..5cd6598df3 --- /dev/null +++ b/third-party/SwiftSVG/Sources/PresentationAttributes.swift @@ -0,0 +1,120 @@ +import Foundation +import Swift2D + +public protocol PresentationAttributes { + var fillColor: String? { get set } + var fillOpacity: Double? { get set } + var fillRule: Fill.Rule? { get set } + var strokeColor: String? { get set } + var strokeWidth: Double? { get set } + var strokeOpacity: Double? { get set } + var strokeLineCap: Stroke.LineCap? { get set } + var strokeLineJoin: Stroke.LineJoin? { get set } + var strokeMiterLimit: Double? { get set } + var transform: String? { get set } +} + +enum PresentationAttributesKeys: String, CodingKey { + case fillColor = "fill" + case fillOpacity = "fill-opacity" + case fillRule = "fill-rule" + case strokeColor = "stroke" + case strokeWidth = "stroke-width" + case strokeOpacity = "stroke-opacity" + case strokeLineCap = "stroke-linecap" + case strokeLineJoin = "stroke-linejoin" + case strokeMiterLimit = "stroke-miterlimit" + case transform +} + +public extension PresentationAttributes { + var presentationDescription: String { + var attributes: [String] = [] + + if let fillColor { + attributes.append("\(PresentationAttributesKeys.fillColor.rawValue)=\"\(fillColor)\"") + } + if let fillOpacity { + attributes.append("\(PresentationAttributesKeys.fillOpacity.rawValue)=\"\(fillOpacity)\"") + } + if let fillRule { + attributes.append("\(PresentationAttributesKeys.fillRule.rawValue)=\"\(fillRule.description)\"") + } + if let strokeColor { + attributes.append("\(PresentationAttributesKeys.strokeColor.rawValue)=\"\(strokeColor)\"") + } + if let strokeWidth { + attributes.append("\(PresentationAttributesKeys.strokeWidth.rawValue)=\"\(strokeWidth)\"") + } + if let strokeOpacity { + attributes.append("\(PresentationAttributesKeys.strokeOpacity.rawValue)=\"\(strokeOpacity)\"") + } + if let strokeLineCap { + attributes.append("\(PresentationAttributesKeys.strokeLineCap.rawValue)=\"\(strokeLineCap.description)\"") + } + if let strokeLineJoin { + attributes.append("\(PresentationAttributesKeys.strokeLineJoin.rawValue)=\"\(strokeLineJoin.description)\"") + } + if let strokeMiterLimit { + attributes.append("\(PresentationAttributesKeys.strokeMiterLimit.rawValue)=\"\(strokeMiterLimit)\"") + } + if let transform { + attributes.append("\(PresentationAttributesKeys.transform.rawValue)=\"\(transform)\"") + } + + return attributes.joined(separator: " ") + } + + var transformations: [Transformation] { + let value = transform?.replacingOccurrences(of: " ", with: "") ?? "" + guard !value.isEmpty else { + return [] + } + + let values = value.split(separator: ")").map { $0.appending(")") } + return values.compactMap { Transformation($0) } + } + + var fill: Fill? { + get { + if fillColor == nil, fillOpacity == nil { + return nil + } + + var fill = Fill() + fill.color = fillColor ?? "black" + fill.opacity = fillOpacity ?? 1.0 + return fill + } + set { + fillColor = newValue?.color + fillOpacity = newValue?.opacity + fillRule = newValue?.rule + } + } + + var stroke: Stroke? { + get { + if strokeColor == nil, strokeOpacity == nil { + return nil + } + + var stroke = Stroke() + stroke.color = strokeColor ?? "black" + stroke.opacity = strokeOpacity ?? 1.0 + stroke.width = strokeWidth ?? 1.0 + stroke.lineCap = strokeLineCap ?? .butt + stroke.lineJoin = strokeLineJoin ?? .miter + stroke.miterLimit = strokeMiterLimit + return stroke + } + set { + strokeColor = newValue?.color + strokeOpacity = newValue?.opacity + strokeWidth = newValue?.width + strokeLineCap = newValue?.lineCap + strokeLineJoin = newValue?.lineJoin + strokeMiterLimit = newValue?.miterLimit + } + } +} diff --git a/third-party/SwiftSVG/Sources/Rectangle.swift b/third-party/SwiftSVG/Sources/Rectangle.swift new file mode 100644 index 0000000000..4f663b846d --- /dev/null +++ b/third-party/SwiftSVG/Sources/Rectangle.swift @@ -0,0 +1,115 @@ +import Swift2D +import XMLCoder + +/// Basic SVG shape that draws rectangles, defined by their position, width, and height. +/// +/// The values used for the x- and y-axis rounded corner radii are determined implicitly +/// if the ‘rx’ or ‘ry’ attributes (or both) are not specified, or are specified but with invalid values. +/// +/// ## Documentation +/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect) +/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#RectElement) +public struct Rectangle: Element { + + /// The x-axis coordinate of the side of the rectangle which + /// has the smaller x-axis coordinate value. + public var x: Double = 0.0 + /// The y-axis coordinate of the side of the rectangle which + /// has the smaller y-axis coordinate value + public var y: Double = 0.0 + /// The width of the rectangle. + public var width: Double = 0.0 + /// The height of the rectangle. + public var height: Double = 0.0 + /// For rounded rectangles, the x-axis radius of the ellipse used + /// to round off the corners of the rectangle. + public var rx: Double? + /// For rounded rectangles, the y-axis radius of the ellipse used + /// to round off the corners of the rectangle. + public var ry: Double? + + // MARK: CoreAttributes + + public var id: String? + + // MARK: PresentationAttributes + + public var fillColor: String? + public var fillOpacity: Double? + public var fillRule: Fill.Rule? + public var strokeColor: String? + public var strokeWidth: Double? + public var strokeOpacity: Double? + public var strokeLineCap: Stroke.LineCap? + public var strokeLineJoin: Stroke.LineJoin? + public var strokeMiterLimit: Double? + public var transform: String? + + // MARK: StylingAttributes + + public var style: String? + + enum CodingKeys: String, CodingKey { + case x + case y + case width + case height + case rx + case ry + case id + case fillColor = "fill" + case fillOpacity = "fill-opacity" + case fillRule = "fill-rule" + case strokeColor = "stroke" + case strokeWidth = "stroke-width" + case strokeOpacity = "stroke-opacity" + case strokeLineCap = "stroke-linecap" + case strokeLineJoin = "stroke-linejoin" + case strokeMiterLimit = "stroke-miterlimit" + case transform + case style + } + + public init() {} + + public init(x: Double, y: Double, width: Double, height: Double, rx: Double? = nil, ry: Double? = nil) { + self.x = x + self.y = y + self.width = width + self.height = height + self.rx = rx + self.ry = ry + } +} + +extension Rectangle: CustomStringConvertible { + public var description: String { + var desc = "" + } +} + +extension Rectangle: DirectionalCommandRepresentable { + public func commands(clockwise: Bool) throws -> [Path.Command] { + RectangleProcessor(rectangle: self).commands(clockwise: clockwise) + } +} + +extension Rectangle: DynamicNodeDecoding { + public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding { + .attribute + } +} + +extension Rectangle: DynamicNodeEncoding { + public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding { + .attribute + } +} diff --git a/third-party/SwiftSVG/Sources/SVG+Swift2D.swift b/third-party/SwiftSVG/Sources/SVG+Swift2D.swift new file mode 100644 index 0000000000..abf62de434 --- /dev/null +++ b/third-party/SwiftSVG/Sources/SVG+Swift2D.swift @@ -0,0 +1,62 @@ +import Foundation +import Swift2D + +public extension SVG { + /// Original size of the document image. + /// + /// Primarily uses the `viewBox` attribute, and will fallback to the 'pixelSize' + var originalSize: Size { + (viewBoxSize ?? pixelSize) ?? .zero + } + + /// Size of the design in a square 'viewBox'. + /// + /// All paths created by this framework are outputted in a 'square'. + var outputSize: Size { + let size = (pixelSize ?? viewBoxSize) ?? .zero + let maxDimension = max(size.width, size.height) + return Size(width: maxDimension, height: maxDimension) + } + + /// Size derived from the `viewBox` document attribute + var viewBoxSize: Size? { + guard let viewBox else { + return nil + } + + let components = viewBox.components(separatedBy: .whitespaces) + guard components.count == 4 else { + return nil + } + + guard let width = Double(components[2]) else { + return nil + } + + guard let height = Double(components[3]) else { + return nil + } + + return Size(width: width, height: height) + } + + /// Size derived from the 'width' & 'height' document attributes + var pixelSize: Size? { + guard let width, !width.isEmpty else { + return nil + } + + guard let height, !height.isEmpty else { + return nil + } + + let widthRawValue = width.replacingOccurrences(of: "px", with: "", options: .caseInsensitive, range: nil) + let heightRawValue = height.replacingOccurrences(of: "px", with: "", options: .caseInsensitive, range: nil) + + guard let w = Double(widthRawValue), let h = Double(heightRawValue) else { + return nil + } + + return Size(width: w, height: h) + } +} diff --git a/third-party/SwiftSVG/Sources/SVG.swift b/third-party/SwiftSVG/Sources/SVG.swift new file mode 100644 index 0000000000..cbf3117bbc --- /dev/null +++ b/third-party/SwiftSVG/Sources/SVG.swift @@ -0,0 +1,160 @@ +import Foundation +import XMLCoder + +/// SVG is a language for describing two-dimensional graphics in XML. +/// +/// The svg element is a container that defines a new coordinate system and viewport. It is used as the outermost +/// element of SVG documents, but it can also be used to embed a SVG fragment inside an SVG or HTML document. +/// +/// ## Documentation +/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg) +/// | [W3](https://www.w3.org/TR/SVG11/) +public struct SVG: Container { + + public var viewBox: String? + public var width: String? + public var height: String? + public var title: String? + public var desc: String? + + // Container + public var circles: [Circle]? + public var ellipses: [Ellipse]? + public var groups: [Group]? + public var lines: [Line]? + public var paths: [Path]? + public var polygons: [Polygon]? + public var polylines: [Polyline]? + public var rectangles: [Rectangle]? + public var texts: [Text]? + + /// A non-optional, non-spaced representation of the `title`. + public var name: String { + let name = title ?? "SVG Document" + let newTitle = name.components(separatedBy: .punctuationCharacters).joined(separator: "_") + return newTitle.replacingOccurrences(of: " ", with: "_") + } + + enum CodingKeys: String, CodingKey { + case width + case height + case viewBox + case title + case desc + case circles = "circle" + case ellipses = "ellipse" + case groups = "g" + case lines = "line" + case paths = "path" + case polylines = "polyline" + case polygons = "polygon" + case rectangles = "rect" + case texts = "text" + } + + public init() {} + + public init(width: Int, height: Int) { + self.width = "\(width)px" + self.height = "\(height)px" + viewBox = "0 0 \(width) \(height)" + } + + public static func make(from url: URL) throws -> SVG { + guard FileManager.default.fileExists(atPath: url.path) else { + throw CocoaError(.fileNoSuchFile) + } + + let data = try Data(contentsOf: url) + return try make(with: data) + } + + public static func make(with data: Data, decoder: XMLDecoder = XMLDecoder()) throws -> SVG { + try decoder.decode(SVG.self, from: data) + } + + /// A collection of all `Path`s in the document. + public func subpaths() throws -> [Path] { + var output: [Path] = [] + let _transformations: [Transformation] = [] + + if let circles { + try output.append(contentsOf: circles.compactMap { try $0.path(applying: _transformations) }) + } + + if let ellipses { + try output.append(contentsOf: ellipses.compactMap { try $0.path(applying: _transformations) }) + } + + if let rectangles { + try output.append(contentsOf: rectangles.compactMap { try $0.path(applying: _transformations) }) + } + + if let polygons { + try output.append(contentsOf: polygons.compactMap { try $0.path(applying: _transformations) }) + } + + if let polylines { + try output.append(contentsOf: polylines.compactMap { try $0.path(applying: _transformations) }) + } + + if let paths { + try output.append(contentsOf: paths.map { try $0.path(applying: _transformations) }) + } + + if let groups { + try groups.forEach { + try output.append(contentsOf: $0.subpaths(applying: _transformations)) + } + } + + return output + } + + /// A singular path that represents all of the `Command`s within the document. + public func coalescedPath() throws -> Path { + let paths = try subpaths() + let commands = try paths.flatMap { try $0.commands() } + return Path(commands: commands) + } +} + +extension SVG: CustomStringConvertible { + public var description: String { + var contents: String = "" + + if let title { + contents.append("\n\(title)") + } + + if let desc { + contents.append("\n\(desc)") + } + + contents.append(containerDescription) + + return "\(contents)\n" + } +} + +extension SVG: DynamicNodeDecoding { + public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding { + switch key { + case CodingKeys.width, CodingKeys.height, CodingKeys.viewBox: + .attribute + default: + .element + } + } +} + +extension SVG: DynamicNodeEncoding { + public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case CodingKeys.width, CodingKeys.height, CodingKeys.viewBox: + .attribute + default: + .element + } + } +} diff --git a/third-party/SwiftSVG/Sources/Stroke.swift b/third-party/SwiftSVG/Sources/Stroke.swift new file mode 100644 index 0000000000..d68cba3dd4 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Stroke.swift @@ -0,0 +1,64 @@ +import Swift2D + +public struct Stroke { + + public var color: String? + public var width: Double? + public var opacity: Double? + public var lineCap: LineCap = .butt + public var lineJoin: LineJoin = .miter + public var miterLimit: Double? + + public init() {} + + /// Presentation attribute defining the shape to be used at the end of open subpaths when they are stroked. + /// + /// The default `LineCap` is `.butt` + public enum LineCap: String, Sendable, Codable, CaseIterable { + /// The stroke for each subpath does not extend beyond its two endpoints. + case butt + /// The end of each subpath the stroke will be extended by a half circle with a diameter equal to the stroke + /// width. + case round + /// The end of each subpath the stroke will be extended by a rectangle with a width equal to half the width of + /// the stroke and a height equal to the width of the stroke. + case square + } + + /// Presentation attribute defining the shape to be used at the corners of paths when they are stroked. + /// + /// The default `LineJoin` is `.miter` + public enum LineJoin: String, Sendable, Codable, CaseIterable { + /// An arcs corner is to be used to join path segments. + /// + /// The arcs shape is formed by extending the outer edges of the stroke at the join point with arcs that have + /// the same curvature as the outer edges at the join point. + case arcs + /// The bevel value indicates that a bevelled corner is to be used to join path segments. + case bevel + /// Indicates that a sharp corner is to be used to join path segments. + /// + /// The corner is formed by extending the outer edges of the stroke at the tangents of the path segments until + /// they intersect. + case miter + /// A sharp corner is to be used to join path segments. + /// + /// The corner is formed by extending the outer edges of the stroke at the tangents of the path segments until + /// they intersect. + case miterClip = "miter-clip" + /// The round value indicates that a round corner is to be used to join path segments. + case round + } +} + +extension Stroke.LineCap: CustomStringConvertible { + public var description: String { + rawValue + } +} + +extension Stroke.LineJoin: CustomStringConvertible { + public var description: String { + rawValue + } +} diff --git a/third-party/SwiftSVG/Sources/StylingAttributes.swift b/third-party/SwiftSVG/Sources/StylingAttributes.swift new file mode 100644 index 0000000000..8c597a42f4 --- /dev/null +++ b/third-party/SwiftSVG/Sources/StylingAttributes.swift @@ -0,0 +1,17 @@ +public protocol StylingAttributes { + var style: String? { get set } +} + +enum StylingAttributesKeys: String, CodingKey { + case style +} + +public extension StylingAttributes { + var stylingDescription: String { + if let style { + "\(StylingAttributesKeys.style.rawValue)=\"\(style)\"" + } else { + "" + } + } +} diff --git a/third-party/SwiftSVG/Sources/Text.swift b/third-party/SwiftSVG/Sources/Text.swift new file mode 100644 index 0000000000..d54f62fcf6 --- /dev/null +++ b/third-party/SwiftSVG/Sources/Text.swift @@ -0,0 +1,110 @@ +import Foundation +import XMLCoder + +/// Graphics element consisting of text +/// +/// It's possible to apply a gradient, pattern, clipping path, mask, or filter to `Text`, like any other SVG graphics element. +/// +/// ## Documentation +/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text) +/// | [W3](https://www.w3.org/TR/SVG11/text.html#TextElement) +public struct Text: Element { + + public var value: String = "" + public var x: Double? + public var y: Double? + public var dx: Double? + public var dy: Double? + + // MARK: CoreAttributes + + public var id: String? + + // MARK: PresentationAttributes + + public var fillColor: String? + public var fillOpacity: Double? + public var fillRule: Fill.Rule? + public var strokeColor: String? + public var strokeWidth: Double? + public var strokeOpacity: Double? + public var strokeLineCap: Stroke.LineCap? + public var strokeLineJoin: Stroke.LineJoin? + public var strokeMiterLimit: Double? + public var transform: String? + + // MARK: StylingAttributes + + public var style: String? + + enum CodingKeys: String, CodingKey { + case value = "" + case x + case y + case dx + case dy + case id + case fillColor = "fill" + case fillOpacity = "fill-opacity" + case fillRule = "fill-rule" + case strokeColor = "stroke" + case strokeWidth = "stroke-width" + case strokeOpacity = "stroke-opacity" + case strokeLineCap = "stroke-linecap" + case strokeLineJoin = "stroke-linejoin" + case strokeMiterLimit = "stroke-miterlimit" + case transform + case style + } + + public init() {} + + public init(value: String) { + self.value = value + } +} + +extension Text: CustomStringConvertible { + public var description: String { + var components: [String] = [] + + if let x, !x.isNaN, !x.isZero { + components.append(String(format: "x=\"%.5f\"", x)) + } + if let y, !y.isNaN, !y.isZero { + components.append(String(format: "y=\"%.5f\"", y)) + } + if let dx, !dx.isNaN, !dx.isZero { + components.append(String(format: "dx=\"%.5f\"", dx)) + } + if let dy, !dy.isNaN, !dy.isZero { + components.append(String(format: "dy=\"%.5f\"", dy)) + } + + components.append(attributeDescription) + + return "\(value)" + } +} + +extension Text: DynamicNodeDecoding { + public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding { + switch key { + case CodingKeys.value: + .element + default: + .attribute + } + } +} + +extension Text: DynamicNodeEncoding { + public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case CodingKeys.value: + .element + default: + .attribute + } + } +} diff --git a/third-party/SwiftSVG/Sources/Transformation.swift b/third-party/SwiftSVG/Sources/Transformation.swift new file mode 100644 index 0000000000..3fb086d55e --- /dev/null +++ b/third-party/SwiftSVG/Sources/Transformation.swift @@ -0,0 +1,113 @@ +import Foundation +import Swift2D + +/// A modification that should be applied to an element and its children. +/// +/// If a list of transforms is provided, then the net effect is as if each transform had been specified separately in +/// the order provided. +/// +/// For example, +/// ``` +/// +/// +/// +/// ``` +/// is functionally equivalent to: +/// ``` +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// ``` +/// +/// The ‘transform’ attribute is applied to an element before processing any other coordinate or length values supplied +/// for that element. +/// +/// ## Documentation +/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform) +/// | [W3](https://www.w3.org/TR/SVG11/coords.html#TransformAttribute) +public enum Transformation { + /// Moves an object by x & y. (Y is assumed to be '0' if not provided) + case translate(x: Double, y: Double) + /// Specifies a transformation in the form of a transformation matrix of six values. + case matrix(a: Double, b: Double, c: Double, d: Double, e: Double, f: Double) + + public enum Prefix: String, CaseIterable { + case translate + case matrix + } + + /// Initializes a new `Transformation` with a raw SVG transformation string. + public init?(_ string: String) { + guard let prefix = Prefix.allCases.first(where: { string.lowercased().hasPrefix($0.rawValue) }) else { + return nil + } + + switch prefix { + case .translate: + guard let start = string.firstIndex(of: "(") else { + return nil + } + + guard let stop = string.lastIndex(of: ")") else { + return nil + } + + var substring = String(string[start ... stop]) + substring = substring.replacingOccurrences(of: "(", with: "") + substring = substring.replacingOccurrences(of: ")", with: "") + + var components = substring.split(separator: " ", omittingEmptySubsequences: true).map { String($0) } + components = components.flatMap { $0.components(separatedBy: ",") } + + let values = components.compactMap { Double($0) }.map { Double($0) } + guard values.count > 0 else { + return nil + } + + if values.count > 1 { + self = .translate(x: values[0], y: values[1]) + } else { + self = .translate(x: values[0], y: 0.0) + } + case .matrix: + guard let start = string.firstIndex(of: "(") else { + return nil + } + + guard let stop = string.lastIndex(of: ")") else { + return nil + } + + var substring = String(string[start ... stop]) + substring = substring.replacingOccurrences(of: "(", with: "") + substring = substring.replacingOccurrences(of: ")", with: "") + + var components = substring.split(separator: " ", omittingEmptySubsequences: true).map { String($0) } + components = components.flatMap { $0.components(separatedBy: ",") } + + let values = components.compactMap { Double($0) }.map { Double($0) } + guard values.count > 5 else { + return nil + } + + self = .matrix(a: values[0], b: values[1], c: values[2], d: values[3], e: values[4], f: values[5]) + } + } +} + +extension Transformation: CustomStringConvertible { + public var description: String { + switch self { + case .translate(let x, let y): + "translate(\(x), \(y))" + case .matrix(let a, let b, let c, let d, let e, let f): + "matrix(\(a), \(b), \(c), \(d), \(e), \(f))" + } + } +} diff --git a/third-party/VectorPlus/BUILD b/third-party/VectorPlus/BUILD new file mode 100644 index 0000000000..c7f57a85a1 --- /dev/null +++ b/third-party/VectorPlus/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "VectorPlus", + module_name = "VectorPlus", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//third-party/SwiftColor", + "//third-party/SwiftSVG", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/third-party/VectorPlus/Sources/CoreGraphics/CGContext+Render.swift b/third-party/VectorPlus/Sources/CoreGraphics/CGContext+Render.swift new file mode 100644 index 0000000000..a6f3e94d73 --- /dev/null +++ b/third-party/VectorPlus/Sources/CoreGraphics/CGContext+Render.swift @@ -0,0 +1,72 @@ +import Swift2D +import SwiftColor +import SwiftSVG +#if canImport(CoreGraphics) +import CoreGraphics + +public extension CGContext { + func render(path: Path, from: Rect, to: Rect) throws { + saveGState() + + let cgPath = CGMutablePath() + + let commands = (try? path.commands()) ?? [] + for (idx, command) in commands.enumerated() { + let previous: Point? = if idx > 0 { + commands[idx - 1].previousPoint + } else { + nil + } + + cgPath.addCommand(command, from: from, to: to, previousPoint: previous) + } + + if let fill = path.fill { + let _color = Pigment(fill.color ?? "black") + if _color.alpha != 0.0 { + let cgColor = CGColor.make(_color) + let color = cgColor.copy(alpha: CGFloat(fill.opacity ?? 1.0)) ?? cgColor + let rule = fill.rule.cgFillRule + + setFillColor(color) + addPath(cgPath) + fillPath(using: rule) + } + } + + if let stroke = path.stroke { + let _color = Pigment(stroke.color ?? "black") + if _color.alpha != 0.0 { + let cgColor = CGColor.make(_color) + let color = cgColor.copy(alpha: CGFloat(stroke.opacity ?? 1.0)) ?? cgColor + let width = stroke.width ?? 1.0 + let lineWidth = width * (to.size.width / from.size.width) + + setLineWidth(CGFloat(lineWidth)) + setStrokeColor(color) + setLineCap(stroke.lineCap.cgLineCap) + setLineJoin(stroke.lineJoin.cgLineJoin) + if let miterLimit = stroke.miterLimit, stroke.lineJoin == .miter { + setMiterLimit(CGFloat(miterLimit)) + } + addPath(cgPath) + strokePath() + } + } + + if path.fill == nil, path.stroke == nil { + let color = CGColor(srgbRed: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) + setFillColor(color) + addPath(cgPath) + fillPath(using: path.fill?.rule.cgFillRule ?? .winding) + } + + restoreGState() + } + + func render(path: Path, in rect: Rect) throws { + try render(path: path, from: rect, to: rect) + } +} + +#endif diff --git a/third-party/VectorPlus/Sources/CoreGraphics/CGMutablePath+Instruction.swift b/third-party/VectorPlus/Sources/CoreGraphics/CGMutablePath+Instruction.swift new file mode 100644 index 0000000000..f810e6d883 --- /dev/null +++ b/third-party/VectorPlus/Sources/CoreGraphics/CGMutablePath+Instruction.swift @@ -0,0 +1,45 @@ +import Swift2D +import SwiftSVG +#if canImport(CoreGraphics) +import CoreGraphics + +public extension CGMutablePath { + /// Adds a `Path.Command` to a _CoreGraphics path_, using the provided rectangles to correctly scale the parameters. + /// + /// - parameter command: The `Path.Command` to append + /// - parameter from: The `Rect` which originally had the instruction. This is typically the `Document.originalSize`. + /// - parameter to: The `Rect` defining the new size. + /// - parameter previousPoint: The last `Point`, used for Elliptical Arc calculations + func addCommand(_ command: Path.Command, from: Rect, to: Rect, previousPoint: Point? = nil) { + let translated = command.translate(from: from, to: to) + switch translated { + case .moveTo(let point): + move(to: CGPoint(point)) + case .lineTo(let point): + addLine(to: CGPoint(point)) + case .cubicBezierCurve(let cp1, let cp2, let point): + addCurve(to: CGPoint(point), control1: CGPoint(cp1), control2: CGPoint(cp2)) + case .quadraticBezierCurve(let cp, let point): + addQuadCurve(to: CGPoint(point), control: CGPoint(cp)) + case .ellipticalArcCurve(_, _, _, _, _, let point): + guard let previousPoint else { + addLine(to: CGPoint(point)) + return + } + + do { + let curves = try command.convertToCubicBezierCurves(with: previousPoint) + for curve in curves { + addCommand(curve, from: from, to: to) + } + } catch { + print(error) + addLine(to: CGPoint(point)) + } + case .closePath: + closeSubpath() + } + } +} + +#endif diff --git a/third-party/VectorPlus/Sources/CoreGraphics/Fill+CoreGraphics.swift b/third-party/VectorPlus/Sources/CoreGraphics/Fill+CoreGraphics.swift new file mode 100644 index 0000000000..e038784c2c --- /dev/null +++ b/third-party/VectorPlus/Sources/CoreGraphics/Fill+CoreGraphics.swift @@ -0,0 +1,14 @@ +import Foundation +import SwiftSVG +#if canImport(CoreGraphics) +import CoreGraphics + +public extension Fill.Rule { + var cgFillRule: CGPathFillRule { + switch self { + case .evenOdd: .evenOdd + case .nonZero: .winding + } + } +} +#endif diff --git a/third-party/VectorPlus/Sources/CoreGraphics/SVG+CGPath.swift b/third-party/VectorPlus/Sources/CoreGraphics/SVG+CGPath.swift new file mode 100644 index 0000000000..ef4cadbb66 --- /dev/null +++ b/third-party/VectorPlus/Sources/CoreGraphics/SVG+CGPath.swift @@ -0,0 +1,37 @@ +import Swift2D +import SwiftSVG +#if canImport(CoreGraphics) +import CoreGraphics + +public extension SVG { + func path(size: Size) -> CGPath { + guard size.height > 0.0, size.width > 0.0 else { + return CGMutablePath() + } + + guard let paths = try? subpaths() else { + return CGMutablePath() + } + + let from = Rect(origin: .zero, size: originalSize) + let to = Rect(origin: .zero, size: size) + + let path = CGMutablePath() + + for p in paths { + let commands = (try? p.commands()) ?? [] + for (idx, command) in commands.enumerated() { + let previous: Point? = if idx > 0 { + commands[idx - 1].previousPoint + } else { + nil + } + + path.addCommand(command, from: from, to: to, previousPoint: previous) + } + } + + return path + } +} +#endif diff --git a/third-party/VectorPlus/Sources/CoreGraphics/Stroke+CoreGraphics.swift b/third-party/VectorPlus/Sources/CoreGraphics/Stroke+CoreGraphics.swift new file mode 100644 index 0000000000..3744223181 --- /dev/null +++ b/third-party/VectorPlus/Sources/CoreGraphics/Stroke+CoreGraphics.swift @@ -0,0 +1,25 @@ +import Foundation +import SwiftSVG +#if canImport(CoreGraphics) +import CoreGraphics + +public extension Stroke.LineCap { + var cgLineCap: CGLineCap { + switch self { + case .butt: .butt + case .round: .round + case .square: .square + } + } +} + +public extension Stroke.LineJoin { + var cgLineJoin: CGLineJoin { + switch self { + case .bevel: .bevel + case .arcs, .miter, .miterClip: .miter + case .round: .round + } + } +} +#endif diff --git a/third-party/VectorPlus/Sources/Fill+VectorPlus.swift b/third-party/VectorPlus/Sources/Fill+VectorPlus.swift new file mode 100644 index 0000000000..c7a66cf9fb --- /dev/null +++ b/third-party/VectorPlus/Sources/Fill+VectorPlus.swift @@ -0,0 +1,29 @@ +import SwiftColor +import SwiftSVG + +public extension Fill { + @available(*, deprecated, renamed: "pigment") + var swiftColor: Pigment? { pigment } + + var pigment: Pigment? { + guard let color, !color.isEmpty else { + return nil + } + + let _color = Pigment(color) + guard _color.alpha != 0.0 else { + return nil + } + + return _color + } +} + +public extension Fill.Rule { + var coreGraphicsDescription: String { + switch self { + case .evenOdd: ".evenOdd" + case .nonZero: ".winding" + } + } +} diff --git a/third-party/VectorPlus/Sources/Path+VectorPlus.swift b/third-party/VectorPlus/Sources/Path+VectorPlus.swift new file mode 100644 index 0000000000..6511f6bbac --- /dev/null +++ b/third-party/VectorPlus/Sources/Path+VectorPlus.swift @@ -0,0 +1,34 @@ +import Swift2D +import SwiftSVG + +public extension Path { + func asCoreGraphicsDescription(variable: String = "path", originalSize: Size) throws -> String { + var outputs: [String] = [] + let commands = (try? commands()) ?? [] + for (idx, command) in commands.enumerated() { + let previous: Point? = if idx > 0 { + commands[idx - 1].previousPoint + } else { + nil + } + + let method = command.coreGraphicsDescription(originalSize: originalSize, previousPoint: previous) + let code = "\(variable)\(method)" + outputs.append(code) + } + return outputs.joined(separator: "\n ") + } +} + +public extension Path.Command { + var previousPoint: Point { + switch self { + case .moveTo(let point): point + case .lineTo(let point): point + case .cubicBezierCurve(_, _, let point): point + case .quadraticBezierCurve(_, let point): point + case .ellipticalArcCurve(_, _, _, _, _, let point): point + case .closePath: .zero + } + } +} diff --git a/third-party/VectorPlus/Sources/Path.Command+VectorPlus.swift b/third-party/VectorPlus/Sources/Path.Command+VectorPlus.swift new file mode 100644 index 0000000000..226e57a643 --- /dev/null +++ b/third-party/VectorPlus/Sources/Path.Command+VectorPlus.swift @@ -0,0 +1,216 @@ +import Foundation +import Swift2D +import SwiftSVG + +public extension Path.Command { + /// Uses the _Power of Math_ to translate a commands controls/points from one `Rect` to another `Rect`. + func translate(from: Rect, to: Rect) -> Path.Command { + switch self { + case .moveTo(let point): + let _point = VectorPoint(point: point, in: from).translate(to: to) + return .moveTo(point: _point) + case .lineTo(let point): + let _point = VectorPoint(point: point, in: from).translate(to: to) + return .lineTo(point: _point) + case .cubicBezierCurve(let cp1, let cp2, let point): + let _cp1 = VectorPoint(point: cp1, in: from).translate(to: to) + let _cp2 = VectorPoint(point: cp2, in: from).translate(to: to) + let _point = VectorPoint(point: point, in: from).translate(to: to) + return .cubicBezierCurve(cp1: _cp1, cp2: _cp2, point: _point) + case .quadraticBezierCurve(let cp, let point): + let _cp = VectorPoint(point: cp, in: from).translate(to: to) + let _point = VectorPoint(point: point, in: from).translate(to: to) + return .quadraticBezierCurve(cp: _cp, point: _point) + case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point): + let _rx = rx * (from.size.maxRadius / to.size.minRadius) + let _ry = ry * (from.size.maxRadius / to.size.minRadius) + let _point = VectorPoint(point: point, in: from).translate(to: to) + return .ellipticalArcCurve(rx: _rx, ry: _ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: _point) + case .closePath: + return self + } + } +} + +public extension Path.Command { + func coreGraphicsDescription(originalSize: Size, previousPoint: Point? = nil) -> String { + let rect = Rect(origin: .zero, size: originalSize) + + switch self { + case .moveTo(let point): + let _point = VectorPoint(point: point, in: rect) + return ".move(to: \(_point.coreGraphicsDescription))" + case .lineTo(let point): + let _point = VectorPoint(point: point, in: rect) + return ".addLine(to: \(_point.coreGraphicsDescription))" + case .cubicBezierCurve(let cp1, let cp2, let point): + let _cp1 = VectorPoint(point: cp1, in: rect) + let _cp2 = VectorPoint(point: cp2, in: rect) + let _point = VectorPoint(point: point, in: rect) + return ".addCurve(to: \(_point.coreGraphicsDescription), control1: \(_cp1.coreGraphicsDescription), control2: \(_cp2.coreGraphicsDescription))" + case .quadraticBezierCurve(let cp, let point): + let _cp = VectorPoint(point: cp, in: rect) + let _point = VectorPoint(point: point, in: rect) + return ".addQuadCurve(to: \(_point.coreGraphicsDescription), control: \(_cp.coreGraphicsDescription))" + case .ellipticalArcCurve(_, _, _, _, _, let point): + guard let previousPoint else { + return Path.Command.lineTo(point: point).coreGraphicsDescription(originalSize: originalSize) + } + + do { + let curves = try convertToCubicBezierCurves(with: previousPoint) + return curves.map { $0.coreGraphicsDescription(originalSize: originalSize) }.joined(separator: "\n") + } catch { + print(error) + return Path.Command.lineTo(point: point).coreGraphicsDescription(originalSize: originalSize) + } + case .closePath: + return ".closeSubpath()" + } + } +} + +extension Path.Command { + /// Converts an `.ellipticalArcCurve` into one or more `.cubicBezierCurve`s. + /// https://github.com/colinmeinke/svg-arc-to-cubic-bezier/blob/master/src/index.js + func convertToCubicBezierCurves(with previousPoint: Point) throws -> [Path.Command] { + guard case let .ellipticalArcCurve(rx, ry, angle, largeArg, clockwise, point) = self else { + throw Path.Command.Error.message("\(#function); Only .ellipticalArcCurve is allowed.") + } + + var curves: [Path.Command] = [] + + guard rx > 0.0, ry > 0.0 else { + throw Path.Command.Error.message("\(#function); rx/ry must be greater than 0.0 (zero).") + } + + let sinφ = sin(angle * (.pi * 2.0) / 360.0) + let cosφ = cos(angle * (.pi * 2.0) / 360.0) + + let pxp = cosφ * (previousPoint.x - point.x) / 2 + sinφ * (previousPoint.y - point.y) / 2.0 + let pyp = -sinφ * (previousPoint.x - point.x) / 2 + cosφ * (previousPoint.y - point.y) / 2.0 + + guard pxp != 0.0, pyp != 0.0 else { + throw Path.Command.Error.message("\(#function); math") + } + + var _rx = abs(rx) + var _ry = abs(ry) + + let λ = pow(pxp, 2.0) / pow(_rx, 2.0) + pow(pyp, 2.0) / pow(_ry, 2.0) + + if λ > 1.0 { + _rx *= sqrt(λ) + _ry *= sqrt(λ) + } + + let _arcCenter = arcCenter(previousPoint: previousPoint, point: point, rx: _rx, ry: _ry, largeArc: largeArg, clockwise: clockwise, sinφ: sinφ, cosφ: cosφ, pxp: pxp, pyp: pyp) + let center = _arcCenter.center + var angle1 = _arcCenter.angle1 + var angle2 = _arcCenter.angle2 + + var ratio = abs(angle2) / ((.pi * 2.0) / 4.0) + if abs(1.0 - ratio) < 0.0000001 { + ratio = 1.0 + } + + let segments = max(ceil(ratio), 1) + + angle2 /= segments + + var rawCurves: [(Point, Point, Point)] = [] + for _ in 0 ... Int(segments) { + rawCurves.append(approximateUnitArc(angle1: angle1, angle2: angle2)) + angle1 += angle2 + } + + for rawCurf in rawCurves { + let _cp1 = mapToEllipse(point: rawCurf.0, rx: _rx, ry: _ry, sinφ: sinφ, cosφ: cosφ, center: center) + let _cp2 = mapToEllipse(point: rawCurf.1, rx: _rx, ry: _ry, sinφ: sinφ, cosφ: cosφ, center: center) + let _point = mapToEllipse(point: rawCurf.2, rx: _rx, ry: _ry, sinφ: sinφ, cosφ: cosφ, center: center) + curves.append(.cubicBezierCurve(cp1: _cp1, cp2: _cp2, point: _point)) + } + + return curves + } +} + +private func arcCenter(previousPoint: Point, point: Point, rx: Double, ry: Double, largeArc: Bool, clockwise: Bool, sinφ: Double, cosφ: Double, pxp: Double, pyp: Double) -> + (center: Point, angle1: Double, angle2: Double) +{ + + let rxsq = pow(rx, 2.0) + let rysq = pow(ry, 2.0) + let pxpsq = pow(pxp, 2.0) + let pypsq = pow(pyp, 2.0) + + var radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq) + if radicant < 0.0 { + radicant = 0.0 + } + + radicant /= (rxsq * pypsq) + (rysq * pxpsq) + radicant = sqrt(radicant) * (largeArc == clockwise ? -1.0 : 1.0) + + let centerxp = radicant * rx / ry * pyp + let centeryp = radicant * -ry / rx * pxp + + let centerx = cosφ * centerxp - sinφ * centeryp + (previousPoint.x + point.x) / 2.0 + let centery = sinφ * centerxp + cosφ * centeryp + (previousPoint.x + point.x) / 2.0 + + let vx1 = (pxp - centerxp) / rx + let vy1 = (pyp - centeryp) / ry + let vx2 = (-pxp - centerxp) / rx + let vy2 = (-pyp - centeryp) / ry + + let angle1 = vectorAngle(u: Point(x: 1, y: 0), v: Point(x: vx1, y: vy1)) + var angle2 = vectorAngle(u: Point(x: vx1, y: vy1), v: Point(x: vx2, y: vy2)) + + if clockwise == false, angle2 > 0.0 { + angle2 -= (.pi * 2.0) + } else if clockwise == true, angle2 < 0.0 { + angle2 += (.pi * 2.0) + } + + return (Point(x: centerx, y: centery), angle1, angle2) +} + +private func vectorAngle(u: Point, v: Point) -> Double { + let sign: Double = ((u.x * v.y - u.y * v.x) < 0.0) ? -1.0 : 1.0 + var dot = u.x * v.x + u.y * v.y + if dot > 1.0 { + dot = 1.0 + } else if dot < -1.0 { + dot = -1.0 + } + + return sign * acos(dot) +} + +private func approximateUnitArc(angle1: Double, angle2: Double) -> (Point, Point, Point) { + // If 90 degree circular arc, use a constant + // as derived from http://spencermortensen.com/articles/bezier-circle + let a: Double = switch angle2 { + case 1.5707963267948966: + 0.551915024494 + case -1.5707963267948966: + -0.551915024494 + default: + 4.0 / 3.0 * tan(angle2 / 4.0) + } + + let x1 = cos(angle1) + let y1 = sin(angle1) + let x2 = cos(angle1 + angle2) + let y2 = sin(angle1 + angle2) + + return (Point(x: x1 - y1 * a, y: y1 + x1 * a), Point(x: x2 + y2 * a, y: y2 - x2 * 1), Point(x: x2, y: y2)) +} + +private func mapToEllipse(point: Point, rx: Double, ry: Double, sinφ: Double, cosφ: Double, center: Point) -> Point { + let x = point.x * rx + let y = point.y * ry + let xp = cosφ * x - sinφ * y + let yp = sinφ * x + cosφ * y + return Point(x: xp + center.x, y: yp + center.y) +} diff --git a/third-party/VectorPlus/Sources/Pigment+VectorPlus.swift b/third-party/VectorPlus/Sources/Pigment+VectorPlus.swift new file mode 100644 index 0000000000..ea9b3e7e04 --- /dev/null +++ b/third-party/VectorPlus/Sources/Pigment+VectorPlus.swift @@ -0,0 +1,7 @@ +import SwiftColor + +public extension Pigment { + var coreGraphicsDescription: String { + "CGColor(srgbRed: \(red), green: \(green), blue: \(blue), alpha: \(alpha))" + } +} diff --git a/third-party/VectorPlus/Sources/Point+VectorPlus.swift b/third-party/VectorPlus/Sources/Point+VectorPlus.swift new file mode 100644 index 0000000000..716a66f023 --- /dev/null +++ b/third-party/VectorPlus/Sources/Point+VectorPlus.swift @@ -0,0 +1,7 @@ +import Swift2D + +public extension Point { + var coreGraphicsDescription: String { + "CGPoint(x: \(x), y: \(y))" + } +} diff --git a/third-party/VectorPlus/Sources/Rect+VectorPlus.swift b/third-party/VectorPlus/Sources/Rect+VectorPlus.swift new file mode 100644 index 0000000000..d66f66f6d6 --- /dev/null +++ b/third-party/VectorPlus/Sources/Rect+VectorPlus.swift @@ -0,0 +1,7 @@ +import Swift2D + +public extension Rect { + var coreGraphicsDescription: String { + "CGRect(origin: \(origin.coreGraphicsDescription), size: \(size.coreGraphicsDescription))" + } +} diff --git a/third-party/VectorPlus/Sources/SVG+AppleSymbols.swift b/third-party/VectorPlus/Sources/SVG+AppleSymbols.swift new file mode 100644 index 0000000000..8eb5759507 --- /dev/null +++ b/third-party/VectorPlus/Sources/SVG+AppleSymbols.swift @@ -0,0 +1,324 @@ +import Foundation +import Swift2D +import SwiftSVG +import XMLCoder + +public extension SVG { + + static func encodeDocument(_ document: SVG, encoder: XMLEncoder = XMLEncoder()) throws -> Data { + let rootAttributes: [String: String] = [ + "version": "1.1", + "xmlns": "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink", + ] + let header = XMLHeader(version: 1.0, encoding: "UTF-8", standalone: nil) + return try encoder.encode(document, withRootKey: "svg", rootAttributes: rootAttributes, header: header) + } + + static func appleSymbols(path: Path, in rect: Rect) throws -> SVG { + var document = SVG(width: 3300, height: 2200) + document.groups = try [.appleSymbolsNotes, .appleSymbolsGuides, .appleSymbols(path: path, in: rect)] + return document + } +} + +public extension Group { + static var appleSymbolsNotes: Group { + var group = Group() + + group.id = "Notes" + group.rectangles = [] + group.lines = [] + group.texts = [] + group.groups = [] + + var artboard = Rectangle(x: 0, y: 0, width: 3300, height: 2200) + artboard.id = "artboard" + artboard.style = "fill:white;opacity:1" + group.rectangles?.append(artboard) + + var topLine = Line(x1: 263, y1: 292, x2: 3036, y2: 292) + topLine.style = "fill:none;stroke:black;opacity:1;stroke-width:0.5;" + group.lines?.append(topLine) + + var bottomLine = Line(x1: 263, y1: 1903, x2: 3036, y2: 1903) + bottomLine.style = "fill:none;stroke:black;opacity:1;stroke-width:0.5;" + group.lines?.append(bottomLine) + + var text = Text() + text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;" + text.transform = "matrix(1 0 0 1 263 322)" + text.value = "Weight/Scale Variations" + group.texts?.append(text) + + text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;text-anchor:middle" + text.transform = "matrix(1 0 0 1 559.711 322)" + text.value = "Ultralight" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 856.422 322)" + text.value = "Thin" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 1153.13 322)" + text.value = "Light" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 1449.84 322)" + text.value = "Regular" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 1746.56 322)" + text.value = "Medium" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 2043.27 322)" + text.value = "Semibold" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 2339.98 322)" + text.value = "Bold" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 2636.69 322)" + text.value = "Heavy" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 2933.4 322)" + text.value = "Black" + group.texts?.append(text) + + var path = Path(data: "M 9.24805 0.830078 C 13.5547 0.830078 17.1387 -2.74414 17.1387 -7.05078 C 17.1387 -11.3574 13.5449 -14.9316 9.23828 -14.9316 C 4.94141 -14.9316 1.36719 -11.3574 1.36719 -7.05078 C 1.36719 -2.74414 4.95117 0.830078 9.24805 0.830078 Z M 9.24805 -0.654297 C 5.70312 -0.654297 2.87109 -3.49609 2.87109 -7.05078 C 2.87109 -10.6055 5.69336 -13.4473 9.23828 -13.4473 C 12.793 -13.4473 15.6348 -10.6055 15.6445 -7.05078 C 15.6543 -3.49609 12.8027 -0.654297 9.24805 -0.654297 Z M 9.22852 -3.42773 C 9.69727 -3.42773 9.9707 -3.74023 9.9707 -4.25781 L 9.9707 -6.31836 L 12.1973 -6.31836 C 12.6953 -6.31836 13.0371 -6.57227 13.0371 -7.04102 C 13.0371 -7.51953 12.7148 -7.7832 12.1973 -7.7832 L 9.9707 -7.7832 L 9.9707 -10.0098 C 9.9707 -10.5273 9.69727 -10.8496 9.22852 -10.8496 C 8.75977 -10.8496 8.50586 -10.5078 8.50586 -10.0098 L 8.50586 -7.7832 L 6.29883 -7.7832 C 5.78125 -7.7832 5.44922 -7.51953 5.44922 -7.04102 C 5.44922 -6.57227 5.80078 -6.31836 6.29883 -6.31836 L 8.50586 -6.31836 L 8.50586 -4.25781 C 8.50586 -3.75977 8.75977 -3.42773 9.22852 -3.42773 Z") + var subGroup = Group("", path: path, transform: "matrix(1 0 0 1 263 1933)") + group.groups?.append(subGroup) + + path.data = "M 11.709 2.91016 C 17.1582 2.91016 21.6699 -1.60156 21.6699 -7.05078 C 21.6699 -12.4902 17.1484 -17.0117 11.6992 -17.0117 C 6.25977 -17.0117 1.74805 -12.4902 1.74805 -7.05078 C 1.74805 -1.60156 6.26953 2.91016 11.709 2.91016 Z M 11.709 1.25 C 7.09961 1.25 3.41797 -2.44141 3.41797 -7.05078 C 3.41797 -11.6504 7.08984 -15.3516 11.6992 -15.3516 C 16.3086 -15.3516 20 -11.6504 20.0098 -7.05078 C 20.0195 -2.44141 16.3184 1.25 11.709 1.25 Z M 11.6895 -2.41211 C 12.207 -2.41211 12.5195 -2.77344 12.5195 -3.33984 L 12.5195 -6.23047 L 15.5762 -6.23047 C 16.123 -6.23047 16.5039 -6.51367 16.5039 -7.03125 C 16.5039 -7.55859 16.1426 -7.86133 15.5762 -7.86133 L 12.5195 -7.86133 L 12.5195 -10.9277 C 12.5195 -11.5039 12.207 -11.8555 11.6895 -11.8555 C 11.1719 -11.8555 10.8789 -11.4844 10.8789 -10.9277 L 10.8789 -7.86133 L 7.83203 -7.86133 C 7.26562 -7.86133 6.89453 -7.55859 6.89453 -7.03125 C 6.89453 -6.51367 7.28516 -6.23047 7.83203 -6.23047 L 10.8789 -6.23047 L 10.8789 -3.33984 C 10.8789 -2.79297 11.1719 -2.41211 11.6895 -2.41211 Z" + subGroup.paths = [path] + subGroup.transform = "matrix(1 0 0 1 281.506 1933)" + group.groups?.append(subGroup) + + path.data = "M 14.9707 5.67383 C 21.9336 5.67383 27.6953 -0.078125 27.6953 -7.04102 C 27.6953 -14.0039 21.9238 -19.7559 14.9609 -19.7559 C 8.00781 -19.7559 2.25586 -14.0039 2.25586 -7.04102 C 2.25586 -0.078125 8.01758 5.67383 14.9707 5.67383 Z M 14.9707 3.85742 C 8.93555 3.85742 4.08203 -1.00586 4.08203 -7.04102 C 4.08203 -13.0762 8.92578 -17.9395 14.9609 -17.9395 C 21.0059 -17.9395 25.8594 -13.0762 25.8691 -7.04102 C 25.8789 -1.00586 21.0156 3.85742 14.9707 3.85742 Z M 14.9512 -1.06445 C 15.5176 -1.06445 15.8691 -1.45508 15.8691 -2.06055 L 15.8691 -6.13281 L 20.1074 -6.13281 C 20.6934 -6.13281 21.1133 -6.46484 21.1133 -7.02148 C 21.1133 -7.59766 20.7227 -7.93945 20.1074 -7.93945 L 15.8691 -7.93945 L 15.8691 -12.1875 C 15.8691 -12.8027 15.5176 -13.1934 14.9512 -13.1934 C 14.3848 -13.1934 14.0625 -12.7832 14.0625 -12.1875 L 14.0625 -7.93945 L 9.83398 -7.93945 C 9.21875 -7.93945 8.80859 -7.59766 8.80859 -7.02148 C 8.80859 -6.46484 9.23828 -6.13281 9.83398 -6.13281 L 14.0625 -6.13281 L 14.0625 -2.06055 C 14.0625 -1.47461 14.3848 -1.06445 14.9512 -1.06445 Z" + subGroup.paths = [path] + subGroup.transform = "matrix(1 0 0 1 304.924 1933)" + group.groups?.append(subGroup) + + text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;" + text.transform = "matrix(1 0 0 1 263 1953)" + text.value = "Design Variations" + group.texts?.append(text) + + text.style = "none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;" + text.transform = "matrix(1 0 0 1 263 1971)" + text.value = "Symbols are supported in up to nine weights and three scales." + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 263 1989)" + text.value = "For optimal layout with text and other symbols, vertically align" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 263 2007)" + text.value = "symbols with the adjacent text." + group.texts?.append(text) + + var rect = Rectangle(x: 776, y: 1919, width: 3, height: 14) + rect.style = "fill:#00AEEF;stroke:none;opacity:0.4;" + group.rectangles?.append(rect) + + path.data = "M 10.5273 0 L 12.373 0 L 7.17773 -14.0918 L 5.43945 -14.0918 L 0.244141 0 L 2.08984 0 L 3.50586 -4.0332 L 9.11133 -4.0332 Z M 6.2793 -11.9531 L 6.33789 -11.9531 L 8.59375 -5.52734 L 4.02344 -5.52734 Z" + subGroup.paths = [path] + subGroup.transform = "matrix(1 0 0 1 779 1933)" + group.groups?.append(subGroup) + + rect.x = 791.617 + group.rectangles?.append(rect) + + text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;" + text.transform = "matrix(1 0 0 1 776 1953)" + text.value = "Margins" + group.texts?.append(text) + + text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;" + text.transform = "matrix(1 0 0 1 776 1971)" + text.value = "Leading and trailing margins on the left and right side of each symbol" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 776 1989)" + text.value = "can be adjusted by modifying the width of the blue rectangles." + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 776 2007)" + text.value = "Modifications are automatically applied proportionally to all" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 776 2025)" + text.value = "scales and weights." + group.texts?.append(text) + + path.data = "M 2.83203 3.11523 L 4.375 4.6582 C 5.22461 5.48828 6.19141 5.41992 7.06055 4.46289 L 17.2754 -6.66016 C 17.7051 -6.36719 18.0957 -6.37695 18.5645 -6.47461 L 19.6094 -6.68945 L 20.3027 -5.99609 L 20.2539 -5.47852 C 20.1855 -4.95117 20.3516 -4.53125 20.8496 -4.0332 L 21.6602 -3.22266 C 22.168 -2.71484 22.8223 -2.68555 23.3008 -3.16406 L 26.5527 -6.41602 C 27.0312 -6.89453 27.0117 -7.54883 26.5039 -8.05664 L 25.6836 -8.87695 C 25.1855 -9.375 24.7754 -9.55078 24.2383 -9.47266 L 23.7109 -9.41406 L 23.0566 -10.0781 L 23.3398 -11.2207 C 23.4863 -11.7871 23.3398 -12.2559 22.7148 -12.8613 L 20.3027 -15.2539 C 16.7578 -18.7793 12.2266 -18.6719 9.11133 -15.5371 C 8.69141 -15.1074 8.64258 -14.5215 8.91602 -14.0918 C 9.15039 -13.7207 9.62891 -13.4961 10.2734 -13.6621 C 11.7871 -14.043 13.3008 -13.9258 14.7852 -12.9199 L 14.1602 -11.3379 C 13.9258 -10.752 13.9453 -10.2734 14.1797 -9.83398 L 3.01758 0.439453 C 2.08008 1.30859 1.97266 2.25586 2.83203 3.11523 Z M 10.6738 -15.1465 C 13.3398 -17.1387 16.6504 -16.8262 19.0527 -14.4141 L 21.6797 -11.8066 C 21.9141 -11.5723 21.9434 -11.3867 21.8848 -11.0938 L 21.5039 -9.53125 L 23.0762 -7.95898 L 24.043 -8.04688 C 24.3262 -8.07617 24.4141 -8.05664 24.6387 -7.83203 L 25.2637 -7.20703 L 22.5098 -4.46289 L 21.8848 -5.07812 C 21.6602 -5.30273 21.6406 -5.40039 21.6699 -5.68359 L 21.7578 -6.64062 L 20.1953 -8.20312 L 18.5742 -7.89062 C 18.291 -7.83203 18.1445 -7.83203 17.9102 -8.07617 L 15.7324 -10.2539 C 15.5078 -10.4883 15.4785 -10.625 15.6055 -10.9473 L 16.5527 -13.2227 C 14.9512 -14.7559 12.8418 -15.6055 10.8008 -14.9512 C 10.7129 -14.9219 10.6445 -14.9414 10.6152 -14.9805 C 10.5859 -15.0293 10.5859 -15.0781 10.6738 -15.1465 Z M 4.10156 2.41211 C 3.61328 1.91406 3.78906 1.61133 4.12109 1.30859 L 15.0781 -8.80859 L 16.3086 -7.57812 L 6.15234 3.34961 C 5.84961 3.68164 5.46875 3.7793 5.06836 3.37891 Z" + subGroup.paths = [path] + subGroup.transform = "matrix(1 0 0 1 1289 1933)" + group.groups?.append(subGroup) + + text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;" + text.transform = "matrix(1 0 0 1 1289 1953)" + text.value = "Exporting" + group.texts?.append(text) + + text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;" + text.transform = "matrix(1 0 0 1 1289 1971)" + text.value = "Symbols should be outlined when exporting to ensure the" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 1289 1989)" + text.value = "design is preserved when submitting to Xcode." + group.texts?.append(text) + + text.id = "template-version" + text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;text-anchor:end;" + text.transform = "matrix(1 0 0 1 3036 1933)" + text.value = "Template v.2.0" + group.texts?.append(text) + + text.id = nil + text.transform = "matrix(1 0 0 1 3036 1969)" + text.value = "Typeset at 100 points" + group.texts?.append(text) + + text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;" + text.transform = "matrix(1 0 0 1 263 726)" + text.value = "Small" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 263 1156)" + text.value = "Medium" + group.texts?.append(text) + + text.transform = "matrix(1 0 0 1 263 1586)" + text.value = "Large" + group.texts?.append(text) + + return group + } + + static var appleSymbolsGuides: Group { + var group = Group() + + group.id = "Guides" + group.groups = [] + group.lines = [] + + var hRef = Group() + hRef.id = "H-reference" + hRef.style = "fill:#27AAE1;stroke:none;" + hRef.transform = "matrix(1 0 0 1 339 696)" + hRef.paths = [Path(data: "M 54.9316 0 L 57.666 0 L 30.5664 -70.459 L 28.0762 -70.459 L 0.976562 0 L 3.66211 0 L 12.9395 -24.4629 L 45.7031 -24.4629 Z M 29.1992 -67.0898 L 29.4434 -67.0898 L 44.8242 -26.709 L 13.8184 -26.709 Z")] + group.groups?.append(hRef) + + var baseline = Line(x1: 263, y1: 696, x2: 3036, y2: 696) + baseline.id = "Baseline-S" + baseline.style = "fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.577;" + group.lines?.append(baseline) + + var capline = Line(x1: 263, y1: 625.541, x2: 3036, y2: 625.541) + capline.id = "Capline-S" + capline.style = "fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.577;" + group.lines?.append(capline) + + hRef.transform = "matrix(1 0 0 1 339 1126)" + hRef.paths = [Path(data: "M 54.9316 0 L 57.666 0 L 30.5664 -70.459 L 28.0762 -70.459 L 0.976562 0 L 3.66211 0 L 12.9395 -24.4629 L 45.7031 -24.4629 Z M 29.1992 -67.0898 L 29.4434 -67.0898 L 44.8242 -26.709 L 13.8184 -26.709 Z")] + group.groups?.append(hRef) + + baseline.id = "Baseline-M" + baseline.y1 = 1126 + baseline.y2 = 1126 + group.lines?.append(baseline) + + capline.id = "Capline-M" + capline.y1 = 1055.54 + capline.y2 = 1055.54 + group.lines?.append(capline) + + hRef.transform = "matrix(1 0 0 1 339 1556)" + hRef.paths = [Path(data: "M 54.9316 0 L 57.666 0 L 30.5664 -70.459 L 28.0762 -70.459 L 0.976562 0 L 3.66211 0 L 12.9395 -24.4629 L 45.7031 -24.4629 Z M 29.1992 -67.0898 L 29.4434 -67.0898 L 44.8242 -26.709 L 13.8184 -26.709 Z")] + group.groups?.append(hRef) + + baseline.id = "Baseline-L" + baseline.y1 = 1556 + baseline.y2 = 1556 + group.lines?.append(baseline) + + capline.id = "Capline-L" + capline.y1 = 1485.54 + capline.y2 = 1485.54 + group.lines?.append(capline) + + var margin = Line(x1: 1399.72, y1: 1030.79, x2: 1399.72, y2: 1150.12) + margin.id = "left-margin" + margin.style = "fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" + group.lines?.append(margin) + + margin = Line(x1: 1499.97, y1: 1030.79, x2: 1499.97, y2: 1150.12) + margin.id = "right-margin" + margin.style = "fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" + group.lines?.append(margin) + + return group + } + + static func appleSymbols(path: Path, in rect: Rect) throws -> Group { + var group = Group() + + group.id = "Symbols" + + let translations: [(name: String, size: Float, center: Point)] = [ + ("Ultralight-S", 0.75, .init(x: 559.0, y: 661.0)), + ("Thin-S", 0.76, .init(x: 857.0, y: 661.0)), + ("Light-S", 0.78, .init(x: 1153.0, y: 661.0)), + ("Regular-S", 0.79, .init(x: 1449.5, y: 661.0)), + ("Medium-S", 0.80, .init(x: 1747.0, y: 661.0)), + ("Semibold-S", 0.81, .init(x: 2043.0, y: 661.0)), + ("Bold-S", 0.82, .init(x: 2340.0, y: 661.0)), + ("Heavy-S", 0.85, .init(x: 2636.5, y: 661.0)), + ("Black-S", 0.86, .init(x: 2933.0, y: 661.0)), + ("Ultralight-M", 0.95, .init(x: 559.0, y: 1091.0)), + ("Thin-M", 0.96, .init(x: 857.0, y: 1091.0)), + ("Light-M", 0.98, .init(x: 1153.0, y: 1091.0)), + ("Regular-M", 1.00, .init(x: 1449.5, y: 1091.0)), + ("Medium-M", 1.02, .init(x: 1747.0, y: 1091.0)), + ("Semibold-M", 1.03, .init(x: 2043.0, y: 1091.0)), + ("Bold-M", 1.05, .init(x: 2340.0, y: 1091.0)), + ("Heavy-M", 1.07, .init(x: 2636.5, y: 1091.0)), + ("Black-M", 1.10, .init(x: 2933.0, y: 1091.0)), + ("Ultralight-L", 1.22, .init(x: 559.0, y: 1521.0)), + ("Thin-L", 1.24, .init(x: 857.0, y: 1521.0)), + ("Light-L", 1.26, .init(x: 1153.0, y: 1521.0)), + ("Regular-L", 1.28, .init(x: 1449.5, y: 1521.0)), + ("Medium-L", 1.30, .init(x: 1747.0, y: 1521.0)), + ("Semibold-L", 1.31, .init(x: 2043.0, y: 1521.0)), + ("Bold-L", 1.33, .init(x: 2340.0, y: 1521.0)), + ("Heavy-L", 1.36, .init(x: 2636.5, y: 1521.0)), + ("Black-L", 1.39, .init(x: 2933.0, y: 1521.0)), + ] + + group.groups = try translations.map { symbol -> Group in + let size = Size(width: 100.0 * symbol.size, height: 100.0 * symbol.size) + let to = Rect(origin: .zero, size: size) + let commands = try path.commands().map { $0.translate(from: rect, to: to) } + let p = Path(commands: commands) + let matrixOrigin = Point(x: symbol.center.x - size.width / 2.0, y: symbol.center.y - size.height / 2.0) + let matrix: Transformation = .matrix(a: 1, b: 0, c: 0, d: 1, e: matrixOrigin.x, f: matrixOrigin.y) + return Group(symbol.name, path: p, transform: matrix.description) + } + + return group + } +} + +private extension Group { + init(_ id: String, path: Path, transform: String) { + self.init() + self.id = id + self.transform = transform + paths = [path] + } +} diff --git a/third-party/VectorPlus/Sources/SVG+Template.swift b/third-party/VectorPlus/Sources/SVG+Template.swift new file mode 100644 index 0000000000..6dbd962d84 --- /dev/null +++ b/third-party/VectorPlus/Sources/SVG+Template.swift @@ -0,0 +1,56 @@ +import Foundation +import Swift2D +import SwiftSVG + +public extension SVG { + func asImageViewSubclass() throws -> String { + let instructions = try asCoreGraphicsDescription() + let renders = try asCGContextDescription() + + return imageViewSubclassTemplate + .replacingOccurrences(of: "{{name}}", with: name) + .replacingOccurrences(of: "{{width}}", with: String(format: "%.1f", originalSize.width)) + .replacingOccurrences(of: "{{height}}", with: String(format: "%.1f", originalSize.height)) + .replacingOccurrences(of: "{{instructions}}", with: instructions) + .replacingOccurrences(of: "{{render}}", with: renders) + } +} + +private extension SVG { + func asCoreGraphicsDescription(variable: String = "path") throws -> String { + try subpaths().map { try $0.asCoreGraphicsDescription(variable: variable, originalSize: originalSize) }.joined(separator: "\n ") + } + + func asCGContextDescription() throws -> String { + var outputs: [String] = [] + + let paths = try subpaths() + try paths.forEach { path in + let instructions = try path.asCoreGraphicsDescription(variable: "path", originalSize: originalSize) + let fillColor = path.fill?.pigment?.coreGraphicsDescription ?? "nil" + let fillOpacity = (path.fillOpacity != nil) ? "\(path.fillOpacity!)" : "nil" + let fillRule = (path.fillRule ?? .nonZero).coreGraphicsDescription + let strokeColor = path.stroke?.pigment?.coreGraphicsDescription ?? "nil" + let strokeOpacity = (path.strokeOpacity != nil) ? "\(path.strokeOpacity!)" : "nil" + let strokeWidth = (path.strokeWidth != nil) ? "\(path.strokeWidth!) * (size.width / width)" : "nil" + let strokeLineCap = (path.strokeLineCap != nil) ? "\(path.strokeLineCap!.coreGraphicsDescription)" : "nil" + let strokeLineJoin = (path.strokeLineJoin != nil) ? "\(path.strokeLineJoin!.coreGraphicsDescription)" : "nil" + let strokeMiterLimit = (path.strokeMiterLimit != nil) ? "\(path.strokeMiterLimit!)" : "nil" + + outputs.append(contextTemplate + .replacingOccurrences(of: "{{instructions}}", with: instructions) + .replacingOccurrences(of: "{{fillColor}}", with: fillColor) + .replacingOccurrences(of: "{{fillOpacity}}", with: fillOpacity) + .replacingOccurrences(of: "{{fillRule}}", with: fillRule) + .replacingOccurrences(of: "{{strokeColor}}", with: strokeColor) + .replacingOccurrences(of: "{{strokeOpacity}}", with: strokeOpacity) + .replacingOccurrences(of: "{{strokeWidth}}", with: strokeWidth) + .replacingOccurrences(of: "{{strokeLineCap}}", with: strokeLineCap) + .replacingOccurrences(of: "{{strokeLineJoin}}", with: strokeLineJoin) + .replacingOccurrences(of: "{{strokeMiterLimit}}", with: strokeMiterLimit) + ) + } + + return outputs.joined(separator: "\n ") + } +} diff --git a/third-party/VectorPlus/Sources/Size+VectorPlus.swift b/third-party/VectorPlus/Sources/Size+VectorPlus.swift new file mode 100644 index 0000000000..3ec4a7bbd4 --- /dev/null +++ b/third-party/VectorPlus/Sources/Size+VectorPlus.swift @@ -0,0 +1,7 @@ +import Swift2D + +public extension Size { + var coreGraphicsDescription: String { + "CGSize(width: \(width), height: \(height))" + } +} diff --git a/third-party/VectorPlus/Sources/Stroke+VectorPlus.swift b/third-party/VectorPlus/Sources/Stroke+VectorPlus.swift new file mode 100644 index 0000000000..0d933d2aca --- /dev/null +++ b/third-party/VectorPlus/Sources/Stroke+VectorPlus.swift @@ -0,0 +1,40 @@ +import SwiftColor +import SwiftSVG + +public extension Stroke { + @available(*, deprecated, renamed: "pigment") + var swiftColor: Pigment? { pigment } + + var pigment: Pigment? { + guard let color, !color.isEmpty else { + return nil + } + + let _color = Pigment(color) + guard _color.alpha != 0.0 else { + return nil + } + + return _color + } +} + +public extension Stroke.LineCap { + var coreGraphicsDescription: String { + switch self { + case .butt: ".butt" + case .round: ".round" + case .square: ".square" + } + } +} + +public extension Stroke.LineJoin { + var coreGraphicsDescription: String { + switch self { + case .bevel: ".bevel" + case .arcs, .miter, .miterClip: ".miter" + case .round: ".round" + } + } +} diff --git a/third-party/VectorPlus/Sources/Template+UIImageView.swift b/third-party/VectorPlus/Sources/Template+UIImageView.swift new file mode 100644 index 0000000000..a6b03c8f47 --- /dev/null +++ b/third-party/VectorPlus/Sources/Template+UIImageView.swift @@ -0,0 +1,177 @@ +let imageViewSubclassTemplate: String = """ +#if canImport(UIKit) +import UIKit + +@IBDesignable +public class {{name}}: UIImageView { + + public static let width: CGFloat = {{width}} + public static let height: CGFloat = {{height}} + public let width: CGFloat = {{width}} + public let height: CGFloat = {{height}} + + public var widthToHeightAspectRatio: CGFloat { + guard width != .nan, width > 0.0 else { + return 0.0 + } + + guard height != .nan, height > 0.0 else { + return 0.0 + } + + return width / height + } + + public var heightToWidthAspectRatio: CGFloat { + guard height != .nan, height > 0.0 else { + return 0.0 + } + + guard width != .nan, width > 0.0 else { + return 0.0 + } + + return height / width + } + + public override init(frame: CGRect) { + super.init(frame: frame) + updateSubviews() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + updateSubviews() + } + + public override var intrinsicContentSize: CGSize { + return CGSize(width: width, height: height) + } + + public override var bounds: CGRect { + didSet { + updateSubviews() + } + } + + public override func prepareForInterfaceBuilder() { + super.prepareForInterfaceBuilder() + updateSubviews() + } + + public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + updateSubviews() + } + + public func updateSubviews() { + image = Self.image(size: bounds.size) + } + + public static func path(size: CGSize) -> CGPath { + guard size.height > 0.0 && size.width > 0.0 else { + return CGMutablePath() + } + + let radius = max(size.width / 2.0, size.height / 2.0) + let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + + let path = CGMutablePath() + {{instructions}} + return path + } + + public static func image(size: CGSize) -> UIImage? { + guard size.height > 0.0 && size.width > 0.0 else { + return nil + } + + let radius = max(size.width / 2.0, size.height / 2.0) + let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + + defer { + UIGraphicsEndImageContext() + } + + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + + guard let context = UIGraphicsGetCurrentContext() else { + return nil + } + + {{render}} + + return UIGraphicsGetImageFromCurrentImageContext() + } + + private static func radians(_ degree: Float) -> CGFloat { + return CGFloat(degree) * (.pi / CGFloat(180)) + } +} + +private extension CGContext { + func rendering(_ block: (CGContext) -> Void) { + block(self) + } +} + +#endif + +""" + +let contextTemplate: String = """ + context.rendering { (ctx) in + ctx.saveGState() + + let path = CGMutablePath() + {{instructions}} + + let defaultColor: CGColor = CGColor(srgbRed: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) + let pathFillColor: CGColor? = {{fillColor}} + let pathFillOpacity: CGFloat? = {{fillOpacity}} + let pathFillRule: CGPathFillRule = {{fillRule}} + let pathStrokeColor: CGColor? = {{strokeColor}} + let pathStrokeOpacity: CGFloat? = {{strokeOpacity}} + let pathStrokeWidth: CGFloat? = {{strokeWidth}} + let pathStrokeLineCap: CGLineCap? = {{strokeLineCap}} + let pathStrokeLineJoin: CGLineJoin? = {{strokeLineJoin}} + let pathStrokeMiterLimit: CGFloat? = {{strokeMiterLimit}} + + if pathFillColor != nil && pathFillOpacity != nil { + let opacity = pathFillOpacity ?? 1.0 + let color = (pathFillColor ?? defaultColor).copy(alpha: opacity) ?? defaultColor + + ctx.setFillColor(color) + ctx.addPath(path) + ctx.fillPath(using: pathFillRule) + } + + if pathStrokeColor != nil && pathStrokeOpacity != nil { + let opacity = pathStrokeOpacity ?? 1.0 + let color = (pathStrokeColor ?? defaultColor).copy(alpha: opacity) ?? defaultColor + let lineWidth = pathStrokeWidth ?? 1.0 + + ctx.setLineWidth(lineWidth) + ctx.setStrokeColor(color) + if let lineCap = pathStrokeLineCap { + ctx.setLineCap(lineCap) + } + if let lineJoin = pathStrokeLineJoin { + ctx.setLineJoin(lineJoin) + if let miterLimit = pathStrokeMiterLimit, lineJoin == .miter { + ctx.setMiterLimit(miterLimit) + } + } + ctx.addPath(path) + ctx.strokePath() + } + + if (pathFillColor == nil && pathFillOpacity == nil) && (pathStrokeColor == nil && pathStrokeOpacity == nil) { + ctx.setFillColor(defaultColor) + ctx.addPath(path) + ctx.fillPath(using: pathFillRule) + } + + ctx.restoreGState() + } +""" diff --git a/third-party/VectorPlus/Sources/UIKit/SVG+UIImage.swift b/third-party/VectorPlus/Sources/UIKit/SVG+UIImage.swift new file mode 100644 index 0000000000..ea336be4f7 --- /dev/null +++ b/third-party/VectorPlus/Sources/UIKit/SVG+UIImage.swift @@ -0,0 +1,43 @@ +#if canImport(UIKit) +import Swift2D +import SwiftSVG +import UIKit + +public extension SVG { + func uiImage(size: Size) -> UIImage? { + guard size.height > 0.0, size.width > 0.0 else { + return nil + } + + let from = Rect(origin: .zero, size: originalSize) + let to = Rect(origin: .zero, size: size) + + let paths: [Path] + do { + paths = try subpaths() + } catch { + return nil + } + + defer { + UIGraphicsEndImageContext() + } + + UIGraphicsBeginImageContextWithOptions(CGSize(size), false, 0.0) + + guard let context = UIGraphicsGetCurrentContext() else { + return nil + } + + for path in paths { + try? context.render(path: path, from: from, to: to) + } + + return UIGraphicsGetImageFromCurrentImageContext() + } + + func pngData(size: Size) -> Data? { + uiImage(size: size)?.pngData() + } +} +#endif diff --git a/third-party/VectorPlus/Sources/UIKit/SVGImageView.swift b/third-party/VectorPlus/Sources/UIKit/SVGImageView.swift new file mode 100644 index 0000000000..b5131c915a --- /dev/null +++ b/third-party/VectorPlus/Sources/UIKit/SVGImageView.swift @@ -0,0 +1,81 @@ +import Swift2D +import SwiftSVG +#if canImport(UIKit) && !os(watchOS) +import UIKit + +@IBDesignable open class SVGImageView: UIImageView { + + public var width: CGFloat { + CGFloat(svg.originalSize.width) + } + + public var height: CGFloat { + CGFloat(svg.originalSize.height) + } + + open var svg: SVG = SVG() { + didSet { + updateSubviews() + } + } + + public var widthToHeightAspectRatio: CGFloat { + guard !width.isNaN, width > 0.0 else { + return 0.0 + } + + guard !height.isNaN, height > 0.0 else { + return 0.0 + } + + return width / height + } + + public var heightToWidthAspectRatio: CGFloat { + guard !height.isNaN, height > 0.0 else { + return 0.0 + } + + guard !width.isNaN, width > 0.0 else { + return 0.0 + } + + return height / width + } + + override public init(frame: CGRect) { + super.init(frame: frame) + updateSubviews() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + updateSubviews() + } + + override public var intrinsicContentSize: CGSize { + CGSize(width: width, height: height) + } + + override public var bounds: CGRect { + didSet { + updateSubviews() + } + } + + override public func prepareForInterfaceBuilder() { + super.prepareForInterfaceBuilder() + updateSubviews() + } + + override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + updateSubviews() + } + + public func updateSubviews() { + image = svg.uiImage(size: Size(bounds.size)) + } +} + +#endif diff --git a/third-party/VectorPlus/Sources/VectorPoint.swift b/third-party/VectorPlus/Sources/VectorPoint.swift new file mode 100644 index 0000000000..91f81b1d20 --- /dev/null +++ b/third-party/VectorPlus/Sources/VectorPoint.swift @@ -0,0 +1,126 @@ +import Swift2D + +/// A cartesian-based struct that describes the relationship of any particular `Point` to the _origin_ of a `Rect`. +public struct VectorPoint { + public enum Sign: String { + case plus = "+" + case minus = "-" + } + + public typealias Offset = (sign: Sign, multiplier: Double) + + public var x: Offset + public var y: Offset + + public init(x: Offset, y: Offset) { + self.x = x + self.y = y + } + + /// Initializes a `VectorPoint` for a given `Point` container in the provided `Rect`. + public init(point: Point, in rect: Rect) { + let radius = rect.size.maxRadius + let cartesianPoint = Self.cartesianPoint(for: point, in: rect) + + if cartesianPoint.x < 0 { + x = (.minus, abs(cartesianPoint.x) / radius) + } else { + x = (.plus, cartesianPoint.x / radius) + } + + if cartesianPoint.y < 0 { + y = (.plus, abs(cartesianPoint.y) / radius) + } else { + y = (.minus, cartesianPoint.y / radius) + } + } +} + +// MARK: - CustomStringConvertible + +extension VectorPoint: CustomStringConvertible { + public var description: String { + "VectorPoint(x: (\(x.sign.rawValue), \(x.multiplier)), y: (\(y.sign.rawValue), \(y.multiplier)))" + } +} + +// MARK: - Equatable + +extension VectorPoint: Equatable { + public static func == (lhs: VectorPoint, rhs: VectorPoint) -> Bool { + guard lhs.x.sign == rhs.x.sign else { + return false + } + guard lhs.x.multiplier == rhs.x.multiplier else { + return false + } + guard lhs.y.sign == rhs.y.sign else { + return false + } + guard lhs.y.multiplier == rhs.y.multiplier else { + return false + } + + return true + } +} + +// MARK: - + +public extension VectorPoint { + /// Translates the provided point within the `Rect` from using the top-left + /// as the _origin_, to using the center as the _origin_. + /// + /// For example: Given `Rect(x: 0, y: 0, width: 100, height: 100)`, the point + /// `Point(x: 25, y: 25)` would translate to `Point(x: -25, y: 25)`. + static func cartesianPoint(for point: Point, in rect: Rect) -> Point { + let origin = Point(x: rect.size.width / 2.0, y: rect.size.height / 2.0) + var cartesianPoint: Point = .zero + + if point.x < origin.x { + cartesianPoint = cartesianPoint.x(-(origin.x - point.x)) + } else if point.x > origin.x { + cartesianPoint = cartesianPoint.x(point.x - origin.x) + } + + if point.y > origin.y { + cartesianPoint = cartesianPoint.y(-(point.y - origin.y)) + } else if point.y < origin.y { + cartesianPoint = cartesianPoint.y(origin.y - point.y) + } + + return cartesianPoint + } +} + +// MARK: - Instance Functionality + +public extension VectorPoint { + /// Calculates the `Point` for this instance in the specified `Rect`. + func translate(to rect: Rect) -> Point { + translate(to: rect.size) + } + + /// Calculates the `Point` in the desired output size + func translate(to outputSize: Size) -> Point { + let center = outputSize.center + let radius = outputSize.minRadius + + switch (x.sign, y.sign) { + case (.plus, .plus): + return Point(x: center.x + (radius * x.multiplier), y: center.y + (radius * y.multiplier)) + case (.plus, .minus): + return Point(x: center.x + (radius * x.multiplier), y: center.y - (radius * y.multiplier)) + case (.minus, .plus): + return Point(x: center.x - (radius * x.multiplier), y: center.y + (radius * y.multiplier)) + case (.minus, .minus): + return Point(x: center.x - (radius * x.multiplier), y: center.y - (radius * y.multiplier)) + } + } +} + +public extension VectorPoint { + var coreGraphicsDescription: String { + "CGPoint(x: center.x \(x.sign.rawValue) (radius * \(x.multiplier)), y: center.y \(y.sign.rawValue) (radius * \(y.multiplier)))" + } +} diff --git a/third-party/XMLCoder/BUILD b/third-party/XMLCoder/BUILD new file mode 100644 index 0000000000..13656e4a0c --- /dev/null +++ b/third-party/XMLCoder/BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "XMLCoder", + module_name = "XMLCoder", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-suppress-warnings", + ], + deps = [ + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Attribute.swift b/third-party/XMLCoder/Sources/Auxiliaries/Attribute.swift new file mode 100644 index 0000000000..a908bee045 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Attribute.swift @@ -0,0 +1,99 @@ +// +// XMLAttribute.swift +// XMLCoder +// +// Created by Benjamin Wetherfield on 6/3/20. +// + +protocol XMLAttributeProtocol {} + +/** Property wrapper specifying that a given property should be encoded and decoded as an XML attribute. + + For example, this type + ```swift + struct Book: Codable { + @Attribute var id: Int + } + ``` + + will encode value `Book(id: 42)` as ``. And vice versa, + it will decode the former into the latter. + */ +@propertyWrapper +public struct Attribute: XMLAttributeProtocol { + public var wrappedValue: Value + + public init(_ wrappedValue: Value) { + self.wrappedValue = wrappedValue + } +} + +extension Attribute: Codable where Value: Codable { + public func encode(to encoder: Encoder) throws { + try wrappedValue.encode(to: encoder) + } + + public init(from decoder: Decoder) throws { + try wrappedValue = .init(from: decoder) + } +} + +extension Attribute: Equatable where Value: Equatable {} +extension Attribute: Hashable where Value: Hashable {} +extension Attribute: Sendable where Value: Sendable {} + +extension Attribute: ExpressibleByIntegerLiteral where Value: ExpressibleByIntegerLiteral { + public typealias IntegerLiteralType = Value.IntegerLiteralType + + public init(integerLiteral value: Value.IntegerLiteralType) { + wrappedValue = Value(integerLiteral: value) + } +} + +extension Attribute: ExpressibleByUnicodeScalarLiteral where Value: ExpressibleByUnicodeScalarLiteral { + public init(unicodeScalarLiteral value: Value.UnicodeScalarLiteralType) { + wrappedValue = Value(unicodeScalarLiteral: value) + } + + public typealias UnicodeScalarLiteralType = Value.UnicodeScalarLiteralType +} + +extension Attribute: ExpressibleByExtendedGraphemeClusterLiteral where Value: ExpressibleByExtendedGraphemeClusterLiteral { + public typealias ExtendedGraphemeClusterLiteralType = Value.ExtendedGraphemeClusterLiteralType + + public init(extendedGraphemeClusterLiteral value: Value.ExtendedGraphemeClusterLiteralType) { + wrappedValue = Value(extendedGraphemeClusterLiteral: value) + } +} + +extension Attribute: ExpressibleByStringLiteral where Value: ExpressibleByStringLiteral { + public typealias StringLiteralType = Value.StringLiteralType + + public init(stringLiteral value: Value.StringLiteralType) { + wrappedValue = Value(stringLiteral: value) + } +} + +extension Attribute: ExpressibleByBooleanLiteral where Value: ExpressibleByBooleanLiteral { + public typealias BooleanLiteralType = Value.BooleanLiteralType + + public init(booleanLiteral value: Value.BooleanLiteralType) { + wrappedValue = Value(booleanLiteral: value) + } +} + +extension Attribute: ExpressibleByNilLiteral where Value: ExpressibleByNilLiteral { + public init(nilLiteral: ()) { + wrappedValue = Value(nilLiteral: ()) + } +} + +protocol XMLOptionalAttributeProtocol: XMLAttributeProtocol { + init() +} + +extension Attribute: XMLOptionalAttributeProtocol where Value: AnyOptional { + init() { + wrappedValue = Value() + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/BoolBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/BoolBox.swift new file mode 100644 index 0000000000..5997804eda --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/BoolBox.swift @@ -0,0 +1,53 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/17/18. +// + +struct BoolBox: Equatable { + typealias Unboxed = Bool + + let unboxed: Unboxed + + init(_ unboxed: Unboxed) { + self.unboxed = unboxed + } + + init?(xmlString: String) { + switch xmlString.lowercased() { + case "false", "0", "n", "no": self.init(false) + case "true", "1", "y", "yes": self.init(true) + case _: return nil + } + } +} + +extension BoolBox: Box { + var isNull: Bool { + return false + } + + /// # Lexical representation + /// Boolean has a lexical representation consisting of the following + /// legal literals {`true`, `false`, `1`, `0`}. + /// + /// # Canonical representation + /// The canonical representation for boolean is the set of literals {`true`, `false`}. + /// + /// --- + /// + /// [Schema definition](https://www.w3.org/TR/xmlschema-2/#boolean) + var xmlString: String? { + return (unboxed) ? "true" : "false" + } +} + +extension BoolBox: SimpleBox {} + +extension BoolBox: CustomStringConvertible { + var description: String { + return unboxed.description + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/Box.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/Box.swift new file mode 100644 index 0000000000..71ecf82267 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/Box.swift @@ -0,0 +1,32 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/17/18. +// + +protocol Box { + var isNull: Bool { get } + var xmlString: String? { get } +} + +/// A box that only describes a single atomic value. +protocol SimpleBox: Box { + // A simple tagging protocol, for now. +} + +protocol TypeErasedSharedBoxProtocol { + func typeErasedUnbox() -> Box +} + +protocol SharedBoxProtocol: TypeErasedSharedBoxProtocol { + associatedtype B: Box + func unbox() -> B +} + +extension SharedBoxProtocol { + func typeErasedUnbox() -> Box { + return unbox() + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/ChoiceBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/ChoiceBox.swift new file mode 100644 index 0000000000..4d2f9b0289 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/ChoiceBox.swift @@ -0,0 +1,41 @@ +// Copyright (c) 2019-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by James Bean on 7/18/19. +// + +/// A `Box` which represents an element which is known to contain an XML choice element. +struct ChoiceBox { + var key: String = "" + var element: Box = NullBox() +} + +extension ChoiceBox: Box { + var isNull: Bool { + return false + } + + var xmlString: String? { + return nil + } +} + +extension ChoiceBox: SimpleBox {} + +extension ChoiceBox { + init?(_ keyedBox: KeyedBox) { + guard + let firstKey = keyedBox.elements.keys.first, + let firstElement = keyedBox.elements[firstKey].first + else { + return nil + } + self.init(key: firstKey, element: firstElement) + } + + init(_ singleKeyedBox: SingleKeyedBox) { + self.init(key: singleKeyedBox.key, element: singleKeyedBox.element) + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/DataBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/DataBox.swift new file mode 100644 index 0000000000..dfa7f94217 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/DataBox.swift @@ -0,0 +1,57 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/19/18. +// + +import Foundation + +struct DataBox: Equatable { + enum Format: Equatable { + case base64 + } + + typealias Unboxed = Data + + let unboxed: Unboxed + let format: Format + + init(_ unboxed: Unboxed, format: Format) { + self.unboxed = unboxed + self.format = format + } + + init?(base64 string: String) { + guard let data = Data(base64Encoded: string) else { + return nil + } + self.init(data, format: .base64) + } + + func xmlString(format: Format) -> String { + switch format { + case .base64: + return unboxed.base64EncodedString() + } + } +} + +extension DataBox: Box { + var isNull: Bool { + return false + } + + var xmlString: String? { + return xmlString(format: format) + } +} + +extension DataBox: SimpleBox {} + +extension DataBox: CustomStringConvertible { + var description: String { + return unboxed.description + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/DateBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/DateBox.swift new file mode 100644 index 0000000000..c18957b569 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/DateBox.swift @@ -0,0 +1,99 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/18/18. +// + +import Foundation + +struct DateBox: Equatable, Sendable { + enum Format: Equatable { + case secondsSince1970 + case millisecondsSince1970 + case iso8601 + case formatter(DateFormatter) + } + + typealias Unboxed = Date + + let unboxed: Unboxed + let format: Format + + init(_ unboxed: Unboxed, format: Format) { + self.unboxed = unboxed + self.format = format + } + + init?(secondsSince1970 string: String) { + guard let seconds = TimeInterval(string) else { + return nil + } + let unboxed = Date(timeIntervalSince1970: seconds) + self.init(unboxed, format: .secondsSince1970) + } + + init?(millisecondsSince1970 string: String) { + guard let milliseconds = TimeInterval(string) else { + return nil + } + let unboxed = Date(timeIntervalSince1970: milliseconds / 1000.0) + self.init(unboxed, format: .millisecondsSince1970) + } + + init?(iso8601 string: String) { + if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { + guard let unboxed = ISO8601DateFormatter.xmlCoderFormatter().date(from: string) else { + return nil + } + self.init(unboxed, format: .iso8601) + } else { + fatalError("ISO8601DateFormatter is unavailable on this platform.") + } + } + + init?(xmlString: String, formatter: DateFormatter) { + guard let date = formatter.date(from: xmlString) else { + return nil + } + self.init(date, format: .formatter(formatter)) + } + + func xmlString(format: Format) -> String { + switch format { + case .secondsSince1970: + let seconds = unboxed.timeIntervalSince1970 + return seconds.description + case .millisecondsSince1970: + let milliseconds = unboxed.timeIntervalSince1970 * 1000.0 + return milliseconds.description + case .iso8601: + if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { + return ISO8601DateFormatter.xmlCoderFormatter().string(from: self.unboxed) + } else { + fatalError("ISO8601DateFormatter is unavailable on this platform.") + } + case let .formatter(formatter): + return formatter.string(from: unboxed) + } + } +} + +extension DateBox: Box { + var isNull: Bool { + return false + } + + var xmlString: String? { + return xmlString(format: format) + } +} + +extension DateBox: SimpleBox {} + +extension DateBox: CustomStringConvertible { + var description: String { + return unboxed.description + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/DecimalBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/DecimalBox.swift new file mode 100644 index 0000000000..35e09443da --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/DecimalBox.swift @@ -0,0 +1,62 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/17/18. +// + +import Foundation + +struct DecimalBox: Equatable { + typealias Unboxed = Decimal + + let unboxed: Unboxed + + init(_ unboxed: Unboxed) { + self.unboxed = unboxed + } + + init?(xmlString: String) { + guard let unboxed = Unboxed(string: xmlString) else { + return nil + } + self.init(unboxed) + } +} + +extension DecimalBox: Box { + var isNull: Bool { + return false + } + + /// # Lexical representation + /// Decimal has a lexical representation consisting of a finite-length sequence of + /// decimal digits separated by a period as a decimal indicator. + /// An optional leading sign is allowed. If the sign is omitted, `"+"` is assumed. + /// Leading and trailing zeroes are optional. If the fractional part is zero, + /// the period and following zero(es) can be omitted. + /// For example: `-1.23`, `12678967.543233`, `+100000.00`, `210`. + /// + /// # Canonical representation + /// The canonical representation for decimal is defined by prohibiting certain + /// options from the Lexical representation. Specifically, the preceding optional + /// `"+"` sign is prohibited. The decimal point is required. Leading and trailing + /// zeroes are prohibited subject to the following: there must be at least one + /// digit to the right and to the left of the decimal point which may be a zero. + /// + /// --- + /// + /// [Schema definition](https://www.w3.org/TR/xmlschema-2/#decimal) + var xmlString: String? { + return "\(unboxed)" + } +} + +extension DecimalBox: SimpleBox {} + +extension DecimalBox: CustomStringConvertible { + var description: String { + return unboxed.description + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/DoubleBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/DoubleBox.swift new file mode 100644 index 0000000000..42302608d0 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/DoubleBox.swift @@ -0,0 +1,49 @@ +// Copyright (c) 2019-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Max Desiatov on 05/10/2019. +// + +struct DoubleBox: Equatable, ValueBox { + typealias Unboxed = Double + + let unboxed: Unboxed + + init(_ value: Unboxed) { + unboxed = value + } + + init?(xmlString: String) { + guard let unboxed = Double(xmlString) else { return nil } + + self.init(unboxed) + } +} + +extension DoubleBox: Box { + var isNull: Bool { + return false + } + + var xmlString: String? { + guard !unboxed.isNaN else { + return "NaN" + } + + guard !unboxed.isInfinite else { + return (unboxed > 0.0) ? "INF" : "-INF" + } + + return unboxed.description + } +} + +extension DoubleBox: SimpleBox {} + +extension DoubleBox: CustomStringConvertible { + var description: String { + return unboxed.description + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/FloatBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/FloatBox.swift new file mode 100644 index 0000000000..3f0e283af7 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/FloatBox.swift @@ -0,0 +1,78 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/17/18. +// + +struct FloatBox: Equatable, ValueBox { + typealias Unboxed = Float + + let unboxed: Unboxed + + init(_ unboxed: Float) { + self.unboxed = Unboxed(unboxed) + } + + init?(xmlString: String) { + guard let unboxed = Unboxed(xmlString) else { + return nil + } + self.init(unboxed) + } +} + +extension FloatBox: Box { + var isNull: Bool { + return false + } + + /// # Lexical representation + /// float values have a lexical representation consisting of a mantissa followed, optionally, + /// by the character `"E"` or `"e"`, followed by an exponent. The exponent **must** be an integer. + /// The mantissa **must** be a decimal number. The representations for exponent and mantissa **must** + /// follow the lexical rules for integer and decimal. If the `"E"` or `"e"` and the following + /// exponent are omitted, an exponent value of `0` is assumed. + /// + /// The special values positive and negative infinity and not-a-number have lexical + /// representations `INF`, `-INF` and `NaN`, respectively. Lexical representations for zero + /// may take a positive or negative sign. + /// + /// For example, `-1E4`, `1267.43233E12`, `12.78e-2`, `12` , `-0`, `0` and `INF` are all + /// legal literals for float. + /// + /// # Canonical representation + /// The canonical representation for float is defined by prohibiting certain options from the + /// Lexical representation. Specifically, the exponent must be indicated by `"E"`. + /// Leading zeroes and the preceding optional `"+"` sign are prohibited in the exponent. + /// If the exponent is zero, it must be indicated by `"E0"`. For the mantissa, the preceding + /// optional `"+"` sign is prohibited and the decimal point is required. Leading and trailing + /// zeroes are prohibited subject to the following: number representations must be normalized + /// such that there is a single digit which is non-zero to the left of the decimal point and + /// at least a single digit to the right of the decimal point unless the value being represented + /// is zero. The canonical representation for zero is `0.0E0`. + /// + /// --- + /// + /// [Schema definition](https://www.w3.org/TR/xmlschema-2/#float) + var xmlString: String? { + guard !unboxed.isNaN else { + return "NaN" + } + + guard !unboxed.isInfinite else { + return (unboxed > 0.0) ? "INF" : "-INF" + } + + return unboxed.description + } +} + +extension FloatBox: SimpleBox {} + +extension FloatBox: CustomStringConvertible { + var description: String { + return unboxed.description + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/IntBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/IntBox.swift new file mode 100644 index 0000000000..35380a6455 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/IntBox.swift @@ -0,0 +1,59 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/17/18. +// + +struct IntBox: Equatable { + typealias Unboxed = Int64 + + let unboxed: Unboxed + + init(_ unboxed: Integer) { + self.unboxed = Unboxed(unboxed) + } + + init?(xmlString: String) { + guard let unboxed = Unboxed(xmlString) else { + return nil + } + self.init(unboxed) + } + + func unbox() -> Integer? { + return Integer(exactly: unboxed) + } +} + +extension IntBox: Box { + var isNull: Bool { + return false + } + + /// # Lexical representation + /// Integer has a lexical representation consisting of a finite-length sequence of + /// decimal digits with an optional leading sign. If the sign is omitted, `"+"` is assumed. + /// For example: `-1`, `0`, `12678967543233`, `+100000`. + /// + /// # Canonical representation + /// The canonical representation for integer is defined by prohibiting certain + /// options from the Lexical representation. Specifically, the preceding optional + /// `"+"` sign is prohibited and leading zeroes are prohibited. + /// + /// --- + /// + /// [Schema definition](https://www.w3.org/TR/xmlschema-2/#integer) + var xmlString: String? { + return unboxed.description + } +} + +extension IntBox: SimpleBox {} + +extension IntBox: CustomStringConvertible { + var description: String { + return unboxed.description + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/KeyedBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/KeyedBox.swift new file mode 100644 index 0000000000..271f665d92 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/KeyedBox.swift @@ -0,0 +1,57 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 11/19/18. +// + +struct KeyedBox { + typealias Key = String + typealias Attribute = SimpleBox + typealias Element = Box + + typealias Attributes = KeyedStorage + typealias Elements = KeyedStorage + + var elements = Elements() + var attributes = Attributes() + + var unboxed: (elements: Elements, attributes: Attributes) { + return ( + elements: elements, + attributes: attributes + ) + } + + var value: SimpleBox? { + return elements.values.first as? SimpleBox + } +} + +extension KeyedBox { + init(elements: E, attributes: A) + where E: Sequence, E.Element == (Key, Element), + A: Sequence, A.Element == (Key, Attribute) + { + let elements = Elements(elements) + let attributes = Attributes(attributes) + self.init(elements: elements, attributes: attributes) + } +} + +extension KeyedBox: Box { + var isNull: Bool { + return false + } + + var xmlString: String? { + return nil + } +} + +extension KeyedBox: CustomStringConvertible { + var description: String { + return "{attributes: \(attributes), elements: \(elements)}" + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/NullBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/NullBox.swift new file mode 100644 index 0000000000..e5dd66b517 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/NullBox.swift @@ -0,0 +1,33 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/17/18. +// + +struct NullBox {} + +extension NullBox: Box { + var isNull: Bool { + return true + } + + var xmlString: String? { + return nil + } +} + +extension NullBox: SimpleBox {} + +extension NullBox: Equatable { + static func ==(_: NullBox, _: NullBox) -> Bool { + return true + } +} + +extension NullBox: CustomStringConvertible { + var description: String { + return "null" + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/SharedBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/SharedBox.swift new file mode 100644 index 0000000000..d05af76f8d --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/SharedBox.swift @@ -0,0 +1,35 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/22/18. +// + +class SharedBox { + private(set) var unboxed: Unboxed + + init(_ wrapped: Unboxed) { + unboxed = wrapped + } + + func withShared(_ body: (inout Unboxed) throws -> T) rethrows -> T { + return try body(&unboxed) + } +} + +extension SharedBox: Box { + var isNull: Bool { + return unboxed.isNull + } + + var xmlString: String? { + return unboxed.xmlString + } +} + +extension SharedBox: SharedBoxProtocol { + func unbox() -> Unboxed { + return unboxed + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/SingleKeyedBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/SingleKeyedBox.swift new file mode 100644 index 0000000000..47c6370d56 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/SingleKeyedBox.swift @@ -0,0 +1,25 @@ +// Copyright (c) 2019-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by James Bean on 7/15/19. +// + +/// A `Box` which contains a single `key` and `element` pair. This is useful for disambiguating elements which could either represent +/// an element nested in a keyed or unkeyed container, or an choice between multiple known-typed values (implemented in Swift using +/// enums with associated values). +struct SingleKeyedBox: SimpleBox { + var key: String + var element: Box +} + +extension SingleKeyedBox: Box { + var isNull: Bool { + return false + } + + var xmlString: String? { + return nil + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/StringBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/StringBox.swift new file mode 100644 index 0000000000..c33629c99a --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/StringBox.swift @@ -0,0 +1,39 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/17/18. +// + +struct StringBox: Equatable { + typealias Unboxed = String + + let unboxed: Unboxed + + init(_ unboxed: Unboxed) { + self.unboxed = unboxed + } + + init(xmlString: Unboxed) { + self.init(xmlString) + } +} + +extension StringBox: Box { + var isNull: Bool { + return false + } + + var xmlString: String? { + return unboxed.description + } +} + +extension StringBox: SimpleBox {} + +extension StringBox: CustomStringConvertible { + var description: String { + return unboxed.description + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/UIntBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/UIntBox.swift new file mode 100644 index 0000000000..2fd70ac156 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/UIntBox.swift @@ -0,0 +1,62 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/17/18. +// + +struct UIntBox: Equatable { + typealias Unboxed = UInt64 + + let unboxed: Unboxed + + init(_ unboxed: Integer) { + self.unboxed = Unboxed(unboxed) + } + + init?(xmlString: String) { + guard let unboxed = Unboxed(xmlString) else { + return nil + } + self.init(unboxed) + } + + func unbox() -> Integer? { + return Integer(exactly: unboxed) + } +} + +extension UIntBox: Box { + var isNull: Bool { + return false + } + + /// # Lexical representation + /// Unsigned integer has a lexical representation consisting of an optional + /// sign followed by a finite-length sequence of decimal digits. + /// If the sign is omitted, the positive sign (`"+"`) is assumed. + /// If the sign is present, it must be `"+"` except for lexical forms denoting zero, + /// which may be preceded by a positive (`"+"`) or a negative (`"-"`) sign. + /// For example: `1`, `0`, `12678967543233`, `+100000`. + /// + /// # Canonical representation + /// The canonical representation for nonNegativeInteger is defined by prohibiting + /// certain options from the Lexical representation. Specifically, + /// the the optional `"+"` sign is prohibited and leading zeroes are prohibited. + /// + /// --- + /// + /// [Schema definition](https://www.w3.org/TR/xmlschema-2/#nonNegativeInteger) + var xmlString: String? { + return unboxed.description + } +} + +extension UIntBox: SimpleBox {} + +extension UIntBox: CustomStringConvertible { + var description: String { + return unboxed.description + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/URLBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/URLBox.swift new file mode 100644 index 0000000000..6c12061783 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/URLBox.swift @@ -0,0 +1,44 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/21/18. +// + +import Foundation + +struct URLBox: Equatable { + typealias Unboxed = URL + + let unboxed: Unboxed + + init(_ unboxed: Unboxed) { + self.unboxed = unboxed + } + + init?(xmlString: String) { + guard let unboxed = Unboxed(string: xmlString) else { + return nil + } + self.init(unboxed) + } +} + +extension URLBox: Box { + var isNull: Bool { + return false + } + + var xmlString: String? { + return unboxed.absoluteString + } +} + +extension URLBox: SimpleBox {} + +extension URLBox: CustomStringConvertible { + var description: String { + return unboxed.description + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/UnkeyedBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/UnkeyedBox.swift new file mode 100644 index 0000000000..203a21674c --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/UnkeyedBox.swift @@ -0,0 +1,19 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 11/20/18. +// + +typealias UnkeyedBox = [Box] + +extension Array: Box { + var isNull: Bool { + return false + } + + var xmlString: String? { + return nil + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Box/ValueBox.swift b/third-party/XMLCoder/Sources/Auxiliaries/Box/ValueBox.swift new file mode 100644 index 0000000000..a6739c471d --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Box/ValueBox.swift @@ -0,0 +1,13 @@ +// Copyright (c) 2019-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Max Desiatov on 05/10/2019. +// + +protocol ValueBox: SimpleBox { + associatedtype Unboxed + + init(_ value: Unboxed) +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Element.swift b/third-party/XMLCoder/Sources/Auxiliaries/Element.swift new file mode 100644 index 0000000000..8728e4ac74 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Element.swift @@ -0,0 +1,43 @@ +// +// XMLElementNode.swift +// XMLCoder +// +// Created by Benjamin Wetherfield on 6/4/20. +// + +protocol XMLElementProtocol {} + +/** Property wrapper specifying that a given property should be encoded and decoded as an XML element. + + For example, this type + ```swift + struct Book: Codable { + @Element var id: Int + } + ``` + + will encode value `Book(id: 42)` as `42`. And vice versa, + it will decode the former into the latter. + */ +@propertyWrapper +public struct Element: XMLElementProtocol { + public var wrappedValue: Value + + public init(_ wrappedValue: Value) { + self.wrappedValue = wrappedValue + } +} + +extension Element: Codable where Value: Codable { + public func encode(to encoder: Encoder) throws { + try wrappedValue.encode(to: encoder) + } + + public init(from decoder: Decoder) throws { + try wrappedValue = .init(from: decoder) + } +} + +extension Element: Equatable where Value: Equatable {} +extension Element: Hashable where Value: Hashable {} +extension Element: Sendable where Value: Sendable {} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/ElementAndAttribute.swift b/third-party/XMLCoder/Sources/Auxiliaries/ElementAndAttribute.swift new file mode 100644 index 0000000000..b04cfc5629 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/ElementAndAttribute.swift @@ -0,0 +1,44 @@ +// +// XMLBothNode.swift +// XMLCoder +// +// Created by Benjamin Wetherfield on 6/7/20. +// + +protocol XMLElementAndAttributeProtocol {} + +/** Property wrapper specifying that a given property should be decoded from either an XML element + or an XML attribute. When encoding, the value will be present as both an attribute, and an element. + + For example, this type + ```swift + struct Book: Codable { + @ElementAndAttribute var id: Int + } + ``` + + will encode value `Book(id: 42)` as `42`. It will decode both + `42` and `` as `Book(id: 42)`. + */ +@propertyWrapper +public struct ElementAndAttribute: XMLElementAndAttributeProtocol { + public var wrappedValue: Value + + public init(_ wrappedValue: Value) { + self.wrappedValue = wrappedValue + } +} + +extension ElementAndAttribute: Codable where Value: Codable { + public func encode(to encoder: Encoder) throws { + try wrappedValue.encode(to: encoder) + } + + public init(from decoder: Decoder) throws { + try wrappedValue = .init(from: decoder) + } +} + +extension ElementAndAttribute: Equatable where Value: Equatable {} +extension ElementAndAttribute: Hashable where Value: Hashable {} +extension ElementAndAttribute: Sendable where Value: Sendable {} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/ISO8601DateFormatter.swift b/third-party/XMLCoder/Sources/Auxiliaries/ISO8601DateFormatter.swift new file mode 100644 index 0000000000..a431b23e64 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/ISO8601DateFormatter.swift @@ -0,0 +1,17 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/21/17. +// + +import Foundation + +extension ISO8601DateFormatter { + static func xmlCoderFormatter() -> ISO8601DateFormatter { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = .withInternetDateTime + return formatter + } +} \ No newline at end of file diff --git a/third-party/XMLCoder/Sources/Auxiliaries/KeyedStorage.swift b/third-party/XMLCoder/Sources/Auxiliaries/KeyedStorage.swift new file mode 100644 index 0000000000..808a38dce7 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/KeyedStorage.swift @@ -0,0 +1,82 @@ +// Copyright (c) 2019-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Max Desiatov on 07/04/2019. +// + +struct KeyedStorage { + typealias Buffer = [(Key, Value)] + typealias KeyMap = [Key: [Int]] + + fileprivate var keyMap = KeyMap() + fileprivate var buffer = Buffer() + + var isEmpty: Bool { + return buffer.isEmpty + } + + var count: Int { + return buffer.count + } + + var keys: [Key] { + return buffer.map { $0.0 } + } + + var values: [Value] { + return buffer.map { $0.1 } + } + + init(_ sequence: S) where S: Sequence, S.Element == (Key, Value) { + buffer = Buffer() + keyMap = KeyMap() + sequence.forEach { key, value in append(value, at: key) } + } + + subscript(key: Key) -> [Value] { + return keyMap[key]?.map { buffer[$0].1 } ?? [] + } + + mutating func append(_ value: Value, at key: Key) { + let i = buffer.count + buffer.append((key, value)) + if keyMap[key] != nil { + keyMap[key]?.append(i) + } else { + keyMap[key] = [i] + } + } + + func map(_ transform: (Key, Value) throws -> T) rethrows -> [T] { + return try buffer.map(transform) + } + + func compactMap( + _ transform: ((Key, Value)) throws -> T? + ) rethrows -> [T] { + return try buffer.compactMap(transform) + } + + mutating func reserveCapacity(_ capacity: Int) { + buffer.reserveCapacity(capacity) + keyMap.reserveCapacity(capacity) + } + + init() {} +} + +extension KeyedStorage: Sequence { + func makeIterator() -> Buffer.Iterator { + return buffer.makeIterator() + } +} + +extension KeyedStorage: CustomStringConvertible { + var description: String { + let result = buffer.map { "\"\($0)\": \($1)" }.joined(separator: ", ") + + return "[\(result)]" + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Metatypes.swift b/third-party/XMLCoder/Sources/Auxiliaries/Metatypes.swift new file mode 100644 index 0000000000..40be7f3322 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Metatypes.swift @@ -0,0 +1,31 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Max Desiatov on 30/12/2018. +// + +/// Type-erased protocol helper for a metatype check in generic `decode` +/// overload. If you custom sequence type is not decoded correctly, try +/// making it confirm to `XMLDecodableSequence`. Default conformances for +/// `Array` and `Dictionary` are already provided by the XMLCoder library. +public protocol XMLDecodableSequence { + init() +} + +extension Array: XMLDecodableSequence {} + +extension Dictionary: XMLDecodableSequence {} + +/// Type-erased protocol helper for a metatype check in generic `decode` +/// overload. +protocol AnyOptional { + init() +} + +extension Optional: AnyOptional { + init() { + self = nil + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/String+Extensions.swift b/third-party/XMLCoder/Sources/Auxiliaries/String+Extensions.swift new file mode 100644 index 0000000000..59ce223f81 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/String+Extensions.swift @@ -0,0 +1,52 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/18/18. +// + +import Foundation + +extension StringProtocol where Self.Index == String.Index { + func escape(_ characterSet: [(character: String, escapedCharacter: String)]) -> String { + var string = String(self) + + for set in characterSet { + string = string.replacingOccurrences(of: set.character, with: set.escapedCharacter, options: .literal) + } + + return string + } +} + +extension StringProtocol { + func capitalizingFirstLetter() -> Self { + guard !isEmpty else { + return self + } + return Self(prefix(1).uppercased() + dropFirst())! + } + + mutating func capitalizeFirstLetter() { + self = capitalizingFirstLetter() + } + + func lowercasingFirstLetter() -> Self { + // avoid lowercasing single letters (I), or capitalized multiples (AThing ! to aThing, leave as AThing) + guard count > 1, !(String(prefix(2)) == prefix(2).lowercased()) else { + return self + } + return Self(prefix(1).lowercased() + dropFirst())! + } + + mutating func lowercaseFirstLetter() { + self = lowercasingFirstLetter() + } +} + +extension String { + func isAllWhitespace() -> Bool { + return unicodeScalars.allSatisfy(CharacterSet.whitespacesAndNewlines.contains) + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/Utils.swift b/third-party/XMLCoder/Sources/Auxiliaries/Utils.swift new file mode 100644 index 0000000000..747f327f6e --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/Utils.swift @@ -0,0 +1,13 @@ +// Copyright (c) 2018-2023 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Alkenso (Vladimir Vashurkin) on 08.06.2023. +// + +import Foundation + +extension CodingKey { + internal var isInlined: Bool { stringValue == "" } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/XMLChoiceCodingKey.swift b/third-party/XMLCoder/Sources/Auxiliaries/XMLChoiceCodingKey.swift new file mode 100644 index 0000000000..aa3dbff25f --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/XMLChoiceCodingKey.swift @@ -0,0 +1,59 @@ +// Copyright (c) 2019-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Benjamin Wetherfield on 7/17/19. +// + +/// An empty marker protocol that can be used in place of `CodingKey`. It must be used when +/// attempting to encode and decode union-type–like enums with associated values to and from `XML` +/// choice elements. +/// +/// - Important: In order for your `XML`-destined `Codable` type to be encoded and/or decoded +/// properly, you must conform your custom `CodingKey` type additionally to `XMLChoiceCodingKey`. +/// +/// For example, say you have defined a type which can hold _either_ an `Int` _or_ a `String`: +/// +/// enum IntOrString { +/// case int(Int) +/// case string(String) +/// } +/// +/// Implementing the requirements for the `Codable` protocol like this: +/// +/// extension IntOrString: Codable { +/// enum CodingKeys: String, XMLChoiceCodingKey { +/// case int +/// case string +/// } +/// +/// func encode(to encoder: Encoder) throws { +/// var container = encoder.container(keyedBy: CodingKeys.self) +/// switch self { +/// case let .int(value): +/// try container.encode(value, forKey: .int) +/// case let .string(value): +/// try container.encode(value, forKey: .string) +/// } +/// } +/// +/// init(from decoder: Decoder) throws { +/// let container = try decoder.container(keyedBy: CodingKeys.self) +/// do { +/// self = .int(try container.decode(Int.self, forKey: .int)) +/// } catch { +/// self = .string(try container.decode(String.self, forKey: .string)) +/// } +/// } +/// } +/// +/// Retroactively conform the `CodingKeys` enum to `XMLChoiceCodingKey` when targeting `XML` as your +/// encoded format. +/// +/// extension IntOrString.CodingKeys: XMLChoiceCodingKey {} +/// +/// - Note: The `XMLChoiceCodingKey` marker protocol allows the `XMLEncoder` / `XMLDecoder` to +/// resolve ambiguities particular to the `XML` format between nested unkeyed container elements and +/// choice elements. +public protocol XMLChoiceCodingKey: CodingKey {} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/XMLCoderElement.swift b/third-party/XMLCoder/Sources/Auxiliaries/XMLCoderElement.swift new file mode 100644 index 0000000000..1b1d8a0ffd --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/XMLCoderElement.swift @@ -0,0 +1,451 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/18/18. +// + +import Foundation + +struct XMLCoderElement: Equatable, Sendable { + struct Attribute: Equatable, Sendable { + let key: String + let value: String + } + + let key: String + private(set) var stringValue: String? + private(set) var elements: [XMLCoderElement] = [] + private(set) var attributes: [Attribute] = [] + private(set) var containsTextNodes: Bool = false + + var isStringNode: Bool { + return key == "" + } + + var isCDATANode: Bool { + return key == "#CDATA" + } + + var isTextNode: Bool { + return isStringNode || isCDATANode + } + + private var isInlined: Bool { + return key.isEmpty + } + + init( + key: String, + elements: [XMLCoderElement] = [], + attributes: [Attribute] = [] + ) { + self.key = key + stringValue = nil + self.elements = elements + self.attributes = attributes + } + + init( + key: String, + stringValue string: String, + attributes: [Attribute] = [] + ) { + self.key = key + elements = [XMLCoderElement(stringValue: string)] + self.attributes = attributes + containsTextNodes = true + } + + init( + key: String, + cdataValue string: String, + attributes: [Attribute] = [] + ) { + self.key = key + elements = [XMLCoderElement(cdataValue: string)] + self.attributes = attributes + containsTextNodes = true + } + + init(stringValue string: String) { + key = "" + stringValue = string + } + + init(cdataValue string: String) { + key = "#CDATA" + stringValue = string + } + + mutating func append(element: XMLCoderElement) { + elements.append(element) + containsTextNodes = containsTextNodes || element.isTextNode + } + + mutating func append(string: String) { + if elements.last?.isTextNode == true { + let oldValue = elements[elements.count - 1].stringValue ?? "" + elements[elements.count - 1].stringValue = oldValue + string + } else { + elements.append(XMLCoderElement(stringValue: string)) + } + containsTextNodes = true + } + + mutating func append(cdata string: String) { + if elements.last?.isCDATANode == true { + let oldValue = elements[elements.count - 1].stringValue ?? "" + elements[elements.count - 1].stringValue = oldValue + string + } else { + elements.append(XMLCoderElement(cdataValue: string)) + } + containsTextNodes = true + } + + mutating func trimTextNodes() { + guard containsTextNodes else { return } + for idx in elements.indices { + elements[idx].stringValue = elements[idx].stringValue?.trimmingCharacters(in: .whitespacesAndNewlines) + } + } + + func transformToBoxTree() -> Box { + if isTextNode { + return StringBox(stringValue!) + } + + let attributes = KeyedStorage(self.attributes.map { attribute in + (key: attribute.key, value: StringBox(attribute.value) as SimpleBox) + }) + + var storage = KeyedStorage() + // storage.reserveCapacity(self.elements) + + for element in self.elements { + + let hasElements = !element.elements.isEmpty + let hasAttributes = !element.attributes.isEmpty + let hasText = element.stringValue != nil + + if hasElements || hasAttributes { + storage.append(element.transformToBoxTree(), at: element.key) + } else if hasText { + storage.append(element.transformToBoxTree(), at: element.key) + } else { + storage.append(SingleKeyedBox(key: element.key, element: NullBox()), at: element.key) + } + } + + return KeyedBox(elements: storage, attributes: attributes) + } + + func toXMLString( + with header: XMLHeader?, + doctype: XMLDocumentType?, + escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), + formatting: XMLEncoder.OutputFormatting, + indentation: XMLEncoder.PrettyPrintIndentation + ) -> String { + var base = "" + + if let header = header, let headerXML = header.toXML() { + base += headerXML + } + + if let doctype = doctype { + base += doctype.toXML() + } + + return base + _toXMLString(escapedCharacters, formatting, indentation) + } + + private func formatUnsortedXMLElements( + _ string: inout String, + _ level: Int, + _ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), + _ formatting: XMLEncoder.OutputFormatting, + _ indentation: XMLEncoder.PrettyPrintIndentation, + _ prettyPrinted: Bool + ) { + formatXMLElements( + from: elements, + into: &string, + at: level, + escapedCharacters: escapedCharacters, + formatting: formatting, + indentation: indentation, + prettyPrinted: prettyPrinted + ) + } + + fileprivate func elementString( + for element: XMLCoderElement, + at level: Int, + formatting: XMLEncoder.OutputFormatting, + indentation: XMLEncoder.PrettyPrintIndentation, + escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), + prettyPrinted: Bool + ) -> String { + if let stringValue = element.stringValue { + if element.isCDATANode { + return "" + } else { + return stringValue.escape(escapedCharacters.elements) + } + } + + var string = "" + let indentLevel = isInlined ? level : level + 1 + string += element._toXMLString(indented: indentLevel, escapedCharacters, formatting, indentation) + string += prettyPrinted && !isInlined ? "\n" : "" + return string + } + + fileprivate func formatSortedXMLElements( + _ string: inout String, + _ level: Int, + _ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), + _ formatting: XMLEncoder.OutputFormatting, + _ indentation: XMLEncoder.PrettyPrintIndentation, + _ prettyPrinted: Bool + ) { + formatXMLElements(from: elements.sorted { $0.key < $1.key }, + into: &string, + at: level, + escapedCharacters: escapedCharacters, + formatting: formatting, + indentation: indentation, + prettyPrinted: prettyPrinted) + } + + fileprivate func formatXMLAttributes( + from attributes: [Attribute], + into string: inout String, + charactersEscapedInAttributes: [(String, String)] + ) { + for attribute in attributes { + string += " \(attribute.key)=\"\(attribute.value.escape(charactersEscapedInAttributes))\"" + } + } + + fileprivate func formatXMLElements( + from elements: [XMLCoderElement], + into string: inout String, + at level: Int, + escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), + formatting: XMLEncoder.OutputFormatting, + indentation: XMLEncoder.PrettyPrintIndentation, + prettyPrinted: Bool + ) { + for element in elements { + string += elementString(for: element, + at: level, + formatting: formatting, + indentation: indentation, + escapedCharacters: escapedCharacters, + prettyPrinted: prettyPrinted && !containsTextNodes) + } + } + + private func formatXMLAttributes( + _ formatting: XMLEncoder.OutputFormatting, + _ string: inout String, + _ charactersEscapedInAttributes: [(String, String)] + ) { + let attributesBelongingToContainer = self.elements.filter { + $0.key.isEmpty && !$0.attributes.isEmpty + }.flatMap { + $0.attributes + } + let allAttributes = self.attributes + attributesBelongingToContainer + + let attributes = formatting.contains(.sortedKeys) ? + allAttributes.sorted(by: { $0.key < $1.key }) : + allAttributes + formatXMLAttributes( + from: attributes, + into: &string, + charactersEscapedInAttributes: charactersEscapedInAttributes + ) + } + + private func formatXMLElements( + _ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), + _ formatting: XMLEncoder.OutputFormatting, + _ indentation: XMLEncoder.PrettyPrintIndentation, + _ string: inout String, + _ level: Int, + _ prettyPrinted: Bool + ) { + if formatting.contains(.sortedKeys) { + formatSortedXMLElements( + &string, level, escapedCharacters, formatting, indentation, prettyPrinted + ) + return + } + formatUnsortedXMLElements( + &string, level, escapedCharacters, formatting, indentation, prettyPrinted + ) + } + + private func _toXMLString( + indented level: Int = 0, + _ escapedCharacters: (elements: [(String, String)], attributes: [(String, String)]), + _ formatting: XMLEncoder.OutputFormatting, + _ indentation: XMLEncoder.PrettyPrintIndentation + ) -> String { + let prettyPrinted = formatting.contains(.prettyPrinted) + let prefix: String + switch indentation { + case let .spaces(count) where prettyPrinted && !isInlined: + prefix = String(repeating: " ", count: level * count) + case let .tabs(count) where prettyPrinted && !isInlined: + prefix = String(repeating: "\t", count: level * count) + default: + prefix = "" + } + var string = prefix + + if !key.isEmpty { + string += "<\(key)" + formatXMLAttributes(formatting, &string, escapedCharacters.attributes) + } + + if !elements.isEmpty || formatting.contains(.noEmptyElements) { + let hasOnlyIntrinsicContent = elements.allSatisfy { element in + element.key.isEmpty && !element.elements.contains { !$0.key.isEmpty } + } + let prettyPrintElements = prettyPrinted && !containsTextNodes && !hasOnlyIntrinsicContent + if !key.isEmpty { + string += prettyPrintElements ? ">\n" : ">" + } + if !elements.isEmpty { + formatXMLElements(escapedCharacters, formatting, indentation, &string, level, prettyPrintElements) + } + + if prettyPrintElements { string += prefix } + if !key.isEmpty { + string += "" + } + } else { + if !key.isEmpty { + string += " />" + } + } + + return string + } +} + +// MARK: - Convenience Initializers + +extension XMLCoderElement { + init(key: String, isStringBoxCDATA isCDATA: Bool, box: UnkeyedBox, attributes: [Attribute] = []) { + if let containsChoice = box as? [ChoiceBox] { + self.init( + key: key, + elements: containsChoice.map { + XMLCoderElement(key: $0.key, isStringBoxCDATA: isCDATA, box: $0.element) + }, + attributes: attributes + ) + } else { + self.init( + key: key, + elements: box.map { XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: $0) }, + attributes: attributes + ) + } + } + + init(key: String, isStringBoxCDATA: Bool, box: ChoiceBox, attributes: [Attribute] = []) { + self.init( + key: key, + elements: [ + XMLCoderElement(key: box.key, isStringBoxCDATA: isStringBoxCDATA, box: box.element), + ], + attributes: attributes + ) + } + + init(key: String, isStringBoxCDATA isCDATA: Bool, box: KeyedBox, attributes: [Attribute] = []) { + var elements: [XMLCoderElement] = [] + + for (key, box) in box.elements { + let fail = { + preconditionFailure("Unclassified box: \(type(of: box))") + } + + switch box { + case let sharedUnkeyedBox as SharedBox: + let box = sharedUnkeyedBox.unboxed + elements.append(contentsOf: box.map { + XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: $0) + }) + case let unkeyedBox as UnkeyedBox: + // This basically injects the unkeyed children directly into self: + elements.append(contentsOf: unkeyedBox.map { + XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: $0) + }) + case let sharedKeyedBox as SharedBox: + let box = sharedKeyedBox.unboxed + elements.append(XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: box)) + case let keyedBox as KeyedBox: + elements.append(XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: keyedBox)) + case let simpleBox as SimpleBox: + elements.append(XMLCoderElement(key: key, isStringBoxCDATA: isCDATA, box: simpleBox)) + default: + fail() + } + } + + let attributes: [Attribute] = attributes + box.attributes.compactMap { key, box in + guard let value = box.xmlString else { + return nil + } + return Attribute(key: key, value: value) + } + + self.init(key: key, elements: elements, attributes: attributes) + } + + init(key: String, isStringBoxCDATA: Bool, box: SimpleBox) { + if isStringBoxCDATA, let stringBox = box as? StringBox { + self.init(key: key, cdataValue: stringBox.unboxed) + } else if let value = box.xmlString { + self.init(key: key, stringValue: value) + } else { + self.init(key: key) + } + } + + init(key: String, isStringBoxCDATA isCDATA: Bool, box: Box, attributes: [Attribute] = []) { + switch box { + case let sharedUnkeyedBox as SharedBox: + self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedUnkeyedBox.unboxed, attributes: attributes) + case let sharedKeyedBox as SharedBox: + self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedKeyedBox.unboxed, attributes: attributes) + case let sharedChoiceBox as SharedBox: + self.init(key: key, isStringBoxCDATA: isCDATA, box: sharedChoiceBox.unboxed, attributes: attributes) + case let unkeyedBox as UnkeyedBox: + self.init(key: key, isStringBoxCDATA: isCDATA, box: unkeyedBox, attributes: attributes) + case let keyedBox as KeyedBox: + self.init(key: key, isStringBoxCDATA: isCDATA, box: keyedBox, attributes: attributes) + case let choiceBox as ChoiceBox: + self.init(key: key, isStringBoxCDATA: isCDATA, box: choiceBox, attributes: attributes) + case let simpleBox as SimpleBox: + self.init(key: key, isStringBoxCDATA: isCDATA, box: simpleBox) + case let box: + preconditionFailure("Unclassified box: \(type(of: box))") + } + } +} + +extension XMLCoderElement { + func isWhitespaceWithNoElements() -> Bool { + let stringValueIsWhitespaceOrNil = stringValue?.isAllWhitespace() ?? true + return self.key == "" && stringValueIsWhitespaceOrNil && self.elements.isEmpty + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/XMLDocumentType.swift b/third-party/XMLCoder/Sources/Auxiliaries/XMLDocumentType.swift new file mode 100644 index 0000000000..ec257ebc49 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/XMLDocumentType.swift @@ -0,0 +1,65 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Joannis Orlandos on 8/11/22. +// + +import Foundation + +public struct XMLDocumentType { + public enum External: String { + case `public` = "PUBLIC" + case system = "SYSTEM" + } + + public let rootElement: String + public let external: External + public let dtdName: String? + public let dtdLocation: String + + internal init( + rootElement: String, + external: External, + dtdName: String?, + dtdLocation: String + ) { + self.rootElement = rootElement + self.external = external + self.dtdName = dtdName + self.dtdLocation = dtdLocation + } + + public static func `public`(rootElement: String, dtdName: String, dtdLocation: String) -> XMLDocumentType { + XMLDocumentType( + rootElement: rootElement, + external: .public, + dtdName: dtdName, + dtdLocation: dtdLocation + ) + } + + public static func system(rootElement: String, dtdLocation: String) -> XMLDocumentType { + XMLDocumentType( + rootElement: rootElement, + external: .system, + dtdName: nil, + dtdLocation: dtdLocation + ) + } + + func toXML() -> String { + var string = "\n" + + return string + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/XMLHeader.swift b/third-party/XMLCoder/Sources/Auxiliaries/XMLHeader.swift new file mode 100644 index 0000000000..6770a20c76 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/XMLHeader.swift @@ -0,0 +1,57 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 12/18/18. +// + +import Foundation + +/// Type that allows overriding XML header during encoding. Pass a value of this type to the `encode` +/// function of `XMLEncoder` to specify the exact value of the header you'd like to see in the encoded +/// data. +public struct XMLHeader: Sendable { + /// The XML standard that the produced document conforms to. + public let version: Double? + + /// The encoding standard used to represent the characters in the produced document. + public let encoding: String? + + /// Indicates whether a document relies on information from an external source. + public let standalone: String? + + public init(version: Double? = nil, encoding: String? = nil, standalone: String? = nil) { + self.version = version + self.encoding = encoding + self.standalone = standalone + } + + func isEmpty() -> Bool { + return version == nil && encoding == nil && standalone == nil + } + + func toXML() -> String? { + guard !isEmpty() else { + return nil + } + + var string = "\n" + + return string + } +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/XMLKey.swift b/third-party/XMLCoder/Sources/Auxiliaries/XMLKey.swift new file mode 100644 index 0000000000..f7f31f3291 --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/XMLKey.swift @@ -0,0 +1,38 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/21/17. +// + +import Foundation + +/// Shared Key Types +struct XMLKey: CodingKey, Sendable { + public let stringValue: String + public let intValue: Int? + + public init?(stringValue: String) { + self.init(key: stringValue) + } + + public init?(intValue: Int) { + self.init(index: intValue) + } + + public init(stringValue: String, intValue: Int?) { + self.stringValue = stringValue + self.intValue = intValue + } + + init(key: String) { + self.init(stringValue: key, intValue: nil) + } + + init(index: Int) { + self.init(stringValue: "\(index)", intValue: index) + } + + static let `super` = XMLKey(stringValue: "super")! +} diff --git a/third-party/XMLCoder/Sources/Auxiliaries/XMLStackParser.swift b/third-party/XMLCoder/Sources/Auxiliaries/XMLStackParser.swift new file mode 100644 index 0000000000..c18a4fd91a --- /dev/null +++ b/third-party/XMLCoder/Sources/Auxiliaries/XMLStackParser.swift @@ -0,0 +1,200 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/14/17. +// + +import Foundation +#if canImport(FoundationXML) +import FoundationXML +#endif + +class XMLStackParser: NSObject { + var root: XMLCoderElement? + private var stack: [XMLCoderElement] = [] + private let trimValueWhitespaces: Bool + private let removeWhitespaceElements: Bool + + init(trimValueWhitespaces: Bool = true, removeWhitespaceElements: Bool = false) { + self.trimValueWhitespaces = trimValueWhitespaces + self.removeWhitespaceElements = removeWhitespaceElements + super.init() + } + + static func parse( + with data: Data, + errorContextLength length: UInt, + shouldProcessNamespaces: Bool, + trimValueWhitespaces: Bool, + removeWhitespaceElements: Bool + ) throws -> Box { + let parser = XMLStackParser(trimValueWhitespaces: trimValueWhitespaces, + removeWhitespaceElements: removeWhitespaceElements) + + let node = try parser.parse( + with: data, + errorContextLength: length, + shouldProcessNamespaces: shouldProcessNamespaces + ) + + return node.transformToBoxTree() + } + + func parse( + with data: Data, + errorContextLength: UInt, + shouldProcessNamespaces: Bool + ) throws -> XMLCoderElement { + let xmlParser = XMLParser(data: data) + xmlParser.shouldProcessNamespaces = shouldProcessNamespaces + xmlParser.delegate = self + + guard !xmlParser.parse() || root == nil else { + return root! + } + + guard let error = xmlParser.parserError else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: [], + debugDescription: "The given data could not be parsed into XML." + )) + } + + // `lineNumber` isn't 0-indexed, so 0 is an invalid value for context + guard errorContextLength > 0 && xmlParser.lineNumber > 0 else { + throw error + } + + let string = String(data: data, encoding: .utf8) ?? "" + let lines = string.split(separator: "\n") + var errorPosition = 0 + let offset = Int(errorContextLength / 2) + for i in 0.. 0 { + lowerBoundIndex = errorPosition - offset + } + + var upperBoundIndex = string.count + if errorPosition + offset < string.count { + upperBoundIndex = errorPosition + offset + } + + let lowerBound = String.Index(utf16Offset: lowerBoundIndex, in: string) + let upperBound = String.Index(utf16Offset: upperBoundIndex, in: string) + + let context = string[lowerBound.. ()) rethrows { + guard !stack.isEmpty else { + return + } + try body(&stack[stack.count - 1]) + } + + func trimWhitespacesIfNeeded(_ string: String) -> String { + return trimValueWhitespaces + ? string.trimmingCharacters(in: .whitespacesAndNewlines) + : string + } +} + +extension XMLStackParser: XMLParserDelegate { + func parserDidStartDocument(_: XMLParser) { + root = nil + stack = [] + } + + func parser(_: XMLParser, + didStartElement elementName: String, + namespaceURI: String?, + qualifiedName: String?, + attributes attributeDict: [String: String] = [:]) + { + let attributes = attributeDict.map { key, value in + XMLCoderElement.Attribute(key: key, value: value) + } + let element = XMLCoderElement(key: elementName, attributes: attributes) + stack.append(element) + } + + func parser(_: XMLParser, + didEndElement _: String, + namespaceURI _: String?, + qualifiedName _: String?) + { + guard var element = stack.popLast() else { + return + } + if trimValueWhitespaces && element.containsTextNodes { + element.trimTextNodes() + } + + let updatedElement = removeWhitespaceElements ? elementWithFilteredElements(element: element) : element + + withCurrentElement { currentElement in + currentElement.append(element: updatedElement) + } + + if stack.isEmpty { + root = updatedElement + } + } + + func elementWithFilteredElements(element: XMLCoderElement) -> XMLCoderElement { + var hasWhitespaceElements = false + var hasNonWhitespaceElements = false + var filteredElements: [XMLCoderElement] = [] + for ele in element.elements { + if ele.isWhitespaceWithNoElements() { + hasWhitespaceElements = true + } else { + hasNonWhitespaceElements = true + filteredElements.append(ele) + } + } + + if hasWhitespaceElements && hasNonWhitespaceElements { + return XMLCoderElement(key: element.key, elements: filteredElements, attributes: element.attributes) + } + return element + } + + func parser(_: XMLParser, foundCharacters string: String) { + let processedString = trimWhitespacesIfNeeded(string) + guard processedString.count > 0, string.count != 0 else { + return + } + + withCurrentElement { currentElement in + currentElement.append(string: string) + } + } + + func parser(_: XMLParser, foundCDATA CDATABlock: Data) { + guard let string = String(data: CDATABlock, encoding: .utf8) else { + return + } + + withCurrentElement { currentElement in + currentElement.append(cdata: string) + } + } +} diff --git a/third-party/XMLCoder/Sources/Decoder/DecodingErrorExtension.swift b/third-party/XMLCoder/Sources/Decoder/DecodingErrorExtension.swift new file mode 100644 index 0000000000..011477ec2c --- /dev/null +++ b/third-party/XMLCoder/Sources/Decoder/DecodingErrorExtension.swift @@ -0,0 +1,54 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/21/17. +// + +import Foundation + +// MARK: - Error Utilities + +extension DecodingError { + /// Returns a `.typeMismatch` error describing the expected type. + /// + /// - parameter path: The path of `CodingKey`s taken to decode a value of this type. + /// - parameter expectation: The type expected to be encountered. + /// - parameter reality: The value that was encountered instead of the expected type. + /// - returns: A `DecodingError` with the appropriate path and debug description. + static func typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: Box) -> DecodingError { + let description = "Expected to decode \(expectation) but found \(_typeDescription(of: reality)) instead." + return .typeMismatch(expectation, Context(codingPath: path, debugDescription: description)) + } + + /// Returns a description of the type of `value` appropriate for an error message. + /// + /// - parameter value: The value whose type to describe. + /// - returns: A string describing `value`. + /// - precondition: `value` is one of the types below. + static func _typeDescription(of box: Box) -> String { + switch box { + case is NullBox: + return "a null value" + case is BoolBox: + return "a boolean value" + case is DecimalBox: + return "a decimal value" + case is IntBox: + return "a signed integer value" + case is UIntBox: + return "an unsigned integer value" + case is FloatBox: + return "a floating-point value" + case is DoubleBox: + return "a double floating-point value" + case is UnkeyedBox: + return "a array value" + case is KeyedBox: + return "a dictionary value" + case _: + return "\(type(of: box))" + } + } +} diff --git a/third-party/XMLCoder/Sources/Decoder/DynamicNodeDecoding.swift b/third-party/XMLCoder/Sources/Decoder/DynamicNodeDecoding.swift new file mode 100644 index 0000000000..9c87ba4326 --- /dev/null +++ b/third-party/XMLCoder/Sources/Decoder/DynamicNodeDecoding.swift @@ -0,0 +1,44 @@ +// Copyright (c) 2019-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Max Desiatov on 01/03/2019. +// + +/** Allows conforming types to specify how its properties will be decoded. + + For example: + ```swift + struct Book: Codable, Equatable, DynamicNodeDecoding { + let id: UInt + let title: String + let categories: [Category] + + enum CodingKeys: String, CodingKey { + case id + case title + case categories = "category" + } + + static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { + switch key { + case Book.CodingKeys.id: return .attribute + default: return .element + } + } + } + ``` + allows XML of this form to be decoded into values of type `Book`: + + ```xml + + Cat in the Hat + Kids + Wildlife + + ``` + */ +public protocol DynamicNodeDecoding: Decodable { + static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding +} diff --git a/third-party/XMLCoder/Sources/Decoder/SingleValueDecodingContainer.swift b/third-party/XMLCoder/Sources/Decoder/SingleValueDecodingContainer.swift new file mode 100644 index 0000000000..0f08b22082 --- /dev/null +++ b/third-party/XMLCoder/Sources/Decoder/SingleValueDecodingContainer.swift @@ -0,0 +1,57 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/20/17. +// + +import Foundation + +extension XMLDecoderImplementation: SingleValueDecodingContainer { + // MARK: SingleValueDecodingContainer Methods + + public func decodeNil() -> Bool { + return (try? topContainer().isNull) ?? true + } + + public func decode(_: Bool.Type) throws -> Bool { + return try unbox(try topContainer()) + } + + public func decode(_: Decimal.Type) throws -> Decimal { + return try unbox(try topContainer()) + } + + public func decode(_: T.Type) throws -> T { + return try unbox(try topContainer()) + } + + public func decode(_: T.Type) throws -> T { + return try unbox(try topContainer()) + } + + public func decode(_: Float.Type) throws -> Float { + return try unbox(try topContainer()) + } + + public func decode(_: Double.Type) throws -> Double { + return try unbox(try topContainer()) + } + + public func decode(_: String.Type) throws -> String { + return try unbox(try topContainer()) + } + + public func decode(_: String.Type) throws -> Date { + return try unbox(try topContainer()) + } + + public func decode(_: String.Type) throws -> Data { + return try unbox(try topContainer()) + } + + public func decode(_: T.Type) throws -> T { + return try unbox(try topContainer()) + } +} diff --git a/third-party/XMLCoder/Sources/Decoder/XMLChoiceDecodingContainer.swift b/third-party/XMLCoder/Sources/Decoder/XMLChoiceDecodingContainer.swift new file mode 100644 index 0000000000..7d4c1026b5 --- /dev/null +++ b/third-party/XMLCoder/Sources/Decoder/XMLChoiceDecodingContainer.swift @@ -0,0 +1,105 @@ +// Copyright (c) 2019-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by James Bean on 7/18/19. +// + +/// Container specialized for decoding XML choice elements. +struct XMLChoiceDecodingContainer: KeyedDecodingContainerProtocol { + typealias Key = K + + // MARK: Properties + + /// A reference to the decoder we're reading from. + private let decoder: XMLDecoderImplementation + + /// A reference to the container we're reading from. + private let container: SharedBox + + /// The path of coding keys taken to get to this point in decoding. + public private(set) var codingPath: [CodingKey] + + // MARK: - Initialization + + /// Initializes `self` by referencing the given decoder and container. + init(referencing decoder: XMLDecoderImplementation, wrapping container: SharedBox) { + self.decoder = decoder + container.withShared { $0.key = decoder.keyTransform($0.key) } + self.container = container + codingPath = decoder.codingPath + } + + // MARK: - KeyedDecodingContainerProtocol Methods + + public var allKeys: [Key] { + return container.withShared { [Key(stringValue: $0.key)!] } + } + + public func contains(_ key: Key) -> Bool { + return container.withShared { $0.key == key.stringValue } + } + + public func decodeNil(forKey key: Key) throws -> Bool { + return container.withShared { $0.element.isNull } + } + + public func decode(_ type: T.Type, forKey key: Key) throws -> T { + guard container.withShared({ $0.key == key.stringValue }), key is XMLChoiceCodingKey else { + throw DecodingError.typeMismatch( + at: codingPath, + expectation: type, + reality: container + ) + } + return try decoder.unbox(container.withShared { $0.element }) + } + + public func nestedContainer( + keyedBy _: NestedKey.Type, forKey key: Key + ) throws -> KeyedDecodingContainer { + guard container.unboxed.key == key.stringValue else { + throw DecodingError.typeMismatch( + at: codingPath, + expectation: NestedKey.self, + reality: container + ) + } + + let value = container.unboxed.element + guard let container = XMLKeyedDecodingContainer(box: value, decoder: decoder) else { + throw DecodingError.typeMismatch( + at: codingPath, + expectation: [String: Any].self, + reality: value + ) + } + + return KeyedDecodingContainer(container) + } + + public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + throw DecodingError.typeMismatch( + at: codingPath, + expectation: Key.self, + reality: container + ) + } + + public func superDecoder() throws -> Decoder { + throw DecodingError.typeMismatch( + at: codingPath, + expectation: Key.self, + reality: container + ) + } + + public func superDecoder(forKey key: Key) throws -> Decoder { + throw DecodingError.typeMismatch( + at: codingPath, + expectation: Key.self, + reality: container + ) + } +} diff --git a/third-party/XMLCoder/Sources/Decoder/XMLDecoder.swift b/third-party/XMLCoder/Sources/Decoder/XMLDecoder.swift new file mode 100644 index 0000000000..1f67b26975 --- /dev/null +++ b/third-party/XMLCoder/Sources/Decoder/XMLDecoder.swift @@ -0,0 +1,405 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/20/17. +// + +import Foundation + +// MARK: - XML Decoder + +/// `XMLDecoder` facilitates the decoding of XML into semantic `Decodable` types. +open class XMLDecoder { + // MARK: Options + + /// The strategy to use for decoding `Date` values. + public enum DateDecodingStrategy { + /// Defer to `Date` for decoding. This is the default strategy. + case deferredToDate + + /// Decode the `Date` as a UNIX timestamp from a XML number. This is the default strategy. + case secondsSince1970 + + /// Decode the `Date` as UNIX millisecond timestamp from a XML number. + case millisecondsSince1970 + + /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). + @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) + case iso8601 + + /// Decode the `Date` as a string parsed by the given formatter. + case formatted(DateFormatter) + + /// Decode the `Date` as a custom box decoded by the given closure. + case custom((_ decoder: Decoder) throws -> Date) + + /// Decode the `Date` as a string parsed by the given formatter for the give key. + static func keyFormatted( + _ formatterForKey: @escaping (CodingKey) throws -> DateFormatter? + ) -> XMLDecoder.DateDecodingStrategy { + return .custom { decoder -> Date in + guard let codingKey = decoder.codingPath.last else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "No Coding Path Found" + )) + } + + guard let container = try? decoder.singleValueContainer(), + let text = try? container.decode(String.self) + else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Could not decode date text" + )) + } + + guard let dateFormatter = try formatterForKey(codingKey) else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "No date formatter for date text" + ) + } + + if let date = dateFormatter.date(from: text) { + return date + } else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Cannot decode date string \(text)" + ) + } + } + } + } + + /// The strategy to use for decoding `Data` values. + public enum DataDecodingStrategy { + /// Defer to `Data` for decoding. + case deferredToData + + /// Decode the `Data` from a Base64-encoded string. This is the default strategy. + case base64 + + /// Decode the `Data` as a custom box decoded by the given closure. + case custom((_ decoder: Decoder) throws -> Data) + + /// Decode the `Data` as a custom box by the given closure for the give key. + static func keyFormatted( + _ formatterForKey: @escaping (CodingKey) throws -> Data? + ) -> XMLDecoder.DataDecodingStrategy { + return .custom { decoder -> Data in + guard let codingKey = decoder.codingPath.last else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "No Coding Path Found" + )) + } + + guard let container = try? decoder.singleValueContainer(), + let text = try? container.decode(String.self) + else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Could not decode date text" + )) + } + + guard let data = try formatterForKey(codingKey) else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Cannot decode data string \(text)" + ) + } + + return data + } + } + } + + /// The strategy to use for non-XML-conforming floating-point values (IEEE 754 infinity and NaN). + public enum NonConformingFloatDecodingStrategy { + /// Throw upon encountering non-conforming values. This is the default strategy. + case `throw` + + /// Decode the values from the given representation strings. + case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String) + } + + /// The strategy to use for automatically changing the box of keys before decoding. + public enum KeyDecodingStrategy { + /// Use the keys specified by each type. This is the default strategy. + case useDefaultKeys + + /// Convert from "snake_case_keys" to "camelCaseKeys" before attempting + /// to match a key with the one specified by each type. + /// + /// The conversion to upper case uses `Locale.system`, also known as + /// the ICU "root" locale. This means the result is consistent + /// regardless of the current user's locale and language preferences. + /// + /// Converting from snake case to camel case: + /// 1. Capitalizes the word starting after each `_` + /// 2. Removes all `_` + /// 3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata). + /// For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`. + /// + /// - Note: Using a key decoding strategy has a nominal performance cost, as each string key has to be inspected for the `_` character. + case convertFromSnakeCase + + /// Convert from "kebab-case" to "kebabCase" before attempting + /// to match a key with the one specified by each type. + case convertFromKebabCase + + /// Convert from "CodingKey" to "codingKey" + case convertFromCapitalized + + /// Convert from "CODING_KEY" to "codingKey" + case convertFromUppercase + + /// Provide a custom conversion from the key in the encoded XML to the + /// keys specified by the decoded types. + /// The full path to the current decoding position is provided for + /// context (in case you need to locate this key within the payload). + /// The returned key is used in place of the last component in the + /// coding path before decoding. + /// If the result of the conversion is a duplicate key, then only one + /// box will be present in the container for the type to decode from. + case custom((_ codingPath: [CodingKey]) -> CodingKey) + + static func _convertFromCapitalized(_ stringKey: String) -> String { + guard !stringKey.isEmpty else { + return stringKey + } + let firstLetter = stringKey.prefix(1).lowercased() + let result = firstLetter + stringKey.dropFirst() + return result + } + + static func _convertFromUppercase(_ stringKey: String) -> String { + _convert(stringKey.lowercased(), usingSeparator: "_") + } + + static func _convertFromSnakeCase(_ stringKey: String) -> String { + return _convert(stringKey, usingSeparator: "_") + } + + static func _convertFromKebabCase(_ stringKey: String) -> String { + return _convert(stringKey, usingSeparator: "-") + } + + static func _convert(_ stringKey: String, usingSeparator separator: Character) -> String { + guard !stringKey.isEmpty else { + return stringKey + } + + // Find the first non-separator character + guard let firstNonSeparator = stringKey.firstIndex(where: { $0 != separator }) else { + // Reached the end without finding a separator character + return stringKey + } + + // Find the last non-separator character + var lastNonSeparator = stringKey.index(before: stringKey.endIndex) + while lastNonSeparator > firstNonSeparator, stringKey[lastNonSeparator] == separator { + stringKey.formIndex(before: &lastNonSeparator) + } + + let keyRange = firstNonSeparator...lastNonSeparator + let leadingSeparatorRange = stringKey.startIndex..value`. + case element + /// Decodes a node from either elements of form `value` or attributes + /// of form `nodeName="value"`, with elements taking priority. + case elementOrAttribute + } + + /// The strategy to use in encoding encoding attributes. Defaults to `.deferredToEncoder`. + open var nodeDecodingStrategy: NodeDecodingStrategy = .deferredToDecoder + + /// Set of strategies to use for encoding of nodes. + public enum NodeDecodingStrategy { + /// Defer to `Encoder` for choosing an encoding. This is the default strategy. + case deferredToDecoder + + /// Return a closure computing the desired node encoding for the value by its coding key. + case custom((Decodable.Type, Decoder) -> ((CodingKey) -> NodeDecoding)) + + func nodeDecodings( + forType codableType: Decodable.Type, + with decoder: Decoder + ) -> ((CodingKey) -> NodeDecoding?) { + switch self { + case .deferredToDecoder: + guard let dynamicType = codableType as? DynamicNodeDecoding.Type else { + return { _ in nil } + } + return dynamicType.nodeDecoding(for:) + case let .custom(closure): + return closure(codableType, decoder) + } + } + } + + /// Contextual user-provided information for use during decoding. + open var userInfo: [CodingUserInfoKey: Any] = [:] + + /// The error context length. Non-zero length makes an error thrown from + /// the XML parser with line/column location repackaged with a context + /// around that location of specified length. For example, if an error was + /// thrown indicating that there's an unexpected character at line 3, column + /// 15 with `errorContextLength` set to 10, a new error type is rethrown + /// containing 5 characters before column 15 and 5 characters after, all on + /// line 3. Line wrapping should be handled correctly too as the context can + /// span more than a few lines. + open var errorContextLength: UInt = 0 + + /** A boolean value that determines whether the parser reports the + namespaces and qualified names of elements. The default value is `false`. + */ + open var shouldProcessNamespaces: Bool = false + + /** A boolean value that determines whether the parser trims whitespaces + and newlines from the end and the beginning of string values. The default + value is `true`. + */ + open var trimValueWhitespaces: Bool + + /** A boolean value that determines whether to remove pure whitespace elements + that have sibling elements that aren't pure whitespace. The default value + is `false`. + */ + open var removeWhitespaceElements: Bool + + /// Options set on the top-level encoder to pass down the decoding hierarchy. + struct Options { + let dateDecodingStrategy: DateDecodingStrategy + let dataDecodingStrategy: DataDecodingStrategy + let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy + let keyDecodingStrategy: KeyDecodingStrategy + let nodeDecodingStrategy: NodeDecodingStrategy + let userInfo: [CodingUserInfoKey: Any] + } + + /// The options set on the top-level decoder. + var options: Options { + return Options( + dateDecodingStrategy: dateDecodingStrategy, + dataDecodingStrategy: dataDecodingStrategy, + nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy, + keyDecodingStrategy: keyDecodingStrategy, + nodeDecodingStrategy: nodeDecodingStrategy, + userInfo: userInfo + ) + } + + // MARK: - Constructing a XML Decoder + + /// Initializes `self` with default strategies. + public init(trimValueWhitespaces: Bool = true, removeWhitespaceElements: Bool = false) { + self.trimValueWhitespaces = trimValueWhitespaces + self.removeWhitespaceElements = removeWhitespaceElements + } + + // MARK: - Decoding Values + + /// Decodes a top-level box of the given type from the given XML representation. + /// + /// - parameter type: The type of the box to decode. + /// - parameter data: The data to decode from. + /// - returns: A box of the requested type. + /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid XML. + /// - throws: An error if any box throws an error during decoding. + open func decode( + _ type: T.Type, + from data: Data + ) throws -> T { + let topLevel: Box = try XMLStackParser.parse( + with: data, + errorContextLength: errorContextLength, + shouldProcessNamespaces: shouldProcessNamespaces, + trimValueWhitespaces: trimValueWhitespaces, + removeWhitespaceElements: removeWhitespaceElements + ) + + let decoder = XMLDecoderImplementation( + referencing: topLevel, + options: options, + nodeDecodings: [] + ) + decoder.nodeDecodings = [ + options.nodeDecodingStrategy.nodeDecodings( + forType: T.self, + with: decoder + ), + ] + + defer { + _ = decoder.nodeDecodings.removeLast() + } + + return try decoder.unbox(topLevel) + } +} + +// MARK: TopLevelDecoder + +#if canImport(Combine) +import protocol Combine.TopLevelDecoder +import protocol Combine.TopLevelEncoder +#elseif canImport(OpenCombine) +import protocol OpenCombine.TopLevelDecoder +import protocol OpenCombine.TopLevelEncoder +#endif + +#if canImport(Combine) || canImport(OpenCombine) +extension XMLDecoder: TopLevelDecoder {} +extension XMLEncoder: TopLevelEncoder {} +#endif diff --git a/third-party/XMLCoder/Sources/Decoder/XMLDecoderImplementation.swift b/third-party/XMLCoder/Sources/Decoder/XMLDecoderImplementation.swift new file mode 100644 index 0000000000..961322f35d --- /dev/null +++ b/third-party/XMLCoder/Sources/Decoder/XMLDecoderImplementation.swift @@ -0,0 +1,497 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/20/17. +// + +import Foundation + +class XMLDecoderImplementation: Decoder { + // MARK: Properties + + /// The decoder's storage. + var storage = XMLDecodingStorage() + + /// Options set on the top-level decoder. + let options: XMLDecoder.Options + + /// The path to the current point in encoding. + public internal(set) var codingPath: [CodingKey] + + public var nodeDecodings: [(CodingKey) -> XMLDecoder.NodeDecoding?] + + /// Contextual user-provided information for use during encoding. + public var userInfo: [CodingUserInfoKey: Any] { + return options.userInfo + } + + // The error context length + open var errorContextLength: UInt = 0 + + // MARK: - Initialization + + /// Initializes `self` with the given top-level container and options. + init( + referencing container: Box, + options: XMLDecoder.Options, + nodeDecodings: [(CodingKey) -> XMLDecoder.NodeDecoding?], + codingPath: [CodingKey] = [] + ) { + storage.push(container: container) + self.codingPath = codingPath + self.nodeDecodings = nodeDecodings + self.options = options + } + + // MARK: - Decoder Methods + + internal func topContainer() throws -> Box { + guard let topContainer = storage.topContainer() else { + throw DecodingError.valueNotFound(Box.self, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Cannot get decoding container -- empty container stack." + )) + } + return topContainer + } + + private func popContainer() throws -> Box { + guard let topContainer = storage.popContainer() else { + throw DecodingError.valueNotFound(Box.self, DecodingError.Context( + codingPath: codingPath, + debugDescription: + """ + Cannot get decoding container -- empty container stack. + """ + )) + } + return topContainer + } + + public func container(keyedBy keyType: Key.Type) throws -> KeyedDecodingContainer { + if let keyed = try topContainer() as? SharedBox { + return KeyedDecodingContainer(XMLKeyedDecodingContainer( + referencing: self, + wrapping: keyed + )) + } + if Key.self is XMLChoiceCodingKey.Type { + return try choiceContainer(keyedBy: keyType) + } else { + return try keyedContainer(keyedBy: keyType) + } + } + + public func unkeyedContainer() throws -> UnkeyedDecodingContainer { + let topContainer = try self.topContainer() + + guard !topContainer.isNull else { + throw DecodingError.valueNotFound( + UnkeyedDecodingContainer.self, + DecodingError.Context( + codingPath: codingPath, + debugDescription: + """ + Cannot get unkeyed decoding container -- found null box instead. + """ + ) + ) + } + + switch topContainer { + case let unkeyed as SharedBox: + return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed) + case let keyed as SharedBox: + return XMLUnkeyedDecodingContainer( + referencing: self, + wrapping: SharedBox(keyed.withShared { $0.elements.map(SingleKeyedBox.init) }) + ) + default: + throw DecodingError.typeMismatch( + at: codingPath, + expectation: [Any].self, + reality: topContainer + ) + } + } + + public func singleValueContainer() throws -> SingleValueDecodingContainer { + return self + } + + private func keyedContainer(keyedBy _: Key.Type) throws -> KeyedDecodingContainer { + let topContainer = try self.topContainer() + let keyedBox: KeyedBox + switch topContainer { + case _ where topContainer.isNull: + throw DecodingError.valueNotFound( + KeyedDecodingContainer.self, + DecodingError.Context( + codingPath: codingPath, + debugDescription: + """ + Cannot get keyed decoding container -- found null box instead. + """ + ) + ) + case let string as StringBox: + keyedBox = KeyedBox( + elements: KeyedStorage([("", string)]), + attributes: KeyedStorage() + ) + case let containsEmpty as SingleKeyedBox where containsEmpty.element is NullBox: + keyedBox = KeyedBox( + elements: KeyedStorage([("", StringBox(""))]), + attributes: KeyedStorage() + ) + case let unkeyed as SharedBox: + guard let keyed = unkeyed.withShared({ $0.first }) as? KeyedBox else { + fallthrough + } + keyedBox = keyed + default: + throw DecodingError.typeMismatch( + at: codingPath, + expectation: [String: Any].self, + reality: topContainer + ) + } + let container = XMLKeyedDecodingContainer( + referencing: self, + wrapping: SharedBox(keyedBox) + ) + return KeyedDecodingContainer(container) + } + + /// - Returns: A `KeyedDecodingContainer` for an XML choice element. + private func choiceContainer(keyedBy _: Key.Type) throws -> KeyedDecodingContainer { + let topContainer = try self.topContainer() + let choiceBox: ChoiceBox + switch topContainer { + case let choice as ChoiceBox: + choiceBox = choice + case let singleKeyed as SingleKeyedBox: + choiceBox = ChoiceBox(singleKeyed) + default: + throw DecodingError.typeMismatch( + at: codingPath, + expectation: [String: Any].self, + reality: topContainer + ) + } + let container = XMLChoiceDecodingContainer( + referencing: self, + wrapping: SharedBox(choiceBox) + ) + return KeyedDecodingContainer(container) + } +} + +// MARK: - Concrete Value Representations + +extension XMLDecoderImplementation { + /// Returns the given box unboxed from a container. + private func typedBox(_ box: Box, for valueType: T.Type) throws -> B { + let error = DecodingError.valueNotFound(valueType, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Expected \(valueType) but found null instead." + )) + switch box { + case let typedBox as B: + return typedBox + case let unkeyedBox as SharedBox: + guard let value = unkeyedBox.withShared({ + $0.first as? B + }) else { throw error } + return value + case let keyedBox as SharedBox: + guard + let value = keyedBox.withShared({ $0.value as? B }) + else { throw error } + return value + case let singleKeyedBox as SingleKeyedBox: + if let value = singleKeyedBox.element as? B { + return value + } else if let box = singleKeyedBox.element as? KeyedBox, let value = box.elements[""].first as? B { + return value + } else { + throw error + } + case is NullBox: + throw error + case let keyedBox as KeyedBox: + guard + let value = keyedBox.value as? B + else { fallthrough } + return value + default: + throw DecodingError.typeMismatch( + at: codingPath, + expectation: valueType, + reality: box + ) + } + } + + func unbox(_ box: Box) throws -> Bool { + let stringBox: StringBox = try typedBox(box, for: Bool.self) + let string = stringBox.unboxed + + guard let boolBox = BoolBox(xmlString: string) else { + throw DecodingError.typeMismatch(at: codingPath, expectation: Bool.self, reality: box) + } + + return boolBox.unboxed + } + + func unbox(_ box: Box) throws -> Decimal { + let stringBox: StringBox = try typedBox(box, for: Decimal.self) + let string = stringBox.unboxed + + guard let decimalBox = DecimalBox(xmlString: string) else { + throw DecodingError.typeMismatch(at: codingPath, expectation: Decimal.self, reality: box) + } + + return decimalBox.unboxed + } + + func unbox(_ box: Box) throws -> T { + let stringBox: StringBox = try typedBox(box, for: T.self) + let string = stringBox.unboxed + + guard let intBox = IntBox(xmlString: string) else { + throw DecodingError.typeMismatch(at: codingPath, expectation: T.self, reality: box) + } + + guard let int: T = intBox.unbox() else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)." + )) + } + + return int + } + + func unbox(_ box: Box) throws -> T { + let stringBox: StringBox = try typedBox(box, for: T.self) + let string = stringBox.unboxed + + guard let uintBox = UIntBox(xmlString: string) else { + throw DecodingError.typeMismatch(at: codingPath, expectation: T.self, reality: box) + } + + guard let uint: T = uintBox.unbox() else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)." + )) + } + + return uint + } + + func unbox(_ box: Box) throws -> Float { + let stringBox: StringBox = try typedBox(box, for: Float.self) + let string = stringBox.unboxed + + guard let floatBox = FloatBox(xmlString: string) else { + throw DecodingError.typeMismatch(at: codingPath, expectation: Float.self, reality: box) + } + + return floatBox.unboxed + } + + func unbox(_ box: Box) throws -> Double { + let stringBox: StringBox = try typedBox(box, for: Double.self) + let string = stringBox.unboxed + + guard let doubleBox = DoubleBox(xmlString: string) else { + throw DecodingError.typeMismatch(at: codingPath, expectation: Double.self, reality: box) + } + + return doubleBox.unboxed + } + + func unbox(_ box: Box) throws -> String { + do { + let stringBox: StringBox = try typedBox(box, for: String.self) + return stringBox.unboxed + } catch { + if box is NullBox { + return "" + } + } + + return "" + } + + func unbox(_ box: Box) throws -> Date { + switch options.dateDecodingStrategy { + case .deferredToDate: + storage.push(container: box) + defer { storage.popContainer() } + return try Date(from: self) + + case .secondsSince1970: + let stringBox: StringBox = try typedBox(box, for: Date.self) + let string = stringBox.unboxed + + guard let dateBox = DateBox(secondsSince1970: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Expected date string to be formatted in seconds since 1970." + )) + } + return dateBox.unboxed + case .millisecondsSince1970: + let stringBox: StringBox = try typedBox(box, for: Date.self) + let string = stringBox.unboxed + + guard let dateBox = DateBox(millisecondsSince1970: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Expected date string to be formatted in milliseconds since 1970." + )) + } + return dateBox.unboxed + case .iso8601: + let stringBox: StringBox = try typedBox(box, for: Date.self) + let string = stringBox.unboxed + + guard let dateBox = DateBox(iso8601: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Expected date string to be ISO8601-formatted." + )) + } + return dateBox.unboxed + case let .formatted(formatter): + let stringBox: StringBox = try typedBox(box, for: Date.self) + let string = stringBox.unboxed + + guard let dateBox = DateBox(xmlString: string, formatter: formatter) else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Date string does not match format expected by formatter." + )) + } + return dateBox.unboxed + case let .custom(closure): + storage.push(container: box) + defer { storage.popContainer() } + return try closure(self) + } + } + + func unbox(_ box: Box) throws -> Data { + switch options.dataDecodingStrategy { + case .deferredToData: + storage.push(container: box) + defer { storage.popContainer() } + return try Data(from: self) + case .base64: + let stringBox: StringBox = try typedBox(box, for: Data.self) + let string = stringBox.unboxed + + guard let dataBox = DataBox(base64: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Encountered Data is not valid Base64" + )) + } + return dataBox.unboxed + case let .custom(closure): + storage.push(container: box) + defer { storage.popContainer() } + return try closure(self) + } + } + + func unbox(_ box: Box) throws -> URL { + let stringBox: StringBox = try typedBox(box, for: URL.self) + let string = stringBox.unboxed + + guard let urlBox = URLBox(xmlString: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Encountered Data is not valid Base64" + )) + } + + return urlBox.unboxed + } + + func unbox(_ box: Box) throws -> T { + let decoded: T? + let type = T.self + + if type == Date.self || type == NSDate.self { + let date: Date = try unbox(box) + decoded = date as? T + } else if type == Data.self || type == NSData.self { + let data: Data = try unbox(box) + decoded = data as? T + } else if type == URL.self || type == NSURL.self { + let data: URL = try unbox(box) + decoded = data as? T + } else if type == Decimal.self || type == NSDecimalNumber.self { + let decimal: Decimal = try unbox(box) + decoded = decimal as? T + } else if + type == String.self || type == NSString.self, + let value = (try unbox(box) as String) as? T + { + decoded = value + } else { + storage.push(container: box) + defer { + storage.popContainer() + } + + do { + decoded = try type.init(from: self) + } catch { + guard case DecodingError.valueNotFound = error, + let type = type as? AnyOptional.Type, + let result = type.init() as? T + else { + throw error + } + + return result + } + } + + guard let result = decoded else { + throw DecodingError.typeMismatch( + at: codingPath, expectation: type, reality: box + ) + } + + return result + } +} + +extension XMLDecoderImplementation { + var keyTransform: (String) -> String { + switch options.keyDecodingStrategy { + case .convertFromSnakeCase: + return XMLDecoder.KeyDecodingStrategy._convertFromSnakeCase + case .convertFromCapitalized: + return XMLDecoder.KeyDecodingStrategy._convertFromCapitalized + case .convertFromUppercase: + return XMLDecoder.KeyDecodingStrategy._convertFromUppercase + case .convertFromKebabCase: + return XMLDecoder.KeyDecodingStrategy._convertFromKebabCase + case .useDefaultKeys: + return { key in key } + case let .custom(converter): + return { key in + converter(self.codingPath + [XMLKey(stringValue: key, intValue: nil)]).stringValue + } + } + } +} diff --git a/third-party/XMLCoder/Sources/Decoder/XMLDecodingStorage.swift b/third-party/XMLCoder/Sources/Decoder/XMLDecodingStorage.swift new file mode 100644 index 0000000000..18d3bdd0ab --- /dev/null +++ b/third-party/XMLCoder/Sources/Decoder/XMLDecodingStorage.swift @@ -0,0 +1,52 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/20/17. +// + +import Foundation + +// MARK: - Decoding Storage + +struct XMLDecodingStorage { + // MARK: Properties + + /// The container stack. + /// Elements may be any one of the XML types (StringBox, KeyedBox). + private var containers: [Box] = [] + + // MARK: - Initialization + + /// Initializes `self` with no containers. + init() {} + + // MARK: - Modifying the Stack + + var count: Int { + return containers.count + } + + func topContainer() -> Box? { + return containers.last + } + + mutating func push(container: Box) { + if let keyedBox = container as? KeyedBox { + containers.append(SharedBox(keyedBox)) + } else if let unkeyedBox = container as? UnkeyedBox { + containers.append(SharedBox(unkeyedBox)) + } else { + containers.append(container) + } + } + + @discardableResult + mutating func popContainer() -> Box? { + guard !containers.isEmpty else { + return nil + } + return containers.removeLast() + } +} diff --git a/third-party/XMLCoder/Sources/Decoder/XMLKeyedDecodingContainer.swift b/third-party/XMLCoder/Sources/Decoder/XMLKeyedDecodingContainer.swift new file mode 100644 index 0000000000..826b92b90a --- /dev/null +++ b/third-party/XMLCoder/Sources/Decoder/XMLKeyedDecodingContainer.swift @@ -0,0 +1,417 @@ +// Copyright (c) 2017-2021 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/21/17. +// + +import Foundation + +// MARK: Decoding Containers + +struct XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { + typealias Key = K + typealias KeyedContainer = SharedBox + typealias UnkeyedContainer = SharedBox + + // MARK: Properties + + /// A reference to the decoder we're reading from. + private let decoder: XMLDecoderImplementation + + /// A reference to the container we're reading from. + private let container: KeyedContainer + + /// The path of coding keys taken to get to this point in decoding. + public private(set) var codingPath: [CodingKey] + + // MARK: - Initialization + + /// Initializes `self` by referencing the given decoder and container. + init( + referencing decoder: XMLDecoderImplementation, + wrapping container: KeyedContainer + ) { + self.decoder = decoder + container.withShared { + $0.elements = .init($0.elements.map { (decoder.keyTransform($0), $1) }) + $0.attributes = .init($0.attributes.map { (decoder.keyTransform($0), $1) }) + } + self.container = container + codingPath = decoder.codingPath + } + + // MARK: - KeyedDecodingContainerProtocol Methods + + public var allKeys: [Key] { + let elementKeys = container.withShared { keyedBox in + keyedBox.elements.keys.compactMap { Key(stringValue: $0) } + } + + let attributeKeys = container.withShared { keyedBox in + keyedBox.attributes.keys.compactMap { Key(stringValue: $0) } + } + + return attributeKeys + elementKeys + } + + public func contains(_ key: Key) -> Bool { + let elements = container.withShared { keyedBox in + keyedBox.elements[key.stringValue] + } + + let attributes = container.withShared { keyedBox in + keyedBox.attributes[key.stringValue] + } + + return !elements.isEmpty || !attributes.isEmpty + } + + public func decodeNil(forKey key: Key) throws -> Bool { + let elements = container.withShared { keyedBox in + keyedBox.elements[key.stringValue] + } + + let attributes = container.withShared { keyedBox in + keyedBox.attributes[key.stringValue] + } + + let box = elements.first ?? attributes.first + + if box is SingleKeyedBox { + return false + } + + return box?.isNull ?? true + } + + public func decode( + _ type: T.Type, forKey key: Key + ) throws -> T { + let attributeFound = container.withShared { keyedBox in + !keyedBox.attributes[key.stringValue].isEmpty + } + + let elementFound = container.withShared { keyedBox in + !keyedBox.elements[key.stringValue].isEmpty || keyedBox.value != nil + } + + if let type = type as? XMLDecodableSequence.Type, + !attributeFound, + !elementFound, + let result = type.init() as? T + { + return result + } + + return try decodeConcrete(type, forKey: key) + } + + public func nestedContainer( + keyedBy _: NestedKey.Type, forKey key: Key + ) throws -> KeyedDecodingContainer { + decoder.codingPath.append(key) + defer { decoder.codingPath.removeLast() } + + let elements = self.container.withShared { keyedBox in + keyedBox.elements[key.stringValue] + } + + let attributes = self.container.withShared { keyedBox in + keyedBox.attributes[key.stringValue] + } + + guard let value = elements.first ?? attributes.first else { + throw DecodingError.keyNotFound(key, DecodingError.Context( + codingPath: codingPath, + debugDescription: + """ + Cannot get \(KeyedDecodingContainer.self) -- \ + no value found for key \"\(key.stringValue)\" + """ + )) + } + + guard let container = XMLKeyedDecodingContainer(box: value, decoder: decoder) else { + throw DecodingError.typeMismatch( + at: codingPath, + expectation: [String: Any].self, + reality: value + ) + } + + return KeyedDecodingContainer(container) + } + + public func nestedUnkeyedContainer( + forKey key: Key + ) throws -> UnkeyedDecodingContainer { + decoder.codingPath.append(key) + defer { decoder.codingPath.removeLast() } + + let elements = container.unboxed.elements[key.stringValue] + + if let containsKeyed = elements as? [KeyedBox], containsKeyed.count == 1, let keyed = containsKeyed.first { + return XMLUnkeyedDecodingContainer( + referencing: decoder, + wrapping: SharedBox(keyed.elements.map(SingleKeyedBox.init)) + ) + } else { + return XMLUnkeyedDecodingContainer( + referencing: decoder, + wrapping: SharedBox(elements) + ) + } + } + + public func superDecoder() throws -> Decoder { + return try _superDecoder(forKey: XMLKey.super) + } + + public func superDecoder(forKey key: Key) throws -> Decoder { + return try _superDecoder(forKey: key) + } +} + +extension XMLKeyedDecodingContainer { + internal init?(box: Box, decoder: XMLDecoderImplementation) { + switch box { + case let keyedContainer as KeyedContainer: + self.init( + referencing: decoder, + wrapping: keyedContainer + ) + case let keyedBox as KeyedBox: + self.init( + referencing: decoder, + wrapping: SharedBox(keyedBox) + ) + case let singleBox as SingleKeyedBox: + let element = (singleBox.key, singleBox.element) + let keyedContainer = KeyedBox(elements: [element], attributes: []) + self.init( + referencing: decoder, + wrapping: SharedBox(keyedContainer) + ) + default: + return nil + } + } +} + +/// Private functions +extension XMLKeyedDecodingContainer { + private func _errorDescription(of key: CodingKey) -> String { + switch decoder.options.keyDecodingStrategy { + case .convertFromSnakeCase: + // In this case we can attempt to recover the original value by + // reversing the transform + let original = key.stringValue + let converted = XMLEncoder.KeyEncodingStrategy + ._convertToSnakeCase(original) + if converted == original { + return "\(key) (\"\(original)\")" + } else { + return "\(key) (\"\(original)\"), converted to \(converted)" + } + default: + // Otherwise, just report the converted string + return "\(key) (\"\(key.stringValue)\")" + } + } + + private func decodeSignedInteger(_ type: T.Type, + forKey key: Key) throws -> T + where T: BinaryInteger & SignedInteger & Decodable + { + return try decodeConcrete(type, forKey: key) + } + + private func decodeUnsignedInteger(_ type: T.Type, + forKey key: Key) throws -> T + where T: BinaryInteger & UnsignedInteger & Decodable + { + return try decodeConcrete(type, forKey: key) + } + + private func decodeFloatingPoint(_ type: T.Type, + forKey key: Key) throws -> T + where T: BinaryFloatingPoint & Decodable + { + return try decodeConcrete(type, forKey: key) + } + + private func decodeConcrete( + _ type: T.Type, + forKey key: Key + ) throws -> T { + guard let strategy = decoder.nodeDecodings.last else { + preconditionFailure( + """ + Attempt to access node decoding strategy from empty stack. + """ + ) + } + + let elements = container + .withShared { keyedBox -> [KeyedBox.Element] in + return (key.isInlined ? keyedBox.elements.values : keyedBox.elements[key.stringValue]).map { + if let singleKeyed = $0 as? SingleKeyedBox { + return singleKeyed.element.isNull ? singleKeyed : singleKeyed.element + } else { + return $0 + } + } + } + + let attributes = container.withShared { keyedBox in + key.isInlined ? keyedBox.attributes.values : keyedBox.attributes[key.stringValue] + } + + decoder.codingPath.append(key) + let nodeDecodings = decoder.options.nodeDecodingStrategy.nodeDecodings( + forType: T.self, + with: decoder + ) + decoder.nodeDecodings.append(nodeDecodings) + defer { + _ = decoder.nodeDecodings.removeLast() + decoder.codingPath.removeLast() + } + + // You can't decode sequences from attributes, but other strategies + // need special handling for empty sequences. + if strategy(key) != .attribute && elements.isEmpty, + let empty = (type as? XMLDecodableSequence.Type)?.init() as? T + { + return empty + } + + // If we are looking at a coding key value intrinsic where the expected type is `String` and + // the value is empty, return CDATA if present otherwise `""`. + if strategy(key) != .attribute, elements.isEmpty, attributes.isEmpty, type == String.self, key.stringValue == "", let emptyString = "" as? T { + let cdata = container.withShared { keyedBox in + keyedBox.elements["#CDATA"].map { + return ($0 as? KeyedBox)?.value ?? $0 + } + }.first + return ((cdata as? StringBox)?.unboxed as? T) ?? emptyString + } + + let box: Box + if key.isInlined { + box = container.typeErasedUnbox() + } else { + switch strategy(key) { + case .attribute?: + box = try getAttributeBox(for: type, attributes, key) + case .element?: + box = try getElementBox(for: type, elements, key) + case .elementOrAttribute?: + box = try getAttributeOrElementBox(attributes, elements, key) + default: + switch type { + case is XMLAttributeProtocol.Type: + box = try getAttributeBox(for: type, attributes, key) + case is XMLElementProtocol.Type: + box = try getElementBox(for: type, elements, key) + default: + box = try getAttributeOrElementBox(attributes, elements, key) + } + } + } + + let value: T? + if !(type is XMLDecodableSequence.Type), let unkeyedBox = box as? UnkeyedBox, + let first = unkeyedBox.first + { + // Handle case where we have held onto a `SingleKeyedBox` + if let singleKeyed = first as? SingleKeyedBox { + if singleKeyed.element.isNull { + value = try decoder.unbox(singleKeyed) + } else { + value = try decoder.unbox(singleKeyed.element) + } + } else { + value = try decoder.unbox(first) + } + } else if box.isNull, let type = type as? XMLOptionalAttributeProtocol.Type, let nullAttribute = type.init() as? T { + value = nullAttribute + } else { + value = try decoder.unbox(box) + } + + if value == nil, let type = type as? AnyOptional.Type, + let result = type.init() as? T + { + return result + } + + guard let unwrapped = value else { + throw DecodingError.valueNotFound(type, DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: + "Expected \(type) value but found null instead." + )) + } + return unwrapped + } + + private func getAttributeBox(for type: T.Type, _ attributes: [KeyedBox.Attribute], _ key: Key) throws -> Box { + if let box = attributes.first { return box } + if type is AnyOptional.Type || type is XMLOptionalAttributeProtocol.Type { return NullBox() } + + throw DecodingError.keyNotFound(key, DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "No attribute found for key \(_errorDescription(of: key))." + )) + } + + private func getElementBox(for type: T.Type, _ elements: [KeyedBox.Element], _ key: Key) throws -> Box { + guard elements.isEmpty else { return elements } + if type is AnyOptional.Type || type is XMLDecodableSequence.Type { return elements } + + throw DecodingError.keyNotFound(key, DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "No element found for key \(_errorDescription(of: key))." + )) + } + + private func getAttributeOrElementBox(_ attributes: [KeyedBox.Attribute], _ elements: [KeyedBox.Element], _ key: Key) throws -> Box { + guard + let anyBox = elements.isEmpty ? attributes.first : elements as Box? + else { + throw DecodingError.keyNotFound(key, DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: + """ + No attribute or element found for key \ + \(_errorDescription(of: key)). + """ + )) + } + return anyBox + } + + private func _superDecoder(forKey key: CodingKey) throws -> Decoder { + decoder.codingPath.append(key) + defer { decoder.codingPath.removeLast() } + + let elements = container.withShared { keyedBox in + keyedBox.elements[key.stringValue] + } + + let attributes = container.withShared { keyedBox in + keyedBox.attributes[key.stringValue] + } + + let box: Box = elements.first ?? attributes.first ?? NullBox() + return XMLDecoderImplementation( + referencing: box, + options: decoder.options, + nodeDecodings: decoder.nodeDecodings, + codingPath: decoder.codingPath + ) + } +} diff --git a/third-party/XMLCoder/Sources/Decoder/XMLUnkeyedDecodingContainer.swift b/third-party/XMLCoder/Sources/Decoder/XMLUnkeyedDecodingContainer.swift new file mode 100644 index 0000000000..0ba8b3a204 --- /dev/null +++ b/third-party/XMLCoder/Sources/Decoder/XMLUnkeyedDecodingContainer.swift @@ -0,0 +1,232 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/21/17. +// + +import Foundation + +struct XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { + // MARK: Properties + + /// A reference to the decoder we're reading from. + private let decoder: XMLDecoderImplementation + + /// A reference to the container we're reading from. + private let container: SharedBox + + /// The path of coding keys taken to get to this point in decoding. + public private(set) var codingPath: [CodingKey] + + /// The index of the element we're about to decode. + public private(set) var currentIndex: Int + + // MARK: - Initialization + + /// Initializes `self` by referencing the given decoder and container. + init(referencing decoder: XMLDecoderImplementation, wrapping container: SharedBox) { + self.decoder = decoder + self.container = container + codingPath = decoder.codingPath + currentIndex = 0 + } + + // MARK: - UnkeyedDecodingContainer Methods + + public var count: Int? { + return container.withShared { unkeyedBox in + unkeyedBox.count + } + } + + public var isAtEnd: Bool { + return currentIndex >= count! + } + + public mutating func decodeNil() throws -> Bool { + guard !isAtEnd else { + throw DecodingError.valueNotFound(Any?.self, DecodingError.Context( + codingPath: decoder.codingPath + [XMLKey(index: currentIndex)], + debugDescription: "Unkeyed container is at end." + )) + } + + let isNull = container.withShared { unkeyedBox in + unkeyedBox[self.currentIndex].isNull + } + + if isNull { + currentIndex += 1 + return true + } else { + return false + } + } + + public mutating func decode(_ type: T.Type) throws -> T { + return try decode(type) { decoder, box in + try decoder.unbox(box) + } + } + + private mutating func decode( + _ type: T.Type, + decode: (XMLDecoderImplementation, Box) throws -> T? + ) throws -> T { + decoder.codingPath.append(XMLKey(index: currentIndex)) + let nodeDecodings = decoder.options.nodeDecodingStrategy.nodeDecodings( + forType: T.self, + with: decoder + ) + decoder.nodeDecodings.append(nodeDecodings) + defer { + _ = decoder.nodeDecodings.removeLast() + _ = decoder.codingPath.removeLast() + } + guard !isAtEnd else { + throw DecodingError.valueNotFound(type, DecodingError.Context( + codingPath: decoder.codingPath + [XMLKey(index: currentIndex)], + debugDescription: "Unkeyed container is at end." + )) + } + + decoder.codingPath.append(XMLKey(index: currentIndex)) + defer { self.decoder.codingPath.removeLast() } + + let box = container.withShared { unkeyedBox in + unkeyedBox[self.currentIndex] + } + + var value: T? + if let singleKeyed = box as? SingleKeyedBox { + do { + value = try decode(decoder, singleKeyed) + } catch { + do { + // Drill down to the element in the case of an nested unkeyed element + value = try decode(decoder, singleKeyed.element) + } catch { + // Specialize for choice elements + value = try decode(decoder, ChoiceBox(key: singleKeyed.key, element: singleKeyed.element)) + } + } + } else { + value = try decode(decoder, box) + } + + defer { currentIndex += 1 } + + if value == nil, let type = type as? AnyOptional.Type, + let result = type.init() as? T + { + return result + } + + guard let decoded: T = value else { + throw DecodingError.valueNotFound(type, DecodingError.Context( + codingPath: decoder.codingPath + [XMLKey(index: currentIndex)], + debugDescription: "Expected \(type) but found null instead." + )) + } + + return decoded + } + + public mutating func nestedContainer( + keyedBy _: NestedKey.Type + ) throws -> KeyedDecodingContainer { + decoder.codingPath.append(XMLKey(index: currentIndex)) + defer { self.decoder.codingPath.removeLast() } + + guard !isAtEnd else { + throw DecodingError.valueNotFound( + KeyedDecodingContainer.self, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Cannot get nested keyed container -- unkeyed container is at end." + ) + ) + } + + let value = self.container.withShared { unkeyedBox in + unkeyedBox[self.currentIndex] + } + guard !value.isNull else { + throw DecodingError.valueNotFound(KeyedDecodingContainer.self, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Cannot get keyed decoding container -- found null value instead." + )) + } + + guard let keyedContainer = value as? SharedBox else { + throw DecodingError.typeMismatch(at: codingPath, + expectation: [String: Any].self, + reality: value) + } + + currentIndex += 1 + let container = XMLKeyedDecodingContainer( + referencing: decoder, + wrapping: keyedContainer + ) + return KeyedDecodingContainer(container) + } + + public mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + decoder.codingPath.append(XMLKey(index: currentIndex)) + defer { self.decoder.codingPath.removeLast() } + + guard !isAtEnd else { + throw DecodingError.valueNotFound( + UnkeyedDecodingContainer.self, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Cannot get nested keyed container -- unkeyed container is at end." + ) + ) + } + + let value = container.withShared { unkeyedBox in + unkeyedBox[self.currentIndex] + } + guard !value.isNull else { + throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Cannot get keyed decoding container -- found null value instead." + )) + } + + guard let unkeyedContainer = value as? SharedBox else { + throw DecodingError.typeMismatch(at: codingPath, + expectation: UnkeyedBox.self, + reality: value) + } + + currentIndex += 1 + return XMLUnkeyedDecodingContainer(referencing: decoder, wrapping: unkeyedContainer) + } + + public mutating func superDecoder() throws -> Decoder { + decoder.codingPath.append(XMLKey(index: currentIndex)) + defer { self.decoder.codingPath.removeLast() } + + guard !isAtEnd else { + throw DecodingError.valueNotFound(Decoder.self, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Cannot get superDecoder() -- unkeyed container is at end." + )) + } + + let value = container.withShared { unkeyedBox in + unkeyedBox[self.currentIndex] + } + currentIndex += 1 + + return XMLDecoderImplementation( + referencing: value, + options: decoder.options, + nodeDecodings: decoder.nodeDecodings, + codingPath: decoder.codingPath + ) + } +} diff --git a/third-party/XMLCoder/Sources/Encoder/DynamicNodeEncoding.swift b/third-party/XMLCoder/Sources/Encoder/DynamicNodeEncoding.swift new file mode 100644 index 0000000000..bbb79fa82b --- /dev/null +++ b/third-party/XMLCoder/Sources/Encoder/DynamicNodeEncoding.swift @@ -0,0 +1,57 @@ +// Copyright (c) 2019-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Joseph Mattiello on 1/24/19. +// + +/** Allows conforming types to specify how its properties will be encoded. + + For example: + ```swift + struct Book: Codable, Equatable, DynamicNodeEncoding { + let id: UInt + let title: String + let categories: [Category] + + enum CodingKeys: String, CodingKey { + case id + case title + case categories = "category" + } + + static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case Book.CodingKeys.id: return .both + default: return .element + } + } + } + ``` + produces XML of this form for values of type `Book`: + + ```xml + + 123 + Cat in the Hat + Kids + Wildlife + + ``` + */ +public protocol DynamicNodeEncoding: Encodable, Sendable { + static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding +} + +extension Array: DynamicNodeEncoding where Element: DynamicNodeEncoding { + public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { + return Element.nodeEncoding(for: key) + } +} + +public extension DynamicNodeEncoding where Self: Collection, Self.Iterator.Element: DynamicNodeEncoding { + static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { + return Element.nodeEncoding(for: key) + } +} diff --git a/third-party/XMLCoder/Sources/Encoder/EncodingErrorExtension.swift b/third-party/XMLCoder/Sources/Encoder/EncodingErrorExtension.swift new file mode 100644 index 0000000000..a364159669 --- /dev/null +++ b/third-party/XMLCoder/Sources/Encoder/EncodingErrorExtension.swift @@ -0,0 +1,36 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/22/17. +// + +import Foundation + +/// Error Utilities +extension EncodingError { + /// Returns a `.invalidValue` error describing the given invalid floating-point value. + /// + /// + /// - parameter value: The value that was invalid to encode. + /// - parameter path: The path of `CodingKey`s taken to encode this value. + /// - returns: An `EncodingError` with the appropriate path and debug description. + static func _invalidFloatingPointValue(_ value: T, at codingPath: [CodingKey]) -> EncodingError { + let valueDescription: String + if value == T.infinity { + valueDescription = "\(T.self).infinity" + } else if value == -T.infinity { + valueDescription = "-\(T.self).infinity" + } else { + valueDescription = "\(T.self).nan" + } + + let debugDescription = """ + Unable to encode \(valueDescription) directly in XML. \ + Use XMLEncoder.NonConformingFloatEncodingStrategy.convertToString \ + to specify how the value should be encoded. + """ + return .invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: debugDescription)) + } +} diff --git a/third-party/XMLCoder/Sources/Encoder/SingleValueEncodingContainer.swift b/third-party/XMLCoder/Sources/Encoder/SingleValueEncodingContainer.swift new file mode 100644 index 0000000000..2d96fdc771 --- /dev/null +++ b/third-party/XMLCoder/Sources/Encoder/SingleValueEncodingContainer.swift @@ -0,0 +1,103 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/22/17. +// + +import Foundation + +extension XMLEncoderImplementation: SingleValueEncodingContainer { + // MARK: - SingleValueEncodingContainer Methods + + func assertCanEncodeNewValue() { + precondition( + canEncodeNewValue, + """ + Attempt to encode value through single value container when \ + previously value already encoded. + """ + ) + } + + public func encodeNil() throws { + assertCanEncodeNewValue() + storage.push(container: box()) + } + + public func encode(_ value: Bool) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int8) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int16) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int32) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int64) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt8) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt16) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt32) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt64) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: String) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Float) throws { + assertCanEncodeNewValue() + try storage.push(container: box(value)) + } + + public func encode(_ value: Double) throws { + assertCanEncodeNewValue() + try storage.push(container: box(value)) + } + + public func encode(_ value: T) throws { + assertCanEncodeNewValue() + try storage.push(container: box(value)) + } +} diff --git a/third-party/XMLCoder/Sources/Encoder/XMLChoiceEncodingContainer.swift b/third-party/XMLCoder/Sources/Encoder/XMLChoiceEncodingContainer.swift new file mode 100644 index 0000000000..5a07e4d1d3 --- /dev/null +++ b/third-party/XMLCoder/Sources/Encoder/XMLChoiceEncodingContainer.swift @@ -0,0 +1,208 @@ +// Copyright (c) 2019-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Benjamin Wetherfield on 7/17/19. +// + +struct XMLChoiceEncodingContainer: KeyedEncodingContainerProtocol { + typealias Key = K + + // MARK: Properties + + /// A reference to the encoder we're writing to. + private let encoder: XMLEncoderImplementation + + /// A reference to the container we're writing to. + private var container: SharedBox + + /// The path of coding keys taken to get to this point in encoding. + public private(set) var codingPath: [CodingKey] + + // MARK: - Initialization + + /// Initializes `self` with the given references. + init( + referencing encoder: XMLEncoderImplementation, + codingPath: [CodingKey], + wrapping container: SharedBox + ) { + self.encoder = encoder + self.codingPath = codingPath + self.container = container + } + + // MARK: - Coding Path Operations + + private func converted(_ key: CodingKey) -> CodingKey { + switch encoder.options.keyEncodingStrategy { + case .useDefaultKeys: + return key + case .convertToSnakeCase: + let newKeyString = XMLEncoder.KeyEncodingStrategy + ._convertToSnakeCase(key.stringValue) + return XMLKey(stringValue: newKeyString, intValue: key.intValue) + case .convertToKebabCase: + let newKeyString = XMLEncoder.KeyEncodingStrategy + ._convertToKebabCase(key.stringValue) + return XMLKey(stringValue: newKeyString, intValue: key.intValue) + case let .custom(converter): + return converter(codingPath + [key]) + case .capitalized: + let newKeyString = XMLEncoder.KeyEncodingStrategy + ._convertToCapitalized(key.stringValue) + return XMLKey(stringValue: newKeyString, intValue: key.intValue) + case .uppercased: + let newKeyString = XMLEncoder.KeyEncodingStrategy + ._convertToUppercased(key.stringValue) + return XMLKey(stringValue: newKeyString, intValue: key.intValue) + case .lowercased: + let newKeyString = XMLEncoder.KeyEncodingStrategy + ._convertToLowercased(key.stringValue) + return XMLKey(stringValue: newKeyString, intValue: key.intValue) + } + } + + // MARK: - KeyedEncodingContainerProtocol Methods + + public mutating func encodeNil(forKey key: Key) throws { + container.withShared { + $0.key = converted(key).stringValue + $0.element = NullBox() + } + } + + public mutating func encode( + _ value: T, + forKey key: Key + ) throws { + return try encode(value, forKey: key) { encoder, value in + try encoder.box(value) + } + } + + private mutating func encode( + _ value: T, + forKey key: Key, + encode: (XMLEncoderImplementation, T) throws -> Box + ) throws { + defer { + _ = self.encoder.nodeEncodings.removeLast() + self.encoder.codingPath.removeLast() + } + encoder.codingPath.append(key) + let nodeEncodings = encoder.options.nodeEncodingStrategy.nodeEncodings( + forType: T.self, + with: encoder + ) + encoder.nodeEncodings.append(nodeEncodings) + let box = try encode(encoder, value) + + let oldSelf = self + let elementEncoder: (T, Key, Box) throws -> () = { _, key, box in + oldSelf.container.withShared { container in + container.element = box + container.key = oldSelf.converted(key).stringValue + } + } + + defer { + self = oldSelf + } + + try elementEncoder(value, key, box) + } + + public mutating func nestedContainer( + keyedBy _: NestedKey.Type, + forKey key: Key + ) -> KeyedEncodingContainer { + if NestedKey.self is XMLChoiceCodingKey.Type { + return nestedChoiceContainer(keyedBy: NestedKey.self, forKey: key) + } else { + return nestedKeyedContainer(keyedBy: NestedKey.self, forKey: key) + } + } + + mutating func nestedKeyedContainer( + keyedBy _: NestedKey.Type, + forKey key: Key + ) -> KeyedEncodingContainer { + let sharedKeyed = SharedBox(KeyedBox()) + + self.container.withShared { container in + container.element = sharedKeyed + container.key = converted(key).stringValue + } + + codingPath.append(key) + defer { self.codingPath.removeLast() } + + let container = XMLKeyedEncodingContainer( + referencing: encoder, + codingPath: codingPath, + wrapping: sharedKeyed + ) + return KeyedEncodingContainer(container) + } + + mutating func nestedChoiceContainer( + keyedBy _: NestedKey.Type, + forKey key: Key + ) -> KeyedEncodingContainer { + let sharedChoice = SharedBox(ChoiceBox()) + + self.container.withShared { container in + container.element = sharedChoice + container.key = converted(key).stringValue + } + + codingPath.append(key) + defer { self.codingPath.removeLast() } + + let container = XMLChoiceEncodingContainer( + referencing: encoder, + codingPath: codingPath, + wrapping: sharedChoice + ) + return KeyedEncodingContainer(container) + } + + public mutating func nestedUnkeyedContainer( + forKey key: Key + ) -> UnkeyedEncodingContainer { + let sharedUnkeyed = SharedBox(UnkeyedBox()) + + container.withShared { container in + container.element = sharedUnkeyed + container.key = converted(key).stringValue + } + + codingPath.append(key) + defer { self.codingPath.removeLast() } + return XMLUnkeyedEncodingContainer( + referencing: encoder, + codingPath: codingPath, + wrapping: sharedUnkeyed + ) + } + + public mutating func superEncoder() -> Encoder { + return XMLReferencingEncoder( + referencing: encoder, + key: XMLKey.super, + convertedKey: converted(XMLKey.super), + wrapping: container + ) + } + + public mutating func superEncoder(forKey key: Key) -> Encoder { + return XMLReferencingEncoder( + referencing: encoder, + key: key, + convertedKey: converted(key), + wrapping: container + ) + } +} diff --git a/third-party/XMLCoder/Sources/Encoder/XMLEncoder.swift b/third-party/XMLCoder/Sources/Encoder/XMLEncoder.swift new file mode 100644 index 0000000000..3444c7330e --- /dev/null +++ b/third-party/XMLCoder/Sources/Encoder/XMLEncoder.swift @@ -0,0 +1,451 @@ +// Copyright © 2017-2021 Shawn Moore and XMLCoder contributors. +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/22/17. +// + +import Foundation + +/// `XMLEncoder` facilitates the encoding of `Encodable` values into XML. +open class XMLEncoder { + // MARK: Options + + /// The formatting of the output XML data. + public struct OutputFormatting: OptionSet, Sendable { + /// The format's default value. + public let rawValue: UInt + + /// Creates an OutputFormatting value with the given raw value. + public init(rawValue: UInt) { + self.rawValue = rawValue + } + + /// Produce human-readable XML with indented output. + public static let prettyPrinted = OutputFormatting(rawValue: 1 << 0) + + /// Produce XML with keys sorted in lexicographic order. + public static let sortedKeys = OutputFormatting(rawValue: 1 << 1) + + /// Produce XML with no short-hand annotation for empty elements, e.g., use `

` over `

` + public static let noEmptyElements = OutputFormatting(rawValue: 1 << 2) + } + + /// The indentation to use when XML is pretty-printed. + public enum PrettyPrintIndentation { + case spaces(Int) + case tabs(Int) + } + + /// A node's encoding type. Specifies how a node will be encoded. + public enum NodeEncoding : Sendable { + case attribute + case element + case both + + public static let `default`: NodeEncoding = .element + } + + /// The strategy to use for encoding `Date` values. + public enum DateEncodingStrategy { + /// Defer to `Date` for choosing an encoding. This is the default strategy. + case deferredToDate + + /// Encode the `Date` as a UNIX timestamp (as a XML number). + case secondsSince1970 + + /// Encode the `Date` as UNIX millisecond timestamp (as a XML number). + case millisecondsSince1970 + + /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). + @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) + case iso8601 + + /// Encode the `Date` as a string formatted by the given formatter. + case formatted(DateFormatter) + + /// Encode the `Date` as a custom value encoded by the given closure. + /// + /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place. + case custom((Date, Encoder) throws -> ()) + } + + /// The strategy to use for encoding `String` values. + public enum StringEncodingStrategy { + /// Defer to `String` for choosing an encoding. This is the default strategy. + case deferredToString + + /// Encode the `String` as a CData-encoded string. + case cdata + } + + /// The strategy to use for encoding `Data` values. + public enum DataEncodingStrategy { + /// Defer to `Data` for choosing an encoding. + case deferredToData + + /// Encoded the `Data` as a Base64-encoded string. This is the default strategy. + case base64 + + /// Encode the `Data` as a custom value encoded by the given closure. + /// + /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place. + case custom((Data, Encoder) throws -> ()) + } + + /// The strategy to use for non-XML-conforming floating-point values (IEEE 754 infinity and NaN). + public enum NonConformingFloatEncodingStrategy { + /// Throw upon encountering non-conforming values. This is the default strategy. + case `throw` + + /// Encode the values using the given representation strings. + case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String) + } + + /// The strategy to use for automatically changing the value of keys before encoding. + public enum KeyEncodingStrategy { + /// Use the keys specified by each type. This is the default strategy. + case useDefaultKeys + + /// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key to XML payload. + /// + /// Capital characters are determined by testing membership in + /// `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters` + /// (Unicode General Categories Lu and Lt). + /// The conversion to lower case uses `Locale.system`, also known as + /// the ICU "root" locale. This means the result is consistent + /// regardless of the current user's locale and language preferences. + /// + /// Converting from camel case to snake case: + /// 1. Splits words at the boundary of lower-case to upper-case + /// 2. Inserts `_` between words + /// 3. Lowercases the entire string + /// 4. Preserves starting and ending `_`. + /// + /// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`. + /// + /// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted. + case convertToSnakeCase + + /// Same as convertToSnakeCase, but using `-` instead of `_` + /// For example, `oneTwoThree` becomes `one-two-three`. + case convertToKebabCase + + /// Capitalize the first letter only + /// `oneTwoThree` becomes `OneTwoThree` + case capitalized + + /// Uppercase ize all letters + /// `oneTwoThree` becomes `ONETWOTHREE` + case uppercased + + /// Lowercase all letters + /// `oneTwoThree` becomes `onetwothree` + case lowercased + + /// Provide a custom conversion to the key in the encoded XML from the + /// keys specified by the encoded types. + /// The full path to the current encoding position is provided for + /// context (in case you need to locate this key within the payload). + /// The returned key is used in place of the last component in the + /// coding path before encoding. + /// If the result of the conversion is a duplicate key, then only one + /// value will be present in the result. + case custom((_ codingPath: [CodingKey]) -> CodingKey) + + static func _convertToSnakeCase(_ stringKey: String) -> String { + return _convert(stringKey, usingSeparator: "_") + } + + static func _convertToKebabCase(_ stringKey: String) -> String { + return _convert(stringKey, usingSeparator: "-") + } + + static func _convert(_ stringKey: String, usingSeparator separator: String) -> String { + guard !stringKey.isEmpty else { + return stringKey + } + + var words: [Range] = [] + // The general idea of this algorithm is to split words on + // transition from lower to upper case, then on transition of >1 + // upper case characters to lowercase + // + // myProperty -> my_property + // myURLProperty -> my_url_property + // + // We assume, per Swift naming conventions, that the first character of the key is lowercase. + var wordStart = stringKey.startIndex + var searchRange = stringKey.index(after: wordStart)..1 capital letters. Turn those into a word, stopping at the capital before the lower case character. + let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound) + words.append(upperCaseRange.lowerBound.. String { + return stringKey.capitalizingFirstLetter() + } + + static func _convertToLowercased(_ stringKey: String) -> String { + return stringKey.lowercased() + } + + static func _convertToUppercased(_ stringKey: String) -> String { + return stringKey.uppercased() + } + } + + @available(*, deprecated, renamed: "NodeEncodingStrategy") + public typealias NodeEncodingStrategies = NodeEncodingStrategy + + public typealias XMLNodeEncoderClosure = @Sendable (CodingKey) -> NodeEncoding? + public typealias XMLEncodingClosure = @Sendable (Encodable.Type, Encoder) -> XMLNodeEncoderClosure + + /// Set of strategies to use for encoding of nodes. + public enum NodeEncodingStrategy { + /// Defer to `Encoder` for choosing an encoding. This is the default strategy. + case deferredToEncoder + + /// Return a closure computing the desired node encoding for the value by its coding key. + case custom(XMLEncodingClosure) + + func nodeEncodings( + forType codableType: Encodable.Type, + with encoder: Encoder + ) -> ((CodingKey) -> NodeEncoding?) { + return encoderClosure(codableType, encoder) + } + + var encoderClosure: XMLEncodingClosure { + switch self { + case .deferredToEncoder: return NodeEncodingStrategy.defaultEncoder + case let .custom(closure): return closure + } + } + + static let defaultEncoder: XMLEncodingClosure = { codableType, _ in + guard let dynamicType = codableType as? DynamicNodeEncoding.Type else { + return { _ in nil } + } + #if compiler(>=6.1) + return dynamicType.nodeEncoding(for:) + #else + return { (@Sendable key: CodingKey) in + dynamicType.nodeEncoding(for: key) + } + #endif + } + } + + /// Characters and their escaped representations to be escaped in attributes + open var charactersEscapedInAttributes = [ + ("&", "&"), + ("<", "<"), + (">", ">"), + ("'", "'"), + ("\"", """), + ] + + /// Characters and their escaped representations to be escaped in elements + open var charactersEscapedInElements = [ + ("&", "&"), + ("<", "<"), + (">", ">"), + ("'", "'"), + ("\"", """), + ] + + /// The output format to produce. Defaults to `[]`. + open var outputFormatting: OutputFormatting = [] + + /// The indentation to use when XML is printed. Defaults to `.spaces(4)`. + open var prettyPrintIndentation: PrettyPrintIndentation = .spaces(4) + + /// The strategy to use in encoding dates. Defaults to `.deferredToDate`. + open var dateEncodingStrategy: DateEncodingStrategy = .deferredToDate + + /// The strategy to use in encoding binary data. Defaults to `.base64`. + open var dataEncodingStrategy: DataEncodingStrategy = .base64 + + /// The strategy to use in encoding non-conforming numbers. Defaults to `.throw`. + open var nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy = .throw + + /// The strategy to use for encoding keys. Defaults to `.useDefaultKeys`. + open var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys + + /// The strategy to use in encoding encoding attributes. Defaults to `.deferredToEncoder`. + open var nodeEncodingStrategy: NodeEncodingStrategy = .deferredToEncoder + + /// The strategy to use in encoding strings. Defaults to `.deferredToString`. + open var stringEncodingStrategy: StringEncodingStrategy = .deferredToString + + /// Contextual user-provided information for use during encoding. + open var userInfo: [CodingUserInfoKey: Any] = [:] + + /// Options set on the top-level encoder to pass down the encoding hierarchy. + struct Options { + let dateEncodingStrategy: DateEncodingStrategy + let dataEncodingStrategy: DataEncodingStrategy + let nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy + let keyEncodingStrategy: KeyEncodingStrategy + let nodeEncodingStrategy: NodeEncodingStrategy + let stringEncodingStrategy: StringEncodingStrategy + let userInfo: [CodingUserInfoKey: Any] + } + + /// The options set on the top-level encoder. + var options: Options { + return Options(dateEncodingStrategy: dateEncodingStrategy, + dataEncodingStrategy: dataEncodingStrategy, + nonConformingFloatEncodingStrategy: nonConformingFloatEncodingStrategy, + keyEncodingStrategy: keyEncodingStrategy, + nodeEncodingStrategy: nodeEncodingStrategy, + stringEncodingStrategy: stringEncodingStrategy, + userInfo: userInfo) + } + + // MARK: - Constructing a XML Encoder + + /// Initializes `self` with default strategies. + public init() {} + + // MARK: - Encoding Values + + /// Encodes the given top-level value and returns its XML representation. + /// + /// - parameter value: The value to encode. + /// - parameter withRootKey: the key used to wrap the encoded values. The + /// default value is inferred from the name of the root type. + /// - parameter rootAttributes: the list of attributes to be added to the root node + /// - parameter header: the XML header to start the encoded data with. + /// - returns: A new `Data` value containing the encoded XML data. + /// - throws: `EncodingError.invalidValue` if a non-conforming + /// floating-point value is encountered during encoding, and the encoding + /// strategy is `.throw`. + /// - throws: An error if any value throws an error during encoding. + open func encode(_ value: T, + withRootKey rootKey: String? = nil, + rootAttributes: [String: String]? = nil, + header: XMLHeader? = nil, + doctype: XMLDocumentType? = nil) throws -> Data + { + let encoder = XMLEncoderImplementation(options: options, nodeEncodings: []) + encoder.nodeEncodings.append(options.nodeEncodingStrategy.nodeEncodings(forType: T.self, with: encoder)) + + let topLevel = try encoder.box(value) + let attributes = rootAttributes?.map(XMLCoderElement.Attribute.init) ?? [] + + let elementOrNone: XMLCoderElement? + + let rootKey = rootKey ?? "\(T.self)".convert(for: keyEncodingStrategy) + + let isStringBoxCDATA = stringEncodingStrategy == .cdata + + if let keyedBox = topLevel as? KeyedBox { + elementOrNone = XMLCoderElement( + key: rootKey, + isStringBoxCDATA: isStringBoxCDATA, + box: keyedBox, + attributes: attributes + ) + } else if let unkeyedBox = topLevel as? UnkeyedBox { + elementOrNone = XMLCoderElement( + key: rootKey, + isStringBoxCDATA: isStringBoxCDATA, + box: unkeyedBox, + attributes: attributes + ) + } else if let choiceBox = topLevel as? ChoiceBox { + elementOrNone = XMLCoderElement( + key: rootKey, + isStringBoxCDATA: isStringBoxCDATA, + box: choiceBox, + attributes: attributes + ) + } else { + fatalError("Unrecognized top-level element of type: \(type(of: topLevel))") + } + + guard let element = elementOrNone else { + throw EncodingError.invalidValue(value, EncodingError.Context( + codingPath: [], + debugDescription: "Unable to encode the given top-level value to XML." + )) + } + + return element.toXMLString( + with: header, + doctype: doctype, + escapedCharacters: ( + elements: charactersEscapedInElements, + attributes: charactersEscapedInAttributes + ), + formatting: outputFormatting, + indentation: prettyPrintIndentation + ).data(using: .utf8, allowLossyConversion: true)! + } + + // MARK: - TopLevelEncoder + + #if canImport(Combine) || canImport(OpenCombine) + open func encode(_ value: T) throws -> Data where T: Encodable { + return try encode(value, withRootKey: nil, rootAttributes: nil, header: nil) + } + #endif +} + +private extension String { + func convert(for encodingStrategy: XMLEncoder.KeyEncodingStrategy) -> String { + switch encodingStrategy { + case .useDefaultKeys: + return self + case .convertToSnakeCase: + return XMLEncoder.KeyEncodingStrategy._convertToSnakeCase(self) + case .convertToKebabCase: + return XMLEncoder.KeyEncodingStrategy._convertToKebabCase(self) + case .custom: + return self + case .capitalized: + return XMLEncoder.KeyEncodingStrategy._convertToCapitalized(self) + case .uppercased: + return XMLEncoder.KeyEncodingStrategy._convertToUppercased(self) + case .lowercased: + return XMLEncoder.KeyEncodingStrategy._convertToLowercased(self) + } + } +} diff --git a/third-party/XMLCoder/Sources/Encoder/XMLEncoderImplementation.swift b/third-party/XMLCoder/Sources/Encoder/XMLEncoderImplementation.swift new file mode 100644 index 0000000000..94e880cb2c --- /dev/null +++ b/third-party/XMLCoder/Sources/Encoder/XMLEncoderImplementation.swift @@ -0,0 +1,292 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/22/17. +// + +import Foundation + +class XMLEncoderImplementation: Encoder { + // MARK: Properties + + /// The encoder's storage. + var storage: XMLEncodingStorage + + /// Options set on the top-level encoder. + let options: XMLEncoder.Options + + /// The path to the current point in encoding. + public var codingPath: [CodingKey] + + public var nodeEncodings: [(CodingKey) -> XMLEncoder.NodeEncoding?] + + /// Contextual user-provided information for use during encoding. + public var userInfo: [CodingUserInfoKey: Any] { + return options.userInfo + } + + // MARK: - Initialization + + /// Initializes `self` with the given top-level encoder options. + init( + options: XMLEncoder.Options, + nodeEncodings: [(CodingKey) -> XMLEncoder.NodeEncoding?], + codingPath: [CodingKey] = [] + ) { + self.options = options + storage = XMLEncodingStorage() + self.codingPath = codingPath + self.nodeEncodings = nodeEncodings + } + + /// Returns whether a new element can be encoded at this coding path. + /// + /// `true` if an element has not yet been encoded at this coding path; `false` otherwise. + var canEncodeNewValue: Bool { + // Every time a new value gets encoded, the key it's encoded for is + // pushed onto the coding path (even if it's a nil key from an unkeyed container). + // At the same time, every time a container is requested, a new value + // gets pushed onto the storage stack. + // If there are more values on the storage stack than on the coding path, + // it means the value is requesting more than one container, which + // violates the precondition. + // + // This means that anytime something that can request a new container + // goes onto the stack, we MUST push a key onto the coding path. + // Things which will not request containers do not need to have the + // coding path extended for them (but it doesn't matter if it is, + // because they will not reach here). + return storage.count == codingPath.count + } + + // MARK: - Encoder Methods + + public func container(keyedBy _: Key.Type) -> KeyedEncodingContainer { + guard canEncodeNewValue else { + return mergeWithExistingKeyedContainer(keyedBy: Key.self) + } + if Key.self is XMLChoiceCodingKey.Type { + return choiceContainer(keyedBy: Key.self) + } else { + return keyedContainer(keyedBy: Key.self) + } + } + + public func unkeyedContainer() -> UnkeyedEncodingContainer { + // If an existing unkeyed container was already requested, return that one. + let topContainer: SharedBox + if canEncodeNewValue { + // We haven't yet pushed a container at this level; do so here. + topContainer = storage.pushUnkeyedContainer() + } else { + guard let container = storage.lastContainer as? SharedBox else { + preconditionFailure( + """ + Attempt to push new unkeyed encoding container when already previously encoded \ + at this path. + """ + ) + } + + topContainer = container + } + + return XMLUnkeyedEncodingContainer(referencing: self, codingPath: codingPath, wrapping: topContainer) + } + + public func singleValueContainer() -> SingleValueEncodingContainer { + return self + } + + private func keyedContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer { + let container = XMLKeyedEncodingContainer( + referencing: self, + codingPath: codingPath, + wrapping: storage.pushKeyedContainer() + ) + return KeyedEncodingContainer(container) + } + + private func choiceContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer { + let container = XMLChoiceEncodingContainer( + referencing: self, + codingPath: codingPath, + wrapping: storage.pushChoiceContainer() + ) + return KeyedEncodingContainer(container) + } + + private func mergeWithExistingKeyedContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer { + switch storage.lastContainer { + case let keyed as SharedBox: + let container = XMLKeyedEncodingContainer( + referencing: self, + codingPath: codingPath, + wrapping: keyed + ) + return KeyedEncodingContainer(container) + case let choice as SharedBox: + _ = storage.popContainer() + let keyed = KeyedBox( + elements: KeyedBox.Elements([choice.withShared { ($0.key, $0.element) }]), + attributes: [] + ) + let container = XMLKeyedEncodingContainer( + referencing: self, + codingPath: codingPath, + wrapping: storage.pushKeyedContainer(keyed) + ) + return KeyedEncodingContainer(container) + default: + preconditionFailure( + """ + No existing keyed encoding container to merge with. + """ + ) + } + } +} + +extension XMLEncoderImplementation { + /// Returns the given value boxed in a container appropriate for pushing onto the container stack. + func box() -> SimpleBox { + return NullBox() + } + + func box(_ value: Bool) -> SimpleBox { + return BoolBox(value) + } + + func box(_ value: Decimal) -> SimpleBox { + return DecimalBox(value) + } + + func box(_ value: T) -> SimpleBox { + return IntBox(value) + } + + func box(_ value: T) -> SimpleBox { + return UIntBox(value) + } + + func box(_ value: Float) throws -> SimpleBox { + return try box(value, FloatBox.self) + } + + func box(_ value: Double) throws -> SimpleBox { + return try box(value, DoubleBox.self) + } + + func box( + _ value: T, + _: B.Type + ) throws -> SimpleBox where B.Unboxed == T { + guard value.isInfinite || value.isNaN else { + return B(value) + } + guard case let .convertToString( + positiveInfinity: posInfString, + negativeInfinity: negInfString, + nan: nanString + ) = options.nonConformingFloatEncodingStrategy else { + throw EncodingError._invalidFloatingPointValue(value, at: codingPath) + } + if value == T.infinity { + return StringBox(posInfString) + } else if value == -T.infinity { + return StringBox(negInfString) + } else { + return StringBox(nanString) + } + } + + func box(_ value: String) -> SimpleBox { + return StringBox(value) + } + + func box(_ value: Date) throws -> Box { + switch options.dateEncodingStrategy { + case .deferredToDate: + try value.encode(to: self) + return storage.popContainer() + case .secondsSince1970: + return DateBox(value, format: .secondsSince1970) + case .millisecondsSince1970: + return DateBox(value, format: .millisecondsSince1970) + case .iso8601: + return DateBox(value, format: .iso8601) + case let .formatted(formatter): + return DateBox(value, format: .formatter(formatter)) + case let .custom(closure): + let depth = storage.count + try closure(value, self) + + guard storage.count > depth else { + return KeyedBox() + } + + return storage.popContainer() + } + } + + func box(_ value: Data) throws -> Box { + switch options.dataEncodingStrategy { + case .deferredToData: + try value.encode(to: self) + return storage.popContainer() + case .base64: + return DataBox(value, format: .base64) + case let .custom(closure): + let depth = storage.count + try closure(value, self) + + guard storage.count > depth else { + return KeyedBox() + } + + return storage.popContainer() + } + } + + func box(_ value: URL) -> SimpleBox { + return URLBox(value) + } + + func box(_ value: T) throws -> Box { + if T.self == Date.self || T.self == NSDate.self, + let value = value as? Date + { + return try box(value) + } else if T.self == Data.self || T.self == NSData.self, + let value = value as? Data + { + return try box(value) + } else if T.self == URL.self || T.self == NSURL.self, + let value = value as? URL + { + return box(value) + } else if T.self == Decimal.self || T.self == NSDecimalNumber.self, + let value = value as? Decimal + { + return box(value) + } + + let depth = storage.count + try value.encode(to: self) + + // The top container should be a new container. + guard storage.count > depth else { + return KeyedBox() + } + + let lastContainer = storage.popContainer() + + guard let sharedBox = lastContainer as? TypeErasedSharedBoxProtocol else { + return lastContainer + } + + return sharedBox.typeErasedUnbox() + } +} diff --git a/third-party/XMLCoder/Sources/Encoder/XMLEncodingStorage.swift b/third-party/XMLCoder/Sources/Encoder/XMLEncodingStorage.swift new file mode 100644 index 0000000000..9a33a9a1eb --- /dev/null +++ b/third-party/XMLCoder/Sources/Encoder/XMLEncodingStorage.swift @@ -0,0 +1,66 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/22/17. +// + +import Foundation + +// MARK: - Encoding Storage and Containers + +struct XMLEncodingStorage { + // MARK: Properties + + /// The container stack. + private var containers: [Box] = [] + + // MARK: - Initialization + + /// Initializes `self` with no containers. + init() {} + + // MARK: - Modifying the Stack + + var count: Int { + return containers.count + } + + var lastContainer: Box? { + return containers.last + } + + mutating func pushKeyedContainer(_ keyedBox: KeyedBox = KeyedBox()) -> SharedBox { + let container = SharedBox(keyedBox) + containers.append(container) + return container + } + + mutating func pushChoiceContainer() -> SharedBox { + let container = SharedBox(ChoiceBox()) + containers.append(container) + return container + } + + mutating func pushUnkeyedContainer() -> SharedBox { + let container = SharedBox(UnkeyedBox()) + containers.append(container) + return container + } + + mutating func push(container: Box) { + if let keyedBox = container as? KeyedBox { + containers.append(SharedBox(keyedBox)) + } else if let unkeyedBox = container as? UnkeyedBox { + containers.append(SharedBox(unkeyedBox)) + } else { + containers.append(container) + } + } + + mutating func popContainer() -> Box { + precondition(!containers.isEmpty, "Empty container stack.") + return containers.popLast()! + } +} diff --git a/third-party/XMLCoder/Sources/Encoder/XMLKeyedEncodingContainer.swift b/third-party/XMLCoder/Sources/Encoder/XMLKeyedEncodingContainer.swift new file mode 100644 index 0000000000..441d7ff40b --- /dev/null +++ b/third-party/XMLCoder/Sources/Encoder/XMLKeyedEncodingContainer.swift @@ -0,0 +1,267 @@ +// Copyright (c) 2018-2021 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 11/20/18. +// + +import Foundation + +struct XMLKeyedEncodingContainer: KeyedEncodingContainerProtocol { + typealias Key = K + + // MARK: Properties + + /// A reference to the encoder we're writing to. + private let encoder: XMLEncoderImplementation + + /// A reference to the container we're writing to. + private var container: SharedBox + + /// The path of coding keys taken to get to this point in encoding. + public private(set) var codingPath: [CodingKey] + + // MARK: - Initialization + + /// Initializes `self` with the given references. + init( + referencing encoder: XMLEncoderImplementation, + codingPath: [CodingKey], + wrapping container: SharedBox + ) { + self.encoder = encoder + self.codingPath = codingPath + self.container = container + } + + // MARK: - Coding Path Operations + + private func converted(_ key: CodingKey) -> CodingKey { + switch encoder.options.keyEncodingStrategy { + case .useDefaultKeys: + return key + case .convertToSnakeCase: + let newKeyString = XMLEncoder.KeyEncodingStrategy + ._convertToSnakeCase(key.stringValue) + return XMLKey(stringValue: newKeyString, intValue: key.intValue) + case .convertToKebabCase: + let newKeyString = XMLEncoder.KeyEncodingStrategy + ._convertToKebabCase(key.stringValue) + return XMLKey(stringValue: newKeyString, intValue: key.intValue) + case let .custom(converter): + return converter(codingPath + [key]) + case .capitalized: + let newKeyString = XMLEncoder.KeyEncodingStrategy + ._convertToCapitalized(key.stringValue) + return XMLKey(stringValue: newKeyString, intValue: key.intValue) + case .uppercased: + let newKeyString = XMLEncoder.KeyEncodingStrategy + ._convertToUppercased(key.stringValue) + return XMLKey(stringValue: newKeyString, intValue: key.intValue) + case .lowercased: + let newKeyString = XMLEncoder.KeyEncodingStrategy + ._convertToLowercased(key.stringValue) + return XMLKey(stringValue: newKeyString, intValue: key.intValue) + } + } + + // MARK: - KeyedEncodingContainerProtocol Methods + + public mutating func encodeNil(forKey key: Key) throws { + container.withShared { + $0.elements.append(NullBox(), at: converted(key).stringValue) + } + } + + public mutating func encode( + _ value: T, + forKey key: Key + ) throws { + return try encode(value, forKey: key) { encoder, value in + try encoder.box(value) + } + } + + private mutating func encode( + _ value: T, + forKey key: Key, + encode: (XMLEncoderImplementation, T) throws -> Box + ) throws { + defer { + _ = self.encoder.nodeEncodings.removeLast() + self.encoder.codingPath.removeLast() + } + guard let strategy = encoder.nodeEncodings.last else { + preconditionFailure( + "Attempt to access node encoding strategy from empty stack." + ) + } + encoder.codingPath.append(key) + let nodeEncodings = encoder.options.nodeEncodingStrategy.nodeEncodings( + forType: T.self, + with: encoder + ) + encoder.nodeEncodings.append(nodeEncodings) + let box = try encode(encoder, value) + + let oldSelf = self + let attributeEncoder: (T, Key, Box) throws -> () = { value, key, box in + guard let attribute = box as? SimpleBox else { + throw EncodingError.invalidValue(value, EncodingError.Context( + codingPath: [], + debugDescription: "Complex values cannot be encoded as attributes." + )) + } + oldSelf.container.withShared { container in + container.attributes.append(attribute, at: oldSelf.converted(key).stringValue) + } + } + + let elementEncoder: (T, Key, Box) throws -> () = { _, key, box in + oldSelf.container.withShared { container in + container.elements.append(box, at: oldSelf.converted(key).stringValue) + } + } + + defer { + self = oldSelf + } + + switch strategy(key) { + case .attribute?: + try attributeEncoder(value, key, box) + case .element?: + try elementEncoder(value, key, box) + case .both?: + try attributeEncoder(value, key, box) + try elementEncoder(value, key, box) + default: + switch value { + case is XMLElementProtocol: + encodeElement(forKey: key, box: box) + case is XMLAttributeProtocol: + try encodeAttribute(value, forKey: key, box: box) + case is XMLElementAndAttributeProtocol: + try encodeAttribute(value, forKey: key, box: box) + encodeElement(forKey: key, box: box) + default: + encodeElement(forKey: key, box: box) + } + } + } + + private mutating func encodeAttribute( + _ value: T, + forKey key: Key, + box: Box + ) throws { + guard let attribute = box as? SimpleBox else { + throw EncodingError.invalidValue(value, EncodingError.Context( + codingPath: [], + debugDescription: "Complex values cannot be encoded as attributes." + )) + } + container.withShared { container in + container.attributes.append(attribute, at: self.converted(key).stringValue) + } + } + + private mutating func encodeElement( + forKey key: Key, + box: Box + ) { + container.withShared { container in + container.elements.append(box, at: self.converted(key).stringValue) + } + } + + public mutating func nestedContainer( + keyedBy _: NestedKey.Type, + forKey key: Key + ) -> KeyedEncodingContainer { + if NestedKey.self is XMLChoiceCodingKey.Type { + return nestedChoiceContainer(keyedBy: NestedKey.self, forKey: key) + } else { + return nestedKeyedContainer(keyedBy: NestedKey.self, forKey: key) + } + } + + mutating func nestedKeyedContainer( + keyedBy _: NestedKey.Type, + forKey key: Key + ) -> KeyedEncodingContainer { + let sharedKeyed = SharedBox(KeyedBox()) + + self.container.withShared { container in + container.elements.append(sharedKeyed, at: converted(key).stringValue) + } + + codingPath.append(key) + defer { self.codingPath.removeLast() } + + let container = XMLKeyedEncodingContainer( + referencing: encoder, + codingPath: codingPath, + wrapping: sharedKeyed + ) + return KeyedEncodingContainer(container) + } + + mutating func nestedChoiceContainer( + keyedBy _: NestedKey.Type, + forKey key: Key + ) -> KeyedEncodingContainer { + let sharedChoice = SharedBox(ChoiceBox()) + + self.container.withShared { container in + container.elements.append(sharedChoice, at: converted(key).stringValue) + } + + codingPath.append(key) + defer { self.codingPath.removeLast() } + + let container = XMLChoiceEncodingContainer( + referencing: encoder, + codingPath: codingPath, + wrapping: sharedChoice + ) + return KeyedEncodingContainer(container) + } + + public mutating func nestedUnkeyedContainer( + forKey key: Key + ) -> UnkeyedEncodingContainer { + let sharedUnkeyed = SharedBox(UnkeyedBox()) + + container.withShared { container in + container.elements.append(sharedUnkeyed, at: converted(key).stringValue) + } + + codingPath.append(key) + defer { self.codingPath.removeLast() } + return XMLUnkeyedEncodingContainer( + referencing: encoder, + codingPath: codingPath, + wrapping: sharedUnkeyed + ) + } + + public mutating func superEncoder() -> Encoder { + return XMLReferencingEncoder( + referencing: encoder, + key: XMLKey.super, + convertedKey: converted(XMLKey.super), + wrapping: container + ) + } + + public mutating func superEncoder(forKey key: Key) -> Encoder { + return XMLReferencingEncoder( + referencing: encoder, + key: key, + convertedKey: converted(key), + wrapping: container + ) + } +} diff --git a/third-party/XMLCoder/Sources/Encoder/XMLReferencingEncoder.swift b/third-party/XMLCoder/Sources/Encoder/XMLReferencingEncoder.swift new file mode 100644 index 0000000000..ff88334717 --- /dev/null +++ b/third-party/XMLCoder/Sources/Encoder/XMLReferencingEncoder.swift @@ -0,0 +1,130 @@ +// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Shawn Moore on 11/25/17. +// + +import Foundation + +/// XMLReferencingEncoder is a special subclass of _XMLEncoder which has its +/// own storage, but references the contents of a different encoder. +/// It's used in superEncoder(), which returns a new encoder for encoding a +// superclass -- the lifetime of the encoder should not escape the scope it's +/// created in, but it doesn't necessarily know when it's done being used +/// (to write to the original container). +class XMLReferencingEncoder: XMLEncoderImplementation { + // MARK: Reference types. + + /// The type of container we're referencing. + private enum Reference { + /// Referencing a specific index in an unkeyed container. + case unkeyed(SharedBox, Int) + + /// Referencing a specific key in a keyed container. + case keyed(SharedBox, String) + + /// Referencing a specific key in a keyed container. + case choice(SharedBox, String) + } + + // MARK: - Properties + + /// The encoder we're referencing. + let encoder: XMLEncoderImplementation + + /// The container reference itself. + private let reference: Reference + + // MARK: - Initialization + + /// Initializes `self` by referencing the given array container in the given encoder. + init( + referencing encoder: XMLEncoderImplementation, + at index: Int, + wrapping sharedUnkeyed: SharedBox + ) { + self.encoder = encoder + reference = .unkeyed(sharedUnkeyed, index) + super.init( + options: encoder.options, + nodeEncodings: encoder.nodeEncodings, + codingPath: encoder.codingPath + ) + + codingPath.append(XMLKey(index: index)) + } + + /// Initializes `self` by referencing the given dictionary container in the given encoder. + init( + referencing encoder: XMLEncoderImplementation, + key: CodingKey, + convertedKey: CodingKey, + wrapping sharedKeyed: SharedBox + ) { + self.encoder = encoder + reference = .keyed(sharedKeyed, convertedKey.stringValue) + super.init( + options: encoder.options, + nodeEncodings: encoder.nodeEncodings, + codingPath: encoder.codingPath + ) + + codingPath.append(key) + } + + init( + referencing encoder: XMLEncoderImplementation, + key: CodingKey, + convertedKey: CodingKey, + wrapping sharedKeyed: SharedBox + ) { + self.encoder = encoder + reference = .choice(sharedKeyed, convertedKey.stringValue) + super.init( + options: encoder.options, + nodeEncodings: encoder.nodeEncodings, + codingPath: encoder.codingPath + ) + + codingPath.append(key) + } + + // MARK: - Coding Path Operations + + override var canEncodeNewValue: Bool { + // With a regular encoder, the storage and coding path grow together. + // A referencing encoder, however, inherits its parents coding path, as well as the key it was created for. + // We have to take this into account. + return storage.count == codingPath.count - encoder.codingPath.count - 1 + } + + // MARK: - Deinitialization + + // Finalizes `self` by writing the contents of our storage to the referenced encoder's storage. + deinit { + let box: Box + switch self.storage.count { + case 0: box = KeyedBox() + case 1: box = self.storage.popContainer() + default: fatalError("Referencing encoder deallocated with multiple containers on stack.") + } + + switch self.reference { + case let .unkeyed(sharedUnkeyedBox, index): + sharedUnkeyedBox.withShared { unkeyedBox in + unkeyedBox.insert(box, at: index) + } + case let .keyed(sharedKeyedBox, key): + sharedKeyedBox.withShared { keyedBox in + keyedBox.elements.append(box, at: key) + } + case let .choice(sharedChoiceBox, key): + sharedChoiceBox.withShared { choiceBox in + choiceBox.element = box + choiceBox.key = key + } + } + } +} diff --git a/third-party/XMLCoder/Sources/Encoder/XMLUnkeyedEncodingContainer.swift b/third-party/XMLCoder/Sources/Encoder/XMLUnkeyedEncodingContainer.swift new file mode 100644 index 0000000000..87f72dc3c0 --- /dev/null +++ b/third-party/XMLCoder/Sources/Encoder/XMLUnkeyedEncodingContainer.swift @@ -0,0 +1,134 @@ +// Copyright (c) 2018-2020 XMLCoder contributors +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +// +// Created by Vincent Esche on 11/20/18. +// + +import Foundation + +struct XMLUnkeyedEncodingContainer: UnkeyedEncodingContainer { + // MARK: Properties + + /// A reference to the encoder we're writing to. + private let encoder: XMLEncoderImplementation + + /// A reference to the container we're writing to. + private let container: SharedBox + + /// The path of coding keys taken to get to this point in encoding. + public private(set) var codingPath: [CodingKey] + + /// The number of elements encoded into the container. + public var count: Int { + return container.withShared { $0.count } + } + + // MARK: - Initialization + + /// Initializes `self` with the given references. + init( + referencing encoder: XMLEncoderImplementation, + codingPath: [CodingKey], + wrapping container: SharedBox + ) { + self.encoder = encoder + self.codingPath = codingPath + self.container = container + } + + // MARK: - UnkeyedEncodingContainer Methods + + public mutating func encodeNil() throws { + container.withShared { container in + container.append(encoder.box()) + } + } + + public mutating func encode(_ value: T) throws { + try encode(value) { encoder, value in + try encoder.box(value) + } + } + + private mutating func encode( + _ value: T, + encode: (XMLEncoderImplementation, T) throws -> Box + ) rethrows { + encoder.codingPath.append(XMLKey(index: count)) + defer { self.encoder.codingPath.removeLast() } + + try container.withShared { container in + container.append(try encode(encoder, value)) + } + } + + public mutating func nestedContainer( + keyedBy _: NestedKey.Type + ) -> KeyedEncodingContainer { + if NestedKey.self is XMLChoiceCodingKey.Type { + return nestedChoiceContainer(keyedBy: NestedKey.self) + } else { + return nestedKeyedContainer(keyedBy: NestedKey.self) + } + } + + public mutating func nestedKeyedContainer(keyedBy _: NestedKey.Type) -> KeyedEncodingContainer { + codingPath.append(XMLKey(index: count)) + defer { self.codingPath.removeLast() } + + let sharedKeyed = SharedBox(KeyedBox()) + self.container.withShared { container in + container.append(sharedKeyed) + } + + let container = XMLKeyedEncodingContainer( + referencing: encoder, + codingPath: codingPath, + wrapping: sharedKeyed + ) + return KeyedEncodingContainer(container) + } + + public mutating func nestedChoiceContainer(keyedBy _: NestedKey.Type) -> KeyedEncodingContainer { + codingPath.append(XMLKey(index: count)) + defer { self.codingPath.removeLast() } + + let sharedChoice = SharedBox(ChoiceBox()) + self.container.withShared { container in + container.append(sharedChoice) + } + + let container = XMLChoiceEncodingContainer( + referencing: encoder, + codingPath: codingPath, + wrapping: sharedChoice + ) + return KeyedEncodingContainer(container) + } + + public mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + codingPath.append(XMLKey(index: count)) + defer { self.codingPath.removeLast() } + + let sharedUnkeyed = SharedBox(UnkeyedBox()) + container.withShared { container in + container.append(sharedUnkeyed) + } + + return XMLUnkeyedEncodingContainer( + referencing: encoder, + codingPath: codingPath, + wrapping: sharedUnkeyed + ) + } + + public mutating func superEncoder() -> Encoder { + return XMLReferencingEncoder( + referencing: encoder, + at: count, + wrapping: container + ) + } +} diff --git a/third-party/openh264/BUILD b/third-party/openh264/BUILD index 46940c42c9..11f3b912e1 100644 --- a/third-party/openh264/BUILD +++ b/third-party/openh264/BUILD @@ -270,3 +270,72 @@ cc_library( "//visibility:public", ], ) + +cc_library( + name = "openh264_decoder", + srcs = [ + "third_party/openh264/src/codec/decoder/core/src/au_parser.cpp", + "third_party/openh264/src/codec/decoder/core/src/bit_stream.cpp", + "third_party/openh264/src/codec/decoder/core/src/cabac_decoder.cpp", + "third_party/openh264/src/codec/decoder/core/src/deblocking.cpp", + "third_party/openh264/src/codec/decoder/core/src/decode_mb_aux.cpp", + "third_party/openh264/src/codec/decoder/core/src/decode_slice.cpp", + "third_party/openh264/src/codec/decoder/core/src/decoder.cpp", + "third_party/openh264/src/codec/decoder/core/src/decoder_core.cpp", + "third_party/openh264/src/codec/decoder/core/src/decoder_data_tables.cpp", + "third_party/openh264/src/codec/decoder/core/src/error_concealment.cpp", + "third_party/openh264/src/codec/decoder/core/src/fmo.cpp", + "third_party/openh264/src/codec/decoder/core/src/get_intra_predictor.cpp", + "third_party/openh264/src/codec/decoder/core/src/manage_dec_ref.cpp", + "third_party/openh264/src/codec/decoder/core/src/memmgr_nal_unit.cpp", + "third_party/openh264/src/codec/decoder/core/src/mv_pred.cpp", + "third_party/openh264/src/codec/decoder/core/src/parse_mb_syn_cabac.cpp", + "third_party/openh264/src/codec/decoder/core/src/parse_mb_syn_cavlc.cpp", + "third_party/openh264/src/codec/decoder/core/src/pic_queue.cpp", + "third_party/openh264/src/codec/decoder/core/src/rec_mb.cpp", + "third_party/openh264/src/codec/decoder/core/src/wels_decoder_thread.cpp", + "third_party/openh264/src/codec/decoder/plus/src/welsDecoderExt.cpp", + "third_party/openh264/src/codec/decoder/core/inc/au_parser.h", + "third_party/openh264/src/codec/decoder/core/inc/bit_stream.h", + "third_party/openh264/src/codec/decoder/core/inc/cabac_decoder.h", + "third_party/openh264/src/codec/decoder/core/inc/deblocking.h", + "third_party/openh264/src/codec/decoder/core/inc/dec_frame.h", + "third_party/openh264/src/codec/decoder/core/inc/dec_golomb.h", + "third_party/openh264/src/codec/decoder/core/inc/decode_mb_aux.h", + "third_party/openh264/src/codec/decoder/core/inc/decode_slice.h", + "third_party/openh264/src/codec/decoder/core/inc/decoder.h", + "third_party/openh264/src/codec/decoder/core/inc/decoder_context.h", + "third_party/openh264/src/codec/decoder/core/inc/decoder_core.h", + "third_party/openh264/src/codec/decoder/core/inc/error_code.h", + "third_party/openh264/src/codec/decoder/core/inc/error_concealment.h", + "third_party/openh264/src/codec/decoder/core/inc/fmo.h", + "third_party/openh264/src/codec/decoder/core/inc/get_intra_predictor.h", + "third_party/openh264/src/codec/decoder/core/inc/manage_dec_ref.h", + "third_party/openh264/src/codec/decoder/core/inc/mb_cache.h", + "third_party/openh264/src/codec/decoder/core/inc/memmgr_nal_unit.h", + "third_party/openh264/src/codec/decoder/core/inc/mv_pred.h", + "third_party/openh264/src/codec/decoder/core/inc/nal_prefix.h", + "third_party/openh264/src/codec/decoder/core/inc/nalu.h", + "third_party/openh264/src/codec/decoder/core/inc/parameter_sets.h", + "third_party/openh264/src/codec/decoder/core/inc/parse_mb_syn_cabac.h", + "third_party/openh264/src/codec/decoder/core/inc/parse_mb_syn_cavlc.h", + "third_party/openh264/src/codec/decoder/core/inc/pic_queue.h", + "third_party/openh264/src/codec/decoder/core/inc/picture.h", + "third_party/openh264/src/codec/decoder/core/inc/rec_mb.h", + "third_party/openh264/src/codec/decoder/core/inc/slice.h", + "third_party/openh264/src/codec/decoder/core/inc/vlc_decoder.h", + "third_party/openh264/src/codec/decoder/core/inc/wels_common_basis.h", + "third_party/openh264/src/codec/decoder/plus/inc/welsDecoderExt.h", + ], + includes = ["."], + copts = [ + "-Ithird-party/openh264/third_party/openh264/src/codec/decoder/core/inc", + "-Ithird-party/openh264/third_party/openh264/src/codec/decoder/plus/inc", + "-Ithird-party/openh264/third_party/openh264/src/codec/common/inc", + "-Ithird-party/openh264/third_party/openh264/src/codec/api/wels", + "-Os", + "-Wno-all", + ], + deps = [":openh264"], + visibility = ["//visibility:public"], +) 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; /** 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/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/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; } 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/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); 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; 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); 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) 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; diff --git a/third-party/subcodec/.gitignore b/third-party/subcodec/.gitignore new file mode 100644 index 0000000000..e31c357df5 --- /dev/null +++ b/third-party/subcodec/.gitignore @@ -0,0 +1,11 @@ +build/ +build-*/ +build_*/ +.build/ +.swiftpm/ +*.h264 +*.yuv +*.mbs +*.trace/ +Tests/SubcodecTests/Fixtures/ +.DS_Store diff --git a/third-party/subcodec/BUILD b/third-party/subcodec/BUILD new file mode 100644 index 0000000000..1a9d598989 --- /dev/null +++ b/third-party/subcodec/BUILD @@ -0,0 +1,84 @@ +cc_library( + name = "h264bitstream", + srcs = [ + "third_party/h264bitstream/h264_stream.c", + "third_party/h264bitstream/h264_nal.c", + "third_party/h264bitstream/h264_sei.c", + ], + hdrs = glob(["third_party/h264bitstream/*.h"]), + includes = ["third_party/h264bitstream"], + copts = ["-Wno-all"], +) + +cc_library( + name = "subcodec", + srcs = [ + "src/frame_writer.cpp", + "src/cavlc.cpp", + "src/h264_parser.cpp", + "src/sprite_data.cpp", + "src/mbs_encode.cpp", + "src/mbs_mux_common.cpp", + "src/mux_surface.cpp", + ], + hdrs = glob(["src/*.h"]), + includes = ["src"], + copts = [ + "-std=c++2b", + "-Wno-all", + ], + deps = [":h264bitstream"], +) + +cc_library( + name = "sprite_encode", + srcs = [ + "src/sprite_encode.cpp", + "src/sprite_extractor.cpp", + ], + hdrs = glob(["src/*.h"]), + includes = ["src"], + copts = [ + "-std=c++2b", + "-Wno-all", + "-Ithird-party/openh264/third_party/openh264/src/codec/api/wels", + ], + deps = [ + ":subcodec", + "//third-party/openh264:openh264", + ], +) + +objc_library( + name = "SubcodecObjC", + module_name = "SubcodecObjC", + enable_modules = True, + srcs = [ + "Sources/SubcodecObjC/SCSprite.mm", + "Sources/SubcodecObjC/SCMuxSurface.mm", + "Sources/SubcodecObjC/SCSpriteRegion.mm", + "Sources/SubcodecObjC/SCDecodedFrame.mm", + "Sources/SubcodecObjC/SCOpenH264Decoder.mm", + "Sources/SubcodecObjC/SCVideoToolboxDecoder.mm", + "Sources/SubcodecObjC/AnnexBSplitter.h", + ], + hdrs = glob(["Sources/SubcodecObjC/include/*.h"]), + includes = ["Sources/SubcodecObjC/include"], + copts = [ + "-std=c++2b", + "-Wno-all", + "-Ithird-party/openh264/third_party/openh264/src/codec/api/wels", + ], + deps = [ + ":subcodec", + ":sprite_encode", + "//third-party/openh264:openh264", + "//third-party/openh264:openh264_decoder", + ], + sdk_frameworks = [ + "VideoToolbox", + "CoreMedia", + "CoreVideo", + ], + visibility = ["//visibility:public"], +) 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